diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index cacfb7af..2276009e 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -39,12 +39,14 @@
"emby": {
"playing": "Playing",
"transcoding": "Transcoding",
- "bitrate": "Bitrate"
+ "bitrate": "Bitrate",
+ "no_active": "No Active Streams"
},
"tautulli": {
"playing": "Playing",
"transcoding": "Transcoding",
- "bitrate": "Bitrate"
+ "bitrate": "Bitrate",
+ "no_active": "No Active Streams"
},
"nzbget": {
"rate": "Rate",
diff --git a/src/components/services/widgets/service/emby.jsx b/src/components/services/widgets/service/emby.jsx
index e42175ab..fc9d3e50 100644
--- a/src/components/services/widgets/service/emby.jsx
+++ b/src/components/services/widgets/service/emby.jsx
@@ -1,17 +1,148 @@
import useSWR from "swr";
import { useTranslation } from "react-i18next";
+import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import Widget from "../widget";
-import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
+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 percent = (PositionTicks / RunTimeTicks) * 100;
+
+ return (
+ <>
+
+
+
+ {Name}
+ {SeriesName && ` - ${SeriesName}`}
+
+
+
+
{IsMuted && }
+
+
+
+
+
+ {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"
+ />
+ )}
+
+
+
{ticksToString(PositionTicks)}
+
+ >
+ );
+}
+
+function SessionEntry({ playCommand, session }) {
+ const {
+ NowPlayingItem: { Name, SeriesName, RunTimeTicks },
+ PlayState: { PositionTicks, IsPaused, IsMuted },
+ } = session;
+ 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)}
+
+ );
+}
+
export default function Emby({ service }) {
const { t } = useTranslation();
const config = service.widget;
- const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
+ const {
+ data: sessionsData,
+ error: sessionsError,
+ mutate: sessionMutate,
+ } = useSWR(formatApiUrl(config, "Sessions"), {
+ refreshInterval: 5000,
+ });
+
+ async function handlePlayCommand(session, command) {
+ const url = formatApiUrl(config, `Sessions/${session.Id}/Playing/${command}`);
+ await fetch(url, {
+ method: "POST",
+ }).then(() => {
+ sessionMutate();
+ });
+ }
if (sessionsError) {
return ;
@@ -19,26 +150,63 @@ export default function Emby({ service }) {
if (!sessionsData) {
return (
-
-
-
-
-
+
);
}
- const playing = sessionsData.filter((session) => session?.NowPlayingItem);
- const transcoding = sessionsData.filter(
- (session) => session?.PlayState && session.PlayState.PlayMethod === "Transcode"
- );
+ 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;
+ });
- const bitrate = playing.reduce((acc, session) => acc + session.NowPlayingItem.Bitrate, 0);
+ if (playing.length === 0) {
+ return (
+
+
+ {t("emby.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/components/services/widgets/service/jellyfin.jsx b/src/components/services/widgets/service/jellyfin.jsx
index 9fe66896..195c133c 100644
--- a/src/components/services/widgets/service/jellyfin.jsx
+++ b/src/components/services/widgets/service/jellyfin.jsx
@@ -1,48 +1,5 @@
-import useSWR from "swr";
-import { useTranslation } from "react-i18next";
-
-import Widget from "../widget";
-import Block from "../block";
-
-import { formatApiUrl } from "utils/api-helpers";
+import Emby from "./emby";
export default function Jellyfin({ service }) {
- const { t } = useTranslation();
-
- const config = service.widget;
-
- const { data: sessionsData, error: sessionsError } = useSWR(formatApiUrl(config, "Sessions"));
-
- if (sessionsError) {
- return ;
- }
-
- if (!sessionsData) {
- return (
-
-
-
-
-
- );
- }
-
- const playing = sessionsData.filter((session) => session?.NowPlayingItem);
- const transcoding = sessionsData.filter(
- (session) => session?.PlayState && session.PlayState.PlayMethod === "Transcode"
- );
-
- const bitrate = playing.reduce(
- (acc, session) =>
- acc + session.NowPlayingQueueFullItems[0].MediaSources.reduce((acb, source) => acb + source.Bitrate, 0),
- 0
- );
-
- return (
-
-
-
-
-
- );
+ return ;
}
diff --git a/src/components/services/widgets/service/tautulli.jsx b/src/components/services/widgets/service/tautulli.jsx
index d86646c3..c92ccbbe 100644
--- a/src/components/services/widgets/service/tautulli.jsx
+++ b/src/components/services/widgets/service/tautulli.jsx
@@ -1,39 +1,157 @@
+/* eslint-disable camelcase */
import useSWR from "swr";
import { useTranslation } from "react-i18next";
+import { BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import Widget from "../widget";
-import Block from "../block";
import { formatApiUrl } from "utils/api-helpers";
+function millisecondsToTime(milliseconds) {
+ 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 millisecondsToString(milliseconds) {
+ const { hours, minutes, seconds } = millisecondsToTime(milliseconds);
+ 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({ session }) {
+ const { full_title, duration, view_offset, progress_percent, state, year, grandparent_year } = session;
+
+ return (
+ <>
+
+
+ {full_title}
+
+
+
{year || grandparent_year}
+
+
+
+
+
+ {state === "paused" && (
+
+ )}
+ {state !== "paused" && (
+
+ )}
+
+
+
+ {millisecondsToString(view_offset)} / {millisecondsToString(duration)}
+
+
+ >
+ );
+}
+
+function SessionEntry({ session }) {
+ const { full_title, view_offset, progress_percent, state } = session;
+
+ return (
+
+
+
+ {state === "paused" && (
+
+ )}
+ {state !== "paused" && (
+
+ )}
+ {full_title}
+
+
+
{millisecondsToString(view_offset)}
+
+ );
+}
+
export default function Tautulli({ service }) {
const { t } = useTranslation();
const config = service.widget;
- const { data: statsData, error: statsError } = useSWR(formatApiUrl(config, "get_activity"));
+ const { data: activityData, error: activityError } = useSWR(formatApiUrl(config, "get_activity"), {
+ refreshInterval: 5000,
+ });
- if (statsError) {
+ if (activityError) {
return ;
}
- if (!statsData) {
+ if (!activityData) {
return (
-
-
-
-
-
+
);
}
- const { data } = statsData.response;
+ const playing = activityData.response.data.sessions.sort((a, b) => {
+ if (a.view_offset > b.view_offset) {
+ return 1;
+ }
+ if (a.view_offset < b.view_offset) {
+ return -1;
+ }
+ return 0;
+ });
+
+ if (playing.length === 0) {
+ return (
+
+
+ {t("tautulli.no_active")}
+
+
+ -
+
+
+ );
+ }
+
+ if (playing.length === 1) {
+ const session = playing[0];
+ return (
+
+
+
+ );
+ }
return (
-
-
-
-
-
+
+ {playing.map((session) => (
+
+ ))}
+
);
}