diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 063e33b55..2ab724a85 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 000000000..274276122
--- /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) {
+ headers.authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
+ }
+
+ const client = new JSONRPCClient(async (rpcRequest) => {
+ const body = JSON.stringify(rpcRequest);
+ const httpRequestParams = {
+ method: "POST",
+ headers,
+ body
+ };
+
+ // eslint-disable-next-line no-unused-vars
+ const [status, contentType, data] = await httpProxy(url, httpRequestParams);
+ if (status === 200) {
+ const json = JSON.parse(data.toString());
+
+ // 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(data?.error ? data : new Error(data.toString()));
+ });
+
+ try {
+ const response = await client.request(method, params);
+ return [200, "application/json", JSON.stringify(response)];
+ }
+ catch (e) {
+ if (e instanceof JSONRPCErrorException) {
+ logger.debug("Error calling JSONPRC endpoint: %s. %s", url, e.message);
+ 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);
+ return 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/utils/proxy/http.js b/src/utils/proxy/http.js
index 16b58bf70..e07f06ff8 100644
--- a/src/utils/proxy/http.js
+++ b/src/utils/proxy/http.js
@@ -18,10 +18,15 @@ function addCookieHandler(url, params) {
};
}
-export function httpsRequest(url, params) {
+function handleRequest(requestor, url, params) {
return new Promise((resolve, reject) => {
addCookieHandler(url, params);
- const request = https.request(url, params, (response) => {
+ if (params?.body) {
+ params.headers = params.headers ?? {};
+ params.headers['content-length'] = Buffer.byteLength(params.body);
+ }
+
+ const request = requestor.request(url, params, (response) => {
const data = [];
response.on("data", (chunk) => {
@@ -38,7 +43,7 @@ export function httpsRequest(url, params) {
reject([500, error]);
});
- if (params.body) {
+ if (params?.body) {
request.write(params.body);
}
@@ -46,32 +51,12 @@ export function httpsRequest(url, params) {
});
}
-export function httpRequest(url, params) {
- return new Promise((resolve, reject) => {
- addCookieHandler(url, params);
- const request = http.request(url, params, (response) => {
- const data = [];
-
- response.on("data", (chunk) => {
- data.push(chunk);
- });
-
- response.on("end", () => {
- addCookieToJar(url, response.headers);
- resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
- });
- });
-
- request.on("error", (error) => {
- reject([500, error]);
- });
-
- if (params.body) {
- request.write(params.body);
- }
+export function httpsRequest(url, params) {
+ return handleRequest(https, url, params);
+}
- request.end();
- });
+export function httpRequest(url, params) {
+ return handleRequest(http, url, params);
}
export async function httpProxy(url, params = {}) {
diff --git a/src/widgets/components.js b/src/widgets/components.js
index eb7c8127e..68f114765 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 000000000..6615cac0c
--- /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;
+ const keys = torrents ? Object.keys(torrents) : [];
+
+ let rateDl = 0;
+ let rateUl = 0;
+ let completed = 0;
+ for (let i = 0; i < keys.length; i += 1) {
+ const torrent = torrents[keys[i]];
+ rateDl += torrent.download_payload_rate;
+ rateUl += torrent.upload_payload_rate;
+ completed += torrent.total_remaining === 0 ? 1 : 0;
+ }
+
+ const leech = keys.length - completed || 0;
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/deluge/proxy.js b/src/widgets/deluge/proxy.js
new file mode 100644
index 000000000..e9dac0d9a
--- /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 000000000..b5518b666
--- /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 4feac7812..000000000
--- 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 975c8dea7..841fb66c0 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/qbittorrent/proxy.js b/src/widgets/qbittorrent/proxy.js
index 14271b656..e1ea7f901 100644
--- a/src/widgets/qbittorrent/proxy.js
+++ b/src/widgets/qbittorrent/proxy.js
@@ -1,30 +1,23 @@
import { formatApiCall } from "utils/proxy/api-helpers";
-import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
const logger = createLogger("qbittorrentProxyHandler");
-async function login(widget, params) {
+async function login(widget) {
logger.debug("qBittorrent is rejecting the request, logging in.");
const loginUrl = new URL(`${widget.url}/api/v2/auth/login`).toString();
const loginBody = `username=${encodeURI(widget.username)}&password=${encodeURI(widget.password)}`;
-
- // using fetch intentionally, for login only, as the httpProxy method causes qBittorrent to
- // complain about header encoding
- return fetch(loginUrl, {
+ const loginParams = {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: loginBody,
- })
- .then(async (response) => {
- addCookieToJar(loginUrl, response.headers);
- setCookieHeader(loginUrl, params);
- const data = await response.text();
- return [response.status, data];
- })
- .catch((err) => [500, err]);
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ const [status, contentType, data] = await httpProxy(loginUrl, loginParams);
+ return [status, data];
}
export default async function qbittorrentProxyHandler(req, res) {
@@ -44,11 +37,10 @@ export default async function qbittorrentProxyHandler(req, res) {
const url = new URL(formatApiCall("{url}/api/v2/{endpoint}", { endpoint, ...widget }));
const params = { method: "GET", headers: {} };
- setCookieHeader(url, params);
let [status, contentType, data] = await httpProxy(url, params);
if (status === 403) {
- [status, data] = await login(widget, params);
+ [status, data] = await login(widget);
if (status !== 200) {
logger.error("HTTP %d logging in to qBittorrent. Data: %s", status, data);
@@ -59,9 +51,9 @@ export default async function qbittorrentProxyHandler(req, res) {
logger.error("Error logging in to qBittorrent: Data: %s", data);
return res.status(401).end(data);
}
- }
- [status, contentType, data] = await httpProxy(url, params);
+ [status, contentType, data] = await httpProxy(url, params);
+ }
if (status !== 200) {
logger.error("HTTP %d getting data from qBittorrent. Data: %s", status, data);
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 2d2f453d4..72417b77a 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";
@@ -49,6 +50,7 @@ const widgets = {
bazarr,
changedetectionio,
coinmarketcap,
+ deluge,
emby,
gluetun,
gotify,