diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 4d1b5774..8098273e 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -112,6 +112,12 @@
"leech": "Leech",
"seed": "Seed"
},
+ "deluge": {
+ "download": "Download",
+ "upload": "Upload",
+ "leech": "Leech",
+ "seed": "Seed"
+ },
"sonarr": {
"wanted": "Wanted",
"queued": "Queued",
diff --git a/src/utils/proxy/handlers/jsonrpc.js b/src/utils/proxy/handlers/jsonrpc.js
new file mode 100644
index 00000000..9677fa50
--- /dev/null
+++ b/src/utils/proxy/handlers/jsonrpc.js
@@ -0,0 +1,82 @@
+import { JSONRPCClient, JSONRPCErrorException } from "json-rpc-2.0";
+
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+import widgets from "widgets/widgets";
+
+const logger = createLogger("jsonrpcProxyHandler");
+
+export async function sendJsonRpcRequest(url, method, params, username, password) {
+ const headers = {
+ "content-type": "application/json",
+ "accept": "application/json"
+ }
+
+ if (username && password) {
+ const authorization = Buffer.from(`${username}:${password}`).toString("base64");
+ headers.authorization = `Basic ${authorization}`;
+ }
+
+ const client = new JSONRPCClient(async (rpcRequest) => {
+ const httpRequestParams = {
+ method: "POST",
+ headers,
+ body: JSON.stringify(rpcRequest)
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ const [status, contentType, data] = await httpProxy(url, httpRequestParams);
+ const dataString = data.toString();
+ if (status === 200) {
+ const json = JSON.parse(dataString);
+
+ // in order to get access to the underlying error object in the JSON response
+ // you must set `result` equal to undefined
+ if (json.error && (json.result === null)) {
+ json.result = undefined;
+ }
+ return client.receive(json);
+ }
+
+ return Promise.reject(new Error(dataString));
+ });
+
+ try {
+ const response = await client.request(method, params);
+ return [200, "application/json", JSON.stringify(response)];
+ }
+ catch (e) {
+ if (e instanceof JSONRPCErrorException) {
+ return [200, "application/json", JSON.stringify({result: null, error: {code: e.code, message: e.message}})];
+ }
+
+ logger.warn("Error calling JSONPRC endpoint: %s. %s", url, e);
+ return [500, "application/json", JSON.stringify({result: null, error: {code: 2, message: e.toString()}})];
+ }
+}
+
+export default async function jsonrpcProxyHandler(req, res) {
+ const { group, service, endpoint: method } = req.query;
+
+ if (group && service) {
+ const widget = await getServiceWidget(group, service);
+ const api = widgets?.[widget.type]?.api;
+
+ if (!api) {
+ return res.status(403).json({ error: "Service does not support API calls" });
+ }
+
+ if (widget) {
+ const url = formatApiCall(api, { ...widget });
+
+ // eslint-disable-next-line no-unused-vars
+ const [status, contentType, data] = await sendJsonRpcRequest(url, method, null, widget.username, widget.password);
+ res.status(status).end(data);
+ }
+ }
+
+ logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/widgets/components.js b/src/widgets/components.js
index b781172b..e15ed4d8 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -7,6 +7,7 @@ const components = {
bazarr: dynamic(() => import("./bazarr/component")),
changedetectionio: dynamic(() => import("./changedetectionio/component")),
coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
+ deluge: dynamic(() => import("./deluge/component")),
docker: dynamic(() => import("./docker/component")),
emby: dynamic(() => import("./emby/component")),
gluetun: dynamic(() => import("./gluetun/component")),
diff --git a/src/widgets/deluge/component.jsx b/src/widgets/deluge/component.jsx
new file mode 100644
index 00000000..40f8c672
--- /dev/null
+++ b/src/widgets/deluge/component.jsx
@@ -0,0 +1,52 @@
+import { useTranslation } from "next-i18next";
+
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const { widget } = service;
+
+ const { data: torrentData, error: torrentError } = useWidgetAPI(widget);
+
+ if (torrentError) {
+ return ;
+ }
+
+ if (!torrentData) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ const { torrents } = torrentData;
+ let count = 0;
+ let rateDl = 0;
+ let rateUl = 0;
+ let completed = 0;
+ for (const key of Object.keys(torrents)) {
+ const torrent = torrents[key];
+ count += 1;
+ rateDl += torrent.download_payload_rate;
+ rateUl += torrent.upload_payload_rate;
+ completed += torrent.total_remaining === 0 ? 1 : 0;
+ }
+
+ const leech = count - completed || 0;
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/deluge/proxy.js b/src/widgets/deluge/proxy.js
new file mode 100644
index 00000000..e9dac0d9
--- /dev/null
+++ b/src/widgets/deluge/proxy.js
@@ -0,0 +1,63 @@
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { sendJsonRpcRequest } from "utils/proxy/handlers/jsonrpc";
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+import widgets from "widgets/widgets";
+
+const logger = createLogger("delugeProxyHandler");
+
+const dataMethod = "web.update_ui";
+const dataParams = [
+ ["queue", "name", "total_wanted", "state", "progress", "download_payload_rate", "upload_payload_rate", "total_remaining"],
+ {}
+];
+const loginMethod = "auth.login";
+
+async function sendRpc(url, method, params, username, password) {
+ const [status, contentType, data] = await sendJsonRpcRequest(url, method, params, username, password);
+ const json = JSON.parse(data.toString());
+ if (json?.error) {
+ if (json.error.code === 1) {
+ return [403, contentType, data];
+ }
+ return [500, contentType, data];
+ }
+
+ return [status, contentType, data];
+}
+
+function login(url, username, password) {
+ return sendRpc(url, loginMethod, [password], username, password);
+}
+
+export default async function delugeProxyHandler(req, res) {
+ const { group, service } = req.query;
+
+ if (!group || !service) {
+ logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+ }
+
+ const widget = await getServiceWidget(group, service);
+
+ if (!widget) {
+ logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+ }
+
+ const api = widgets?.[widget.type]?.api
+ const url = new URL(formatApiCall(api, { ...widget }));
+
+ let [status, contentType, data] = await sendRpc(url, dataMethod, dataParams, widget.username, widget.password);
+ if (status === 403) {
+ [status, contentType, data] = await login(url, widget.username, widget.password);
+ if (status !== 200) {
+ return res.status(status).end(data);
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ [status, contentType, data] = await sendRpc(url, dataMethod, dataParams, widget.username, widget.password);
+ }
+
+ return res.status(status).end(data);
+}
diff --git a/src/widgets/deluge/widget.js b/src/widgets/deluge/widget.js
new file mode 100644
index 00000000..b5518b66
--- /dev/null
+++ b/src/widgets/deluge/widget.js
@@ -0,0 +1,8 @@
+import delugeProxyHandler from "./proxy";
+
+const widget = {
+ api: "{url}/json",
+ proxyHandler: delugeProxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/nzbget/proxy.js b/src/widgets/nzbget/proxy.js
deleted file mode 100644
index 4feac781..00000000
--- a/src/widgets/nzbget/proxy.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { JSONRPCClient } from "json-rpc-2.0";
-
-import getServiceWidget from "utils/config/service-helpers";
-
-export default async function nzbgetProxyHandler(req, res) {
- const { group, service, endpoint } = req.query;
-
- if (group && service) {
- const widget = await getServiceWidget(group, service);
-
- if (widget) {
- const constructedUrl = new URL(widget.url);
- constructedUrl.pathname = "jsonrpc";
-
- const authorization = Buffer.from(`${widget.username}:${widget.password}`).toString("base64");
-
- const client = new JSONRPCClient((jsonRPCRequest) =>
- fetch(constructedUrl.toString(), {
- method: "POST",
- headers: {
- "content-type": "application/json",
- authorization: `Basic ${authorization}`,
- },
- body: JSON.stringify(jsonRPCRequest),
- }).then(async (response) => {
- if (response.status === 200) {
- const jsonRPCResponse = await response.json();
- return client.receive(jsonRPCResponse);
- }
-
- return Promise.reject(new Error(response.statusText));
- })
- );
-
- return res.send(await client.request(endpoint));
- }
- }
-
- return res.status(400).json({ error: "Invalid proxy service type" });
-}
diff --git a/src/widgets/nzbget/widget.js b/src/widgets/nzbget/widget.js
index 975c8dea..841fb66c 100644
--- a/src/widgets/nzbget/widget.js
+++ b/src/widgets/nzbget/widget.js
@@ -1,7 +1,8 @@
-import nzbgetProxyHandler from "./proxy";
+import jsonrpcProxyHandler from "utils/proxy/handlers/jsonrpc";
const widget = {
- proxyHandler: nzbgetProxyHandler,
+ api: "{url}/jsonrpc",
+ proxyHandler: jsonrpcProxyHandler,
};
export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index fe432832..6d5c4088 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -4,6 +4,7 @@ import autobrr from "./autobrr/widget";
import bazarr from "./bazarr/widget";
import changedetectionio from "./changedetectionio/widget";
import coinmarketcap from "./coinmarketcap/widget";
+import deluge from "./deluge/widget";
import emby from "./emby/widget";
import gluetun from "./gluetun/widget";
import gotify from "./gotify/widget";
@@ -47,6 +48,7 @@ const widgets = {
bazarr,
changedetectionio,
coinmarketcap,
+ deluge,
emby,
gluetun,
gotify,