diff --git a/src/components/services/widgets/service/docker.jsx b/src/components/services/widgets/service/docker.jsx
index 89ac73c8..ca9476db 100644
--- a/src/components/services/widgets/service/docker.jsx
+++ b/src/components/services/widgets/service/docker.jsx
@@ -4,7 +4,7 @@ import { useTranslation } from "next-i18next";
import Widget from "../widget";
import Block from "../block";
-import calculateCPUPercent from "utils/stats-helpers";
+import calculateCPUPercent from "widgets/docker/stats-helpers";
export default function Docker({ service }) {
const { t } = useTranslation();
diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js
index 2efb01c2..2db0d8fa 100644
--- a/src/pages/api/services/proxy.js
+++ b/src/pages/api/services/proxy.js
@@ -1,3 +1,4 @@
+import { formatApiCall } from "utils/api-helpers";
import createLogger from "utils/logger";
import genericProxyHandler from "utils/proxies/generic";
import widgets from "widgets/widgets";
@@ -31,12 +32,15 @@ export default async function handler(req, res) {
return res.status(403).json({ error: "Unsupported service endpoint" });
}
- if (req.query.params) {
- const queryParams = JSON.parse(req.query.params);
+ req.query.endpoint = endpoint;
+ if (req.query.segments) {
+ const segments = JSON.parse(req.query.segments);
+ req.query.endpoint = formatApiCall(endpoint, segments);
+ }
+ if (req.query.query) {
+ const queryParams = JSON.parse(req.query.query);
const query = new URLSearchParams(mappingParams.map((p) => [p, queryParams[p]]));
- req.query.endpoint = `${endpoint}?${query}`;
- } else {
- req.query.endpoint = endpoint;
+ req.query.endpoint = `${req.query.endpoint}?${query}`;
}
if (endpointProxy instanceof Function) {
diff --git a/src/utils/api-helpers.js b/src/utils/api-helpers.js
index 6865824f..9293e987 100644
--- a/src/utils/api-helpers.js
+++ b/src/utils/api-helpers.js
@@ -32,15 +32,28 @@ export function formatApiCall(url, args) {
return url.replace(find, replace);
}
-export function formatProxyUrl(widget, endpoint, endpointParams) {
+function getURLSearchParams(widget, endpoint) {
const params = new URLSearchParams({
type: widget.type,
group: widget.service_group,
service: widget.service_name,
endpoint,
});
- if (endpointParams) {
- params.append("params", JSON.stringify(endpointParams));
+ return params;
+}
+
+export function formatProxyUrlWithSegments(widget, endpoint, segments) {
+ const params = getURLSearchParams(widget, endpoint);
+ if (segments) {
+ params.append("segments", JSON.stringify(segments))
+ }
+ return `/api/services/proxy?${params.toString()}`;
+}
+
+export function formatProxyUrl(widget, endpoint, queryParams) {
+ const params = getURLSearchParams(widget, endpoint);
+ if (queryParams) {
+ params.append("query", JSON.stringify(queryParams));
}
return `/api/services/proxy?${params.toString()}`;
}
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 07b1e1fa..0831ba7d 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -4,6 +4,12 @@ const components = {
adguard: dynamic(() => import("./adguard/component")),
bazarr: dynamic(() => import("./bazarr/component")),
coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
+ docker: dynamic(() => import("./docker/component")),
+ emby: dynamic(() => import("./emby/component")),
+ gotify: dynamic(() => import("./gotify/component")),
+ jackett: dynamic(() => import("./jackett/component")),
+ jellyfin: dynamic(() => import("./emby/component")),
+ jellyseerr: dynamic(() => import("./jellyseerr/component")),
overseerr: dynamic(() => import("./overseerr/component")),
portainer: dynamic(() => import("./portainer/component")),
prowlarr: dynamic(() => import("./prowlarr/component")),
diff --git a/src/widgets/docker/component.jsx b/src/widgets/docker/component.jsx
new file mode 100644
index 00000000..bf015060
--- /dev/null
+++ b/src/widgets/docker/component.jsx
@@ -0,0 +1,63 @@
+import useSWR from "swr";
+import { useTranslation } from "next-i18next";
+
+import calculateCPUPercent from "./stats-helpers";
+
+import Widget from "components/services/widgets/widget";
+import Block from "components/services/widgets/block";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const config = service.widget;
+
+ const { data: statusData, error: statusError } = useSWR(
+ `/api/docker/status/${config.container}/${config.server || ""}`,
+ {
+ refreshInterval: 5000,
+ }
+ );
+
+ const { data: statsData, error: statsError } = useSWR(
+ `/api/docker/stats/${config.container}/${config.server || ""}`,
+ {
+ refreshInterval: 5000,
+ }
+ );
+
+ if (statsError || statusError) {
+ return ;
+ }
+
+ if (statusData && statusData.status !== "running") {
+ return (
+
+
+
+ );
+ }
+
+ if (!statsData || !statusData) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {statsData.stats.networks && (
+ <>
+
+
+ >
+ )}
+
+ );
+}
diff --git a/src/utils/stats-helpers.js b/src/widgets/docker/stats-helpers.js
similarity index 100%
rename from src/utils/stats-helpers.js
rename to src/widgets/docker/stats-helpers.js
diff --git a/src/widgets/emby/component.jsx b/src/widgets/emby/component.jsx
new file mode 100644
index 00000000..5096174d
--- /dev/null
+++ b/src/widgets/emby/component.jsx
@@ -0,0 +1,239 @@
+import useSWR from "swr";
+import { useTranslation } from "next-i18next";
+import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
+import { MdOutlineSmartDisplay } from "react-icons/md";
+
+import Widget from "components/services/widgets/widget";
+import { formatProxyUrl, formatProxyUrlWithSegments } 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 { 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 config = service.widget;
+
+ const {
+ data: sessionsData,
+ error: sessionsError,
+ mutate: sessionMutate,
+ } = useSWR(formatProxyUrl(config, "Sessions"), {
+ refreshInterval: 5000,
+ });
+
+ async function handlePlayCommand(session, command) {
+ const url = formatProxyUrlWithSegments(config, "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("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/widgets/emby/widget.js b/src/widgets/emby/widget.js
new file mode 100644
index 00000000..42157522
--- /dev/null
+++ b/src/widgets/emby/widget.js
@@ -0,0 +1,19 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/emby/{endpoint}?api_key={key}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ "Sessions": {
+ endpoint: "Sessions",
+ },
+ "PlayControl": {
+ method: "POST",
+ enpoint: "Sessions/{sessionId}/Playing/{command}",
+ segments: ["sessionId", "command"]
+ }
+ },
+};
+
+export default widget;
diff --git a/src/widgets/gotify/component.jsx b/src/widgets/gotify/component.jsx
new file mode 100644
index 00000000..178dedca
--- /dev/null
+++ b/src/widgets/gotify/component.jsx
@@ -0,0 +1,28 @@
+import useSWR from "swr";
+import { useTranslation } from "next-i18next";
+
+import Widget from "components/services/widgets/widget";
+import Block from "components/services/widgets/block";
+import { formatProxyUrl } from "utils/api-helpers";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const config = service.widget;
+
+ const { data: appsData, error: appsError } = useSWR(formatProxyUrl(config, `application`));
+ const { data: messagesData, error: messagesError } = useSWR(formatProxyUrl(config, `message`));
+ const { data: clientsData, error: clientsError } = useSWR(formatProxyUrl(config, `client`));
+
+ if (appsError || messagesError || clientsError) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/gotify/widget.js b/src/widgets/gotify/widget.js
new file mode 100644
index 00000000..2ad71180
--- /dev/null
+++ b/src/widgets/gotify/widget.js
@@ -0,0 +1,20 @@
+import credentialedProxyHandler from "utils/proxies/credentialed";
+
+const widget = {
+ api: "{url}/{endpoint}",
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ "application": {
+ endpoint: "application"
+ },
+ "client": {
+ endpoint: "client"
+ },
+ "message": {
+ endpoint: "message"
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/jackett/component.jsx b/src/widgets/jackett/component.jsx
new file mode 100644
index 00000000..738355fc
--- /dev/null
+++ b/src/widgets/jackett/component.jsx
@@ -0,0 +1,36 @@
+import useSWR from "swr";
+import { useTranslation } from "next-i18next";
+
+import Widget from "components/services/widgets/widget";
+import Block from "components/services/widgets/block";
+import { formatProxyUrl } from "utils/api-helpers";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const config = service.widget;
+
+ const { data: indexersData, error: indexersError } = useSWR(formatProxyUrl(config, "indexers"));
+
+ if (indexersError) {
+ return ;
+ }
+
+ if (!indexersData) {
+ return (
+
+
+
+
+ );
+ }
+
+ const errored = indexersData.filter((indexer) => indexer.last_error);
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/widgets/jackett/widget.js b/src/widgets/jackett/widget.js
new file mode 100644
index 00000000..d787c3e4
--- /dev/null
+++ b/src/widgets/jackett/widget.js
@@ -0,0 +1,14 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/api/v2.0/{endpoint}?apikey={key}&configured=true",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ "indexers": {
+ endpoint: "indexers"
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/jellyseerr/component.jsx b/src/widgets/jellyseerr/component.jsx
new file mode 100644
index 00000000..74685ddc
--- /dev/null
+++ b/src/widgets/jellyseerr/component.jsx
@@ -0,0 +1,36 @@
+import useSWR from "swr";
+import { useTranslation } from "next-i18next";
+
+import Widget from "components/services/widgets/widget";
+import Block from "components/services/widgets/block";
+import { formatProxyUrl } from "utils/api-helpers";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const config = service.widget;
+
+ const { data: statsData, error: statsError } = useSWR(formatProxyUrl(config, `request/count`));
+
+ if (statsError) {
+ return ;
+ }
+
+ if (!statsData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/jellyseerr/widget.js b/src/widgets/jellyseerr/widget.js
new file mode 100644
index 00000000..4b823efc
--- /dev/null
+++ b/src/widgets/jellyseerr/widget.js
@@ -0,0 +1,14 @@
+import credentialedProxyHandler from "utils/proxies/credentialed";
+
+const widget = {
+ api: "{url}/api/v1/{endpoint}",
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ "request/count": {
+ endpoint: "request/count"
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 78b17726..241c78d9 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -1,6 +1,10 @@
import adguard from "./adguard/widget";
import bazarr from "./bazarr/widget";
import coinmarketcap from "./coinmarketcap/widget";
+import emby from "./emby/widget";
+import gotify from "./gotify/widget";
+import jackett from "./jackett/widget";
+import jellyseerr from "./jellyseerr/widget";
import overseerr from "./overseerr/widget";
import portainer from "./portainer/widget";
import prowlarr from "./prowlarr/widget";
@@ -20,6 +24,11 @@ const widgets = {
adguard,
bazarr,
coinmarketcap,
+ emby,
+ gotify,
+ jackett,
+ jellyfin: emby,
+ jellyseerr,
overseerr,
portainer,
prowlarr,