diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js
index 43a9a3d08..2efb01c26 100644
--- a/src/pages/api/services/proxy.js
+++ b/src/pages/api/services/proxy.js
@@ -1,5 +1,3 @@
-import { URLSearchParams } from "next/dist/compiled/@edge-runtime/primitives/url";
-
import createLogger from "utils/logger";
import genericProxyHandler from "utils/proxies/generic";
import widgets from "widgets/widgets";
@@ -35,10 +33,9 @@ export default async function handler(req, res) {
if (req.query.params) {
const queryParams = JSON.parse(req.query.params);
- const query = new URLSearchParams(mappingParams.map(p => [p, queryParams[p]]));
+ const query = new URLSearchParams(mappingParams.map((p) => [p, queryParams[p]]));
req.query.endpoint = `${endpoint}?${query}`;
- }
- else {
+ } else {
req.query.endpoint = endpoint;
}
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 5827a575e..07b1e1fa1 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -5,8 +5,19 @@ const components = {
bazarr: dynamic(() => import("./bazarr/component")),
coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
overseerr: dynamic(() => import("./overseerr/component")),
+ portainer: dynamic(() => import("./portainer/component")),
+ prowlarr: dynamic(() => import("./prowlarr/component")),
+ qbittorrent: dynamic(() => import("./qbittorrent/component")),
radarr: dynamic(() => import("./radarr/component")),
sonarr: dynamic(() => import("./sonarr/component")),
+ readarr: dynamic(() => import("./readarr/component")),
+ rutorrent: dynamic(() => import("./rutorrent/component")),
+ sabnzbd: dynamic(() => import("./sabnzbd/component")),
+ speedtest: dynamic(() => import("./speedtest/component")),
+ strelaysrv: dynamic(() => import("./strelaysrv/component")),
+ tautulli: dynamic(() => import("./tautulli/component")),
+ traefik: dynamic(() => import("./traefik/component")),
+ transmission: dynamic(() => import("./transmission/component")),
};
export default components;
diff --git a/src/widgets/portainer/component.jsx b/src/widgets/portainer/component.jsx
new file mode 100644
index 000000000..272b8449a
--- /dev/null
+++ b/src/widgets/portainer/component.jsx
@@ -0,0 +1,48 @@
+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: containersData, error: containersError } = useSWR(
+ formatProxyUrl(config, `docker/containers/json`, {
+ all: 1,
+ })
+ );
+
+ if (containersError) {
+ return ;
+ }
+
+ if (!containersData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (containersData.error) {
+ return ;
+ }
+
+ const running = containersData.filter((c) => c.State === "running").length;
+ const stopped = containersData.filter((c) => c.State === "exited").length;
+ const total = containersData.length;
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/portainer/widget.js b/src/widgets/portainer/widget.js
new file mode 100644
index 000000000..58d6c3068
--- /dev/null
+++ b/src/widgets/portainer/widget.js
@@ -0,0 +1,15 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/api/endpoints/{env}/{endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ "docker/containers/json": {
+ endpoint: "docker/containers/json",
+ params: ["all"],
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/prowlarr/component.jsx b/src/widgets/prowlarr/component.jsx
new file mode 100644
index 000000000..c7ebdf96e
--- /dev/null
+++ b/src/widgets/prowlarr/component.jsx
@@ -0,0 +1,54 @@
+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, "indexer"));
+ const { data: grabsData, error: grabsError } = useSWR(formatProxyUrl(config, "indexerstats"));
+
+ if (indexersError || grabsError) {
+ return ;
+ }
+
+ if (!indexersData || !grabsData) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ const indexers = indexersData?.filter((indexer) => indexer.enable === true);
+
+ let numberOfGrabs = 0;
+ let numberOfQueries = 0;
+ let numberOfFailedGrabs = 0;
+ let numberOfFailedQueries = 0;
+ grabsData?.indexers?.forEach((element) => {
+ numberOfGrabs += element.numberOfGrabs;
+ numberOfQueries += element.numberOfQueries;
+ numberOfFailedGrabs += numberOfFailedGrabs + element.numberOfFailedGrabs;
+ numberOfFailedQueries += numberOfFailedQueries + element.numberOfFailedQueries;
+ });
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/prowlarr/widget.js b/src/widgets/prowlarr/widget.js
new file mode 100644
index 000000000..a19b83a34
--- /dev/null
+++ b/src/widgets/prowlarr/widget.js
@@ -0,0 +1,17 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/api/v1/{endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ indexer: {
+ endpoint: "indexer",
+ },
+ indexerstats: {
+ endpoint: "indexerstats",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/qbittorrent/component.jsx b/src/widgets/qbittorrent/component.jsx
new file mode 100644
index 000000000..1c98541be
--- /dev/null
+++ b/src/widgets/qbittorrent/component.jsx
@@ -0,0 +1,68 @@
+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: torrentData, error: torrentError } = useSWR(formatProxyUrl(config, "torrents/info"));
+
+ if (torrentError) {
+ return ;
+ }
+
+ if (!torrentData) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ let rateDl = 0;
+ let rateUl = 0;
+ let completed = 0;
+
+ for (let i = 0; i < torrentData.length; i += 1) {
+ const torrent = torrentData[i];
+ rateDl += torrent.dlspeed;
+ rateUl += torrent.upspeed;
+ if (torrent.progress === 1) {
+ completed += 1;
+ }
+ }
+
+ const leech = torrentData.length - completed;
+
+ let unitsDl = "KB/s";
+ let unitsUl = "KB/s";
+ rateDl /= 1024;
+ rateUl /= 1024;
+
+ if (rateDl > 1024) {
+ rateDl /= 1024;
+ unitsDl = "MB/s";
+ }
+
+ if (rateUl > 1024) {
+ rateUl /= 1024;
+ unitsUl = "MB/s";
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/utils/proxies/qbittorrent.js b/src/widgets/qbittorrent/proxy.js
similarity index 100%
rename from src/utils/proxies/qbittorrent.js
rename to src/widgets/qbittorrent/proxy.js
diff --git a/src/widgets/qbittorrent/widget.js b/src/widgets/qbittorrent/widget.js
new file mode 100644
index 000000000..9c892848f
--- /dev/null
+++ b/src/widgets/qbittorrent/widget.js
@@ -0,0 +1,8 @@
+import qbittorrentProxyHandler from "./proxy";
+
+const widget = {
+ api: "{url}/api/v2/{endpoint}",
+ proxyHandler: qbittorrentProxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/readarr/component.jsx b/src/widgets/readarr/component.jsx
new file mode 100644
index 000000000..131d94d7c
--- /dev/null
+++ b/src/widgets/readarr/component.jsx
@@ -0,0 +1,38 @@
+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: booksData, error: booksError } = useSWR(formatProxyUrl(config, "book"));
+ const { data: wantedData, error: wantedError } = useSWR(formatProxyUrl(config, "wanted/missing"));
+ const { data: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue/status"));
+
+ if (booksError || wantedError || queueError) {
+ return ;
+ }
+
+ if (!booksData || !wantedData || !queueData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/readarr/widget.js b/src/widgets/readarr/widget.js
new file mode 100644
index 000000000..f826cf1ff
--- /dev/null
+++ b/src/widgets/readarr/widget.js
@@ -0,0 +1,24 @@
+import genericProxyHandler from "utils/proxies/generic";
+import { jsonArrayFilter } from "utils/api-helpers";
+
+const widget = {
+ api: "{url}/api/v1/{endpoint}?apikey={key}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ book: {
+ endpoint: "book",
+ map: (data) => ({
+ have: jsonArrayFilter(data, (item) => item?.statistics?.bookFileCount > 0).length,
+ }),
+ },
+ "queue/status": {
+ endpoint: "queue/status",
+ },
+ "wanted/missing": {
+ endpoint: "wanted/missing",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/rutorrent/component.jsx b/src/widgets/rutorrent/component.jsx
new file mode 100644
index 000000000..5766fbab6
--- /dev/null
+++ b/src/widgets/rutorrent/component.jsx
@@ -0,0 +1,42 @@
+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: statusData, error: statusError } = useSWR(formatProxyUrl(config));
+
+ if (statusError) {
+ return ;
+ }
+
+ if (!statusData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const upload = statusData.reduce((acc, torrent) => acc + parseInt(torrent["d.get_up_rate"], 10), 0);
+
+ const download = statusData.reduce((acc, torrent) => acc + parseInt(torrent["d.get_down_rate"], 10), 0);
+
+ const active = statusData.filter((torrent) => torrent["d.get_state"] === "1");
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/utils/proxies/rutorrent.js b/src/widgets/rutorrent/proxy.js
similarity index 100%
rename from src/utils/proxies/rutorrent.js
rename to src/widgets/rutorrent/proxy.js
diff --git a/src/widgets/rutorrent/widget.js b/src/widgets/rutorrent/widget.js
new file mode 100644
index 000000000..cde092f60
--- /dev/null
+++ b/src/widgets/rutorrent/widget.js
@@ -0,0 +1,8 @@
+import rutorrentProxyHandler from "./proxy";
+
+const widget = {
+ api: "{url}/plugins/httprpc/action.php",
+ proxyHandler: rutorrentProxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/sabnzbd/component.jsx b/src/widgets/sabnzbd/component.jsx
new file mode 100644
index 000000000..7e77ae04f
--- /dev/null
+++ b/src/widgets/sabnzbd/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: queueData, error: queueError } = useSWR(formatProxyUrl(config, "queue"));
+
+ if (queueError) {
+ return ;
+ }
+
+ if (!queueData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/sabnzbd/widget.js b/src/widgets/sabnzbd/widget.js
new file mode 100644
index 000000000..67e64e973
--- /dev/null
+++ b/src/widgets/sabnzbd/widget.js
@@ -0,0 +1,14 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/api/?apikey={key}&output=json&mode={endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ queue: {
+ endpoint: "queue",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/sonarr/widget.js b/src/widgets/sonarr/widget.js
index c17182058..45d6e533a 100644
--- a/src/widgets/sonarr/widget.js
+++ b/src/widgets/sonarr/widget.js
@@ -6,19 +6,19 @@ const widget = {
proxyHandler: genericProxyHandler,
mappings: {
- "series": {
+ series: {
endpoint: "series",
map: (data) => ({
total: asJson(data).length,
}),
},
- "queue": {
+ queue: {
endpoint: "queue",
},
"wanted/missing": {
- endpoint: "wanted/missing",
- },
+ endpoint: "wanted/missing",
},
+ },
};
export default widget;
diff --git a/src/widgets/speedtest/component.jsx b/src/widgets/speedtest/component.jsx
new file mode 100644
index 000000000..6e30231fa
--- /dev/null
+++ b/src/widgets/speedtest/component.jsx
@@ -0,0 +1,45 @@
+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: speedtestData, error: speedtestError } = useSWR(formatProxyUrl(config, "speedtest/latest"));
+
+ if (speedtestError) {
+ return ;
+ }
+
+ if (!speedtestData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/speedtest/widget.js b/src/widgets/speedtest/widget.js
new file mode 100644
index 000000000..6c89b913c
--- /dev/null
+++ b/src/widgets/speedtest/widget.js
@@ -0,0 +1,14 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/api/{endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ "speedtest/latest": {
+ endpoint: "speedtest/latest",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/strelaysrv/component.jsx b/src/widgets/strelaysrv/component.jsx
new file mode 100644
index 000000000..1e0578308
--- /dev/null
+++ b/src/widgets/strelaysrv/component.jsx
@@ -0,0 +1,43 @@
+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, `status`));
+
+ if (statsError) {
+ return ;
+ }
+
+ if (!statsData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/strelaysrv/widget.js b/src/widgets/strelaysrv/widget.js
new file mode 100644
index 000000000..b0c8139bb
--- /dev/null
+++ b/src/widgets/strelaysrv/widget.js
@@ -0,0 +1,14 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/{endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ status: {
+ endpoint: "status",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/tautulli/component.jsx b/src/widgets/tautulli/component.jsx
new file mode 100644
index 000000000..084d457ba
--- /dev/null
+++ b/src/widgets/tautulli/component.jsx
@@ -0,0 +1,182 @@
+/* eslint-disable camelcase */
+import useSWR from "swr";
+import { useTranslation } from "next-i18next";
+import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
+import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
+
+import Widget from "components/services/widgets/widget";
+import { formatProxyUrl } 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, video_decision, audio_decision } = session;
+
+ return (
+ <>
+
+
+
+ {video_decision === "direct play" && audio_decision === "direct play" && (
+
+ )}
+ {video_decision === "copy" && audio_decision === "copy" && }
+ {video_decision !== "copy" &&
+ video_decision !== "direct play" &&
+ (audio_decision !== "copy" || audio_decision !== "direct play") && }
+ {(video_decision === "copy" || video_decision === "direct play") &&
+ audio_decision !== "copy" &&
+ audio_decision !== "direct play" && }
+
+
+
+
+
+
+ {state === "paused" && (
+
+ )}
+ {state !== "paused" && (
+
+ )}
+
+
+
+ {millisecondsToString(view_offset)}
+ /
+ {millisecondsToString(duration)}
+
+
+ >
+ );
+}
+
+function SessionEntry({ session }) {
+ const { full_title, view_offset, progress_percent, state, video_decision, audio_decision } = session;
+
+ return (
+
+
+
+ {state === "paused" && (
+
+ )}
+ {state !== "paused" && (
+
+ )}
+
+
+
+ {video_decision === "direct play" && audio_decision === "direct play" && (
+
+ )}
+ {video_decision === "copy" && audio_decision === "copy" && }
+ {video_decision !== "copy" &&
+ video_decision !== "direct play" &&
+ (audio_decision !== "copy" || audio_decision !== "direct play") && }
+ {(video_decision === "copy" || video_decision === "direct play") &&
+ audio_decision !== "copy" &&
+ audio_decision !== "direct play" && }
+
+
{millisecondsToString(view_offset)}
+
+ );
+}
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const config = service.widget;
+
+ const { data: activityData, error: activityError } = useSWR(formatProxyUrl(config, "get_activity"), {
+ refreshInterval: 5000,
+ });
+
+ if (activityError) {
+ return ;
+ }
+
+ if (!activityData) {
+ return (
+
+ );
+ }
+
+ 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) => (
+
+ ))}
+
+ );
+}
diff --git a/src/widgets/tautulli/widget.js b/src/widgets/tautulli/widget.js
new file mode 100644
index 000000000..4f7239947
--- /dev/null
+++ b/src/widgets/tautulli/widget.js
@@ -0,0 +1,14 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/api/v2?apikey={key}&cmd={endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ get_activity: {
+ endpoint: "get_activity",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/traefik/component.jsx b/src/widgets/traefik/component.jsx
new file mode 100644
index 000000000..a87b35ee0
--- /dev/null
+++ b/src/widgets/traefik/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: traefikData, error: traefikError } = useSWR(formatProxyUrl(config, "overview"));
+
+ if (traefikError) {
+ return ;
+ }
+
+ if (!traefikData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/traefik/widget.js b/src/widgets/traefik/widget.js
new file mode 100644
index 000000000..ed39af2c7
--- /dev/null
+++ b/src/widgets/traefik/widget.js
@@ -0,0 +1,14 @@
+import genericProxyHandler from "utils/proxies/generic";
+
+const widget = {
+ api: "{url}/api/{endpoint}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ overview: {
+ endpoint: "overview",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/transmission/component.jsx b/src/widgets/transmission/component.jsx
new file mode 100644
index 000000000..b935f4b57
--- /dev/null
+++ b/src/widgets/transmission/component.jsx
@@ -0,0 +1,69 @@
+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: torrentData, error: torrentError } = useSWR(formatProxyUrl(config));
+
+ if (torrentError) {
+ return ;
+ }
+
+ if (!torrentData) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ const { torrents } = torrentData.arguments;
+ let rateDl = 0;
+ let rateUl = 0;
+ let completed = 0;
+
+ for (let i = 0; i < torrents.length; i += 1) {
+ const torrent = torrents[i];
+ rateDl += torrent.rateDownload;
+ rateUl += torrent.rateUpload;
+ if (torrent.percentDone === 1) {
+ completed += 1;
+ }
+ }
+
+ const leech = torrents.length - completed;
+
+ let unitsDl = "KB/s";
+ let unitsUl = "KB/s";
+ rateDl /= 1024;
+ rateUl /= 1024;
+
+ if (rateDl > 1024) {
+ rateDl /= 1024;
+ unitsDl = "MB/s";
+ }
+
+ if (rateUl > 1024) {
+ rateUl /= 1024;
+ unitsUl = "MB/s";
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/utils/proxies/transmission.js b/src/widgets/transmission/proxy.js
similarity index 100%
rename from src/utils/proxies/transmission.js
rename to src/widgets/transmission/proxy.js
diff --git a/src/widgets/transmission/widget.js b/src/widgets/transmission/widget.js
new file mode 100644
index 000000000..321f25baa
--- /dev/null
+++ b/src/widgets/transmission/widget.js
@@ -0,0 +1,8 @@
+import transmissionProxyHandler from "./proxy";
+
+const widget = {
+ api: "{url}/transmission/rpc",
+ proxyHandler: transmissionProxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 03a8e4a5b..78b177261 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -2,16 +2,38 @@ import adguard from "./adguard/widget";
import bazarr from "./bazarr/widget";
import coinmarketcap from "./coinmarketcap/widget";
import overseerr from "./overseerr/widget";
+import portainer from "./portainer/widget";
+import prowlarr from "./prowlarr/widget";
+import qbittorrent from "./qbittorrent/widget";
import radarr from "./radarr/widget";
-import sonarr from "./sonarr/widget"
+import sonarr from "./sonarr/widget";
+import readarr from "./readarr/widget";
+import rutorrent from "./rutorrent/widget";
+import sabnzbd from "./sabnzbd/widget";
+import speedtest from "./speedtest/widget";
+import strelaysrv from "./strelaysrv/widget";
+import tautulli from "./tautulli/widget";
+import traefik from "./traefik/widget";
+import transmission from "./transmission/widget";
const widgets = {
adguard,
bazarr,
coinmarketcap,
overseerr,
+ portainer,
+ prowlarr,
+ qbittorrent,
radarr,
sonarr,
+ readarr,
+ rutorrent,
+ sabnzbd,
+ speedtest,
+ strelaysrv,
+ tautulli,
+ traefik,
+ transmission,
};
export default widgets;