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) => ( + + ))} +
); }