diff --git a/src/widgets/components.js b/src/widgets/components.js index 43a46fa9..a73afc1f 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -19,7 +19,7 @@ const components = { hdhomerun: dynamic(() => import("./hdhomerun/component")), homebridge: dynamic(() => import("./homebridge/component")), jackett: dynamic(() => import("./jackett/component")), - jellyfin: dynamic(() => import("./emby/component")), + jellyfin: dynamic(() => import("./jellyfin/component")), jellyseerr: dynamic(() => import("./jellyseerr/component")), lidarr: dynamic(() => import("./lidarr/component")), mastodon: dynamic(() => import("./mastodon/component")), diff --git a/src/widgets/jellyfin/component.jsx b/src/widgets/jellyfin/component.jsx new file mode 100644 index 00000000..c1365f30 --- /dev/null +++ b/src/widgets/jellyfin/component.jsx @@ -0,0 +1,239 @@ +import { useTranslation } from "next-i18next"; +import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs"; +import { MdOutlineSmartDisplay } from "react-icons/md"; + +import Container from "components/services/widget/container"; +import { formatProxyUrlWithSegments } 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 SingleSessionEntry({ playCommand, session }) { + const { + NowPlayingItem: { Name, SeriesName, RunTimeTicks }, + PlayState: { PositionTicks, IsPaused, IsMuted }, + } = session; + + const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || { + IsVideoDirect: true, + VideoDecoderIsHardware: true, + VideoEncoderIsHardware: true, + }; + + const percent = (PositionTicks / RunTimeTicks) * 100; + + return ( + <> +
+
+
+ {Name} + {SeriesName && ` - ${SeriesName}`} +
+
+
+ {IsVideoDirect && } + {!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && } + {!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && ( + + )} +
+
+ +
+
+
+ {IsPaused && ( + { + playCommand(session, "Unpause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} + {!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 }) { + const { + NowPlayingItem: { Name, SeriesName, RunTimeTicks }, + PlayState: { PositionTicks, IsPaused, IsMuted }, + } = session; + + const { IsVideoDirect, VideoDecoderIsHardware, VideoEncoderIsHardware } = session?.TranscodingInfo || {}; + + const percent = (PositionTicks / RunTimeTicks) * 100; + + return ( +
+
+
+ {IsPaused && ( + { + playCommand(session, "Unpause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} + {!IsPaused && ( + { + playCommand(session, "Pause"); + }} + className="inline-block w-4 h-4 cursor-pointer -mt-[1px] mr-1 opacity-80" + /> + )} +
+
+
+ {Name} + {SeriesName && ` - ${SeriesName}`} +
+
+
{IsMuted && }
+
{ticksToString(PositionTicks)}
+
+ {IsVideoDirect && } + {!IsVideoDirect && (!VideoDecoderIsHardware || !VideoEncoderIsHardware) && } + {!IsVideoDirect && VideoDecoderIsHardware && VideoEncoderIsHardware && } +
+
+ ); +} + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { + data: sessionsData, + error: sessionsError, + mutate: sessionMutate, + } = useWidgetAPI(widget, "Sessions", { + refreshInterval: 5000, + }); + + async function handlePlayCommand(session, command) { + const url = formatProxyUrlWithSegments(widget, "PlayControl", { + sessionId: session.Id, + command, + }); + await fetch(url).then(() => { + sessionMutate(); + }); + } + + if (sessionsError) { + return ; + } + + if (!sessionsData) { + return ( +
+
+ - +
+
+ - +
+
+ ); + } + + 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 ( +
+
+ {t("jellyfin.no_active")} +
+
+ - +
+
+ ); + } + + if (playing.length === 1) { + const session = playing[0]; + return ( +
+ handlePlayCommand(currentSession, command)} + session={session} + /> +
+ ); + } + + return ( +
+ {playing.map((session) => ( + handlePlayCommand(currentSession, command)} + session={session} + /> + ))} +
+ ); +} diff --git a/src/widgets/jellyfin/widget.js b/src/widgets/jellyfin/widget.js new file mode 100644 index 00000000..e875ff54 --- /dev/null +++ b/src/widgets/jellyfin/widget.js @@ -0,0 +1,19 @@ +import genericProxyHandler from "utils/proxy/handlers/generic"; + +const widget = { + api: "{url}/{endpoint}?api_key={key}", + proxyHandler: genericProxyHandler, + + mappings: { + Sessions: { + endpoint: "Sessions", + }, + PlayControl: { + method: "POST", + endpoint: "Sessions/{sessionId}/Playing/{command}", + segments: ["sessionId", "command"], + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 133903fb..6b469124 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -14,6 +14,7 @@ import gotify from "./gotify/widget"; import hdhomerun from "./hdhomerun/widget"; import homebridge from "./homebridge/widget"; import jackett from "./jackett/widget"; +import jellyfin from "./jellyfin/widget"; import jellyseerr from "./jellyseerr/widget"; import lidarr from "./lidarr/widget"; import mastodon from "./mastodon/widget"; @@ -76,7 +77,7 @@ const widgets = { hdhomerun, homebridge, jackett, - jellyfin: emby, + jellyfin, jellyseerr, lidarr, mastodon,