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,