From 4a7c9a7c1a83e00210dec64a7c0ca1f237d14ed6 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:22:55 -0800 Subject: [PATCH] Actually shit, lets just split the widgets now --- public/locales/en/common.json | 10 + src/components/services/widget/container.jsx | 1 - src/widgets/emby/component.jsx | 15 +- src/widgets/jellyfin/component.jsx | 346 ++++++++++++++++++- 4 files changed, 357 insertions(+), 15 deletions(-) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 2d237a23c..f9da2791d 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -107,6 +107,16 @@ "episodes": "Episodes", "songs": "Songs" }, + "jellyfin": { + "playing": "Playing", + "transcoding": "Transcoding", + "bitrate": "Bitrate", + "no_active": "No Active Streams", + "movies": "Movies", + "series": "Series", + "episodes": "Episodes", + "songs": "Songs" + }, "esphome": { "offline": "Offline", "offline_alt": "Offline", diff --git a/src/components/services/widget/container.jsx b/src/components/services/widget/container.jsx index 66434cfc2..e5962e445 100644 --- a/src/components/services/widget/container.jsx +++ b/src/components/services/widget/container.jsx @@ -9,7 +9,6 @@ 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/widgets/emby/component.jsx b/src/widgets/emby/component.jsx index 1f6c9b4dc..39a347185 100644 --- a/src/widgets/emby/component.jsx +++ b/src/widgets/emby/component.jsx @@ -202,31 +202,22 @@ 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 ? sessionsEndpoint : "", { + } = useWidgetAPI(widget, enableNowPlaying ? "Sessions" : "", { refreshInterval: enableNowPlaying ? 5000 : undefined, }); - const { data: countData, error: countError } = useWidgetAPI(widget, countEndpoint, { + const { data: countData, error: countError } = useWidgetAPI(widget, "Count", { refreshInterval: 60000, }); async function handlePlayCommand(session, command) { - const mappedCommand = commandMap[command] ?? command; - const params = getURLSearchParams(widget, mappedCommand); + const params = getURLSearchParams(widget, command); params.append( "segments", JSON.stringify({ diff --git a/src/widgets/jellyfin/component.jsx b/src/widgets/jellyfin/component.jsx index e88dae0da..591ee73de 100644 --- a/src/widgets/jellyfin/component.jsx +++ b/src/widgets/jellyfin/component.jsx @@ -1,3 +1,345 @@ -import EmbyComponent from "../emby/component"; +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; +import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill, BsVolumeMuteFill } from "react-icons/bs"; +import { MdOutlineSmartDisplay } from "react-icons/md"; -export default EmbyComponent; +import { getURLSearchParams } from "utils/proxy/api-helpers"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +function ticksToTime(ticks) { + const milliseconds = ticks / 10000; + const seconds = Math.floor((milliseconds / 1000) % 60); + const minutes = Math.floor((milliseconds / (1000 * 60)) % 60); + const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24); + return { hours, minutes, seconds }; +} + +function ticksToString(ticks) { + const { hours, minutes, seconds } = ticksToTime(ticks); + const parts = []; + if (hours > 0) { + parts.push(hours); + } + parts.push(minutes); + parts.push(seconds); + + return parts.map((part) => part.toString().padStart(2, "0")).join(":"); +} + +function generateStreamTitle(session, enableUser, showEpisodeNumber) { + const { + NowPlayingItem: { Name, SeriesName, Type, ParentIndexNumber, IndexNumber, AlbumArtist, Album }, + UserName, + } = session; + let streamTitle = ""; + + if (Type === "Episode" && showEpisodeNumber) { + const seasonStr = ParentIndexNumber ? `S${ParentIndexNumber.toString().padStart(2, "0")}` : ""; + const episodeStr = IndexNumber ? `E${IndexNumber.toString().padStart(2, "0")}` : ""; + streamTitle = `${SeriesName}: ${seasonStr} ยท ${episodeStr} - ${Name}`; + } else if (Type === "Audio") { + streamTitle = `${AlbumArtist} - ${Album} - ${Name}`; + } else { + streamTitle = `${Name}${SeriesName ? ` - ${SeriesName}` : ""}`; + } + + return enableUser ? `${streamTitle} (${UserName})` : streamTitle; +} + +function SingleSessionEntry({ playCommand, session, enableUser, showEpisodeNumber, enableMediaControl }) { + const { + PlayState: { PositionTicks, IsPaused, IsMuted }, + } = session; + + const RunTimeTicks = + session.NowPlayingItem?.RunTimeTicks ?? session.NowPlayingItem?.CurrentProgram?.RunTimeTicks ?? 0; + + const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || { + IsVideoDirect: true, + }; // if no transcodinginfo its videodirect + + const percent = Math.min(1, PositionTicks / RunTimeTicks) * 100; + + const streamTitle = generateStreamTitle(session, enableUser, showEpisodeNumber); + return ( + <> +
+
+
+ {streamTitle} +
+
+
+ {IsVideoDirect && } + {!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && } + {!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && ( + + )} +
+
+ +
+
+
+ {enableMediaControl && IsPaused && ( + { + playCommand(session, "Unpause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} + {enableMediaControl && !IsPaused && ( + { + playCommand(session, "Pause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} +
+
+
{IsMuted && }
+
+ {ticksToString(PositionTicks)} + / + {ticksToString(RunTimeTicks)} +
+
+ + ); +} + +function SessionEntry({ playCommand, session, enableUser, showEpisodeNumber, enableMediaControl }) { + const { + PlayState: { PositionTicks, IsPaused, IsMuted }, + } = session; + + const RunTimeTicks = + session.NowPlayingItem?.RunTimeTicks ?? session.NowPlayingItem?.CurrentProgram?.RunTimeTicks ?? 0; + + const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || { + IsVideoDirect: true, + }; // if no transcodinginfo its videodirect + + const streamTitle = generateStreamTitle(session, enableUser, showEpisodeNumber); + + const percent = Math.min(1, PositionTicks / RunTimeTicks) * 100; + + return ( +
+
+
+ {enableMediaControl && IsPaused && ( + { + playCommand(session, "Unpause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} + {enableMediaControl && !IsPaused && ( + { + playCommand(session, "Pause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} +
+
+
+ {streamTitle} +
+
+
{IsMuted && }
+
{ticksToString(PositionTicks)}
+
+ {IsVideoDirect && } + {!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && } + {!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && } +
+
+ ); +} + +function CountBlocks({ service, countData }) { + const { t } = useTranslation(); + + if (!countData) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + const version = widget?.version ?? 1; + const useJellyfinV2 = 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 ? sessionsEndpoint : "", { + refreshInterval: enableNowPlaying ? 5000 : undefined, + }); + + const { data: countData, error: countError } = useWidgetAPI(widget, countEndpoint, { + refreshInterval: 60000, + }); + + async function handlePlayCommand(session, command) { + const mappedCommand = commandMap[command] ?? command; + const params = getURLSearchParams(widget, mappedCommand); + params.append( + "segments", + JSON.stringify({ + sessionId: session.Id, + }), + ); + const url = `/api/services/proxy?${params.toString()}`; + await fetch(url, { + method: "POST", + }).then(() => { + sessionMutate(); + }); + } + + if (sessionsError || countError) { + return ; + } + + const enableBlocks = service.widget?.enableBlocks; + const enableMediaControl = service.widget?.enableMediaControl !== false; // default is true + const enableUser = !!service.widget?.enableUser; // default is false + const expandOneStreamToTwoRows = service.widget?.expandOneStreamToTwoRows !== false; // default is true + const showEpisodeNumber = !!service.widget?.showEpisodeNumber; // default is false + + if ((enableNowPlaying && !sessionsData) || !countData) { + return ( + <> + {enableBlocks && } + {enableNowPlaying && ( +
+
+ - +
+ {expandOneStreamToTwoRows && ( +
+ - +
+ )} +
+ )} + + ); + } + + if (enableNowPlaying) { + const playing = sessionsData + .filter((session) => session?.NowPlayingItem) + .sort((a, b) => { + if (a.PlayState.PositionTicks > b.PlayState.PositionTicks) { + return 1; + } + if (a.PlayState.PositionTicks < b.PlayState.PositionTicks) { + return -1; + } + return 0; + }); + + if (playing.length === 0) { + return ( + <> + {enableBlocks && } +
+
+ {t("jellyfin.no_active")} +
+ {expandOneStreamToTwoRows && ( +
+ - +
+ )} +
+ + ); + } + + if (expandOneStreamToTwoRows && playing.length === 1) { + const session = playing[0]; + return ( + <> + {enableBlocks && } +
+ handlePlayCommand(currentSession, command)} + session={session} + enableUser={enableUser} + showEpisodeNumber={showEpisodeNumber} + enableMediaControl={enableMediaControl} + /> +
+ + ); + } + + return ( + <> + {enableBlocks && } +
+ {playing.map((session) => ( + handlePlayCommand(currentSession, command)} + session={session} + enableUser={enableUser} + showEpisodeNumber={showEpisodeNumber} + enableMediaControl={enableMediaControl} + /> + ))} +
+ + ); + } + + if (enableBlocks) { + return ; + } +}