From 9ae682a9c0fbacd3a831d0f909491794abffe783 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:06:12 -0800 Subject: [PATCH] Enhancement: support jellyfin 10.12 breaking API changes --- docs/widgets/services/jellyfin.md | 6 +++ src/components/services/widget/container.jsx | 1 + src/utils/config/service-helpers.js | 1 + src/utils/proxy/handlers/credentialed.js | 2 + src/widgets/components.js | 2 +- src/widgets/emby/component.jsx | 18 ++++++--- src/widgets/jellyfin/component.jsx | 3 ++ src/widgets/jellyfin/widget.js | 42 ++++++++++++++++++++ src/widgets/widgets.js | 3 +- 9 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src/widgets/jellyfin/component.jsx create mode 100644 src/widgets/jellyfin/widget.js diff --git a/docs/widgets/services/jellyfin.md b/docs/widgets/services/jellyfin.md index 667930e3c..8849e0645 100644 --- a/docs/widgets/services/jellyfin.md +++ b/docs/widgets/services/jellyfin.md @@ -9,11 +9,17 @@ You can create an API key from inside Jellyfin at `Settings > Advanced > Api Key As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "songs"]`. These blocks are disabled by default but can be enabled with the `enableBlocks` option, and the "Now Playing" feature (enabled by default) can be disabled with the `enableNowPlaying` option. +| Jellyfin Version | Homepage Widget Version | +| ---------------- | ----------------------- | +| < 10.12 | 1 (default) | +| >= 10.12 | 2 | + ```yaml widget: type: jellyfin url: http://jellyfin.host.or.ip key: apikeyapikeyapikeyapikeyapikey + version: 2 # optional, default is 1 enableBlocks: true # optional, defaults to false enableNowPlaying: true # optional, defaults to true enableUser: true # optional, defaults to false diff --git a/src/components/services/widget/container.jsx b/src/components/services/widget/container.jsx index e5962e445..66434cfc2 100644 --- a/src/components/services/widget/container.jsx +++ b/src/components/services/widget/container.jsx @@ -9,6 +9,7 @@ import { buildHighlightConfig } from "utils/highlights"; const ALIASED_WIDGETS = { pialert: "netalertx", hoarder: "karakeep", + jellyfin: "emby", }; export default function Container({ error = false, children, service }) { diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 2d477db57..d6fb2ed6f 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -556,6 +556,7 @@ export function cleanServiceGroups(groups) { "beszel", "glances", "immich", + "jellyfin", "komga", "mealie", "pfsense", diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 3c61aa00e..13a8f90c8 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -123,6 +123,8 @@ export default async function credentialedProxyHandler(req, res, map) { // v1 does not require a key headers.Authorization = `Bearer ${widget.key}`; } + } else if (widget.type === "jellyfin") { + headers["Authorization"] = `MediaBrowser Token=${widget.key}`; } else { headers["X-API-Key"] = `${widget.key}`; } diff --git a/src/widgets/components.js b/src/widgets/components.js index 911be5fb8..beb04e92e 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -60,7 +60,7 @@ const components = { immich: dynamic(() => import("./immich/component")), jackett: dynamic(() => import("./jackett/component")), jdownloader: dynamic(() => import("./jdownloader/component")), - jellyfin: dynamic(() => import("./emby/component")), + jellyfin: dynamic(() => import("./jellyfin/component")), jellyseerr: dynamic(() => import("./jellyseerr/component")), jellystat: dynamic(() => import("./jellystat/component")), kavita: dynamic(() => import("./kavita/component")), diff --git a/src/widgets/emby/component.jsx b/src/widgets/emby/component.jsx index b03a00415..1f6c9b4dc 100644 --- a/src/widgets/emby/component.jsx +++ b/src/widgets/emby/component.jsx @@ -176,9 +176,6 @@ function SessionEntry({ playCommand, session, enableUser, showEpisodeNumber, ena function CountBlocks({ service, countData }) { const { t } = useTranslation(); - // allows filtering - // eslint-disable-next-line no-param-reassign - if (service.widget?.type === "jellyfin") service.widget.type = "emby"; if (!countData) { return ( @@ -205,22 +202,31 @@ export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; + const version = widget?.version ?? 1; + const useJellyfinV2 = widget?.type === "jellyfin" && version === 2; + const sessionsEndpoint = useJellyfinV2 ? "SessionsV2" : "Sessions"; + const countEndpoint = useJellyfinV2 ? "CountV2" : "Count"; + const commandMap = { + Pause: useJellyfinV2 ? "PauseV2" : "Pause", + Unpause: useJellyfinV2 ? "UnpauseV2" : "Unpause", + }; const enableNowPlaying = service.widget?.enableNowPlaying ?? true; const { data: sessionsData, error: sessionsError, mutate: sessionMutate, - } = useWidgetAPI(widget, enableNowPlaying ? "Sessions" : "", { + } = useWidgetAPI(widget, enableNowPlaying ? sessionsEndpoint : "", { refreshInterval: enableNowPlaying ? 5000 : undefined, }); - const { data: countData, error: countError } = useWidgetAPI(widget, "Count", { + const { data: countData, error: countError } = useWidgetAPI(widget, countEndpoint, { refreshInterval: 60000, }); async function handlePlayCommand(session, command) { - const params = getURLSearchParams(widget, command); + const mappedCommand = commandMap[command] ?? command; + const params = getURLSearchParams(widget, mappedCommand); params.append( "segments", JSON.stringify({ diff --git a/src/widgets/jellyfin/component.jsx b/src/widgets/jellyfin/component.jsx new file mode 100644 index 000000000..e88dae0da --- /dev/null +++ b/src/widgets/jellyfin/component.jsx @@ -0,0 +1,3 @@ +import EmbyComponent from "../emby/component"; + +export default EmbyComponent; diff --git a/src/widgets/jellyfin/widget.js b/src/widgets/jellyfin/widget.js new file mode 100644 index 000000000..b1f603e4d --- /dev/null +++ b/src/widgets/jellyfin/widget.js @@ -0,0 +1,42 @@ +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "{url}/{endpoint}", + proxyHandler: credentialedProxyHandler, + mappings: { + Sessions: { + endpoint: "emby/Sessions?api_key={key}", + }, + Count: { + endpoint: "emby/Items/Counts?api_key={key}", + }, + Unpause: { + method: "POST", + endpoint: "emby/Sessions/{sessionId}/Playing/Unpause?api_key={key}", + segments: ["sessionId"], + }, + Pause: { + method: "POST", + endpoint: "emby/Sessions/{sessionId}/Playing/Pause?api_key={key}", + segments: ["sessionId"], + }, + SessionsV2: { + endpoint: "Sessions", + }, + CountV2: { + endpoint: "Items/Counts", + }, + UnpauseV2: { + method: "POST", + endpoint: "Sessions/{sessionId}/Playing/Unpause", + segments: ["sessionId"], + }, + PauseV2: { + method: "POST", + endpoint: "Sessions/{sessionId}/Playing/Pause", + segments: ["sessionId"], + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index dcc0ba65e..ffa7d668d 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -51,6 +51,7 @@ import homebridge from "./homebridge/widget"; import immich from "./immich/widget"; import jackett from "./jackett/widget"; import jdownloader from "./jdownloader/widget"; +import jellyfin from "./jellyfin/widget"; import jellyseerr from "./jellyseerr/widget"; import jellystat from "./jellystat/widget"; import karakeep from "./karakeep/widget"; @@ -201,7 +202,7 @@ const widgets = { immich, jackett, jdownloader, - jellyfin: emby, + jellyfin, jellyseerr, jellystat, kavita,