From b3db549a6505615d11819f1a5874d143f4d2e9f9 Mon Sep 17 00:00:00 2001 From: Jason Fischer Date: Mon, 12 Sep 2022 19:35:47 -0700 Subject: [PATCH 1/2] Add Transmission widget - Update http.js to support writing request bodies - Update http.js to support returning all response headers resolves: #104 --- public/locales/en/common.json | 6 ++ src/components/services/widget.jsx | 4 +- .../services/widgets/service/sabnzbd.jsx | 2 +- .../services/widgets/service/transmission.jsx | 69 +++++++++++++++++++ src/pages/api/services/proxy.js | 2 + src/utils/api-helpers.js | 1 + src/utils/http.js | 12 +++- src/utils/proxies/transmission.js | 56 +++++++++++++++ 8 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 src/components/services/widgets/service/transmission.jsx create mode 100644 src/utils/proxies/transmission.js diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 7c60dbe13..577e94227 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -63,6 +63,12 @@ "upload": "Upload", "download": "Download" }, + "transmission": { + "download": "Download", + "upload": "Upload", + "leech": "Leech", + "seed": "Seed" + }, "sonarr": { "wanted": "Wanted", "queued": "Queued", diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx index 2bebe84af..e4a67d25d 100644 --- a/src/components/services/widget.jsx +++ b/src/components/services/widget.jsx @@ -8,6 +8,7 @@ import Portainer from "./widgets/service/portainer"; import Emby from "./widgets/service/emby"; import Nzbget from "./widgets/service/nzbget"; import SABnzbd from "./widgets/service/sabnzbd"; +import Transmission from "./widgets/service/transmission"; import Docker from "./widgets/service/docker"; import Pihole from "./widgets/service/pihole"; import Rutorrent from "./widgets/service/rutorrent"; @@ -31,6 +32,8 @@ const widgetMappings = { emby: Emby, jellyfin: Jellyfin, nzbget: Nzbget, + sabnzbd: SABnzbd, + transmission: Transmission, pihole: Pihole, rutorrent: Rutorrent, speedtest: Speedtest, @@ -41,7 +44,6 @@ const widgetMappings = { npm: Npm, tautulli: Tautulli, gotify: Gotify, - sabnzbd: SABnzbd }; export default function Widget({ service }) { diff --git a/src/components/services/widgets/service/sabnzbd.jsx b/src/components/services/widgets/service/sabnzbd.jsx index d1a08a27e..8c777a70a 100644 --- a/src/components/services/widgets/service/sabnzbd.jsx +++ b/src/components/services/widgets/service/sabnzbd.jsx @@ -29,7 +29,7 @@ export default function SABnzbd({ service }) { return ( - + diff --git a/src/components/services/widgets/service/transmission.jsx b/src/components/services/widgets/service/transmission.jsx new file mode 100644 index 000000000..22d75288c --- /dev/null +++ b/src/components/services/widgets/service/transmission.jsx @@ -0,0 +1,69 @@ +import useSWR from "swr"; +import { useTranslation } from "react-i18next"; + +import Widget from "../widget"; +import Block from "../block"; + +import { formatApiUrl } from "utils/api-helpers"; + +export default function Transmission({ service }) { + const { t } = useTranslation(); + + const config = service.widget; + + const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config)); + + if (torrentError) { + return ; + } + + if (!torrentData) { + return ( + + + + + + + ); + } + + const torrents = torrentData.arguments.torrents; + let rateDl = 0; + let rateUl = 0; + let completed = 0; + + for (let torrent of torrents) { + rateDl += torrent.rateDownload; + rateUl += torrent.rateUpload; + if (torrent.percentDone === 1) { + completed++; + } + } + + 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/pages/api/services/proxy.js b/src/pages/api/services/proxy.js index 0a4440293..7c64e4357 100644 --- a/src/pages/api/services/proxy.js +++ b/src/pages/api/services/proxy.js @@ -3,6 +3,7 @@ import credentialedProxyHandler from "utils/proxies/credentialed"; import rutorrentProxyHandler from "utils/proxies/rutorrent"; import nzbgetProxyHandler from "utils/proxies/nzbget"; import npmProxyHandler from "utils/proxies/npm"; +import transmissionProxyHandler from "utils/proxies/transmission"; const serviceProxyHandlers = { // uses query param auth @@ -27,6 +28,7 @@ const serviceProxyHandlers = { rutorrent: rutorrentProxyHandler, nzbget: nzbgetProxyHandler, npm: npmProxyHandler, + transmission: transmissionProxyHandler, }; export default async function handler(req, res) { diff --git a/src/utils/api-helpers.js b/src/utils/api-helpers.js index 340ffaaae..2085fafcf 100644 --- a/src/utils/api-helpers.js +++ b/src/utils/api-helpers.js @@ -9,6 +9,7 @@ const formats = { traefik: `{url}/api/{endpoint}`, portainer: `{url}/api/endpoints/{env}/{endpoint}`, rutorrent: `{url}/plugins/httprpc/action.php`, + transmission: `{url}/transmission/rpc`, jellyseerr: `{url}/api/v1/{endpoint}`, overseerr: `{url}/api/v1/{endpoint}`, ombi: `{url}/api/v1/{endpoint}`, diff --git a/src/utils/http.js b/src/utils/http.js index 5f32f1a15..76e882ed6 100644 --- a/src/utils/http.js +++ b/src/utils/http.js @@ -12,7 +12,7 @@ export function httpsRequest(url, params) { }); response.on("end", () => { - resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data)]); + resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]); }); }); @@ -20,6 +20,10 @@ export function httpsRequest(url, params) { reject([500, error]); }); + if (params.body) { + request.write(params.body); + } + request.end(); }); } @@ -34,7 +38,7 @@ export function httpRequest(url, params) { }); response.on("end", () => { - resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data)]); + resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]); }); }); @@ -42,6 +46,10 @@ export function httpRequest(url, params) { reject([500, error]); }); + if (params.body) { + request.write(params.body); + } + request.end(); }); } diff --git a/src/utils/proxies/transmission.js b/src/utils/proxies/transmission.js new file mode 100644 index 000000000..250b235c2 --- /dev/null +++ b/src/utils/proxies/transmission.js @@ -0,0 +1,56 @@ +import { httpProxy } from "utils/http"; +import { formatApiCall } from "utils/api-helpers"; + +import getServiceWidget from "utils/service-helpers"; + +export default async function transmissionProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (!group || !service) { + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const widget = await getServiceWidget(group, service); + + if (!widget) { + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const url = new URL(formatApiCall(widget.type, { endpoint, ...widget })); + const csrfHeaderName = "x-transmission-session-id"; + + const method = "POST"; + const body = JSON.stringify({ + method: "torrent-get", + arguments: { + fields: ["percentDone", "status", "rateDownload", "rateUpload"] + } + }); + + const reqHeaders = { + "content-type": "application/json", + }; + + let [status, contentType, data, responseHeaders] = await httpProxy(url, { + method: method, + auth: `${widget.username}:${widget.password}`, + body: body, + headers: reqHeaders, + }); + + if (status === 409) { + // Transmission is rejecting the request, but returning a CSRF token + reqHeaders[csrfHeaderName] = responseHeaders[csrfHeaderName]; + + // retry the request, now with the CSRF token + [status, contentType, data] = await httpProxy(url, { + method: method, + auth: `${widget.username}:${widget.password}`, + body: body, + headers: reqHeaders, + }); + } + + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(data); +} From b19b4f047ef8096f5954d2c901e4f9d1f5da1e2f Mon Sep 17 00:00:00 2001 From: Jason Fischer Date: Mon, 12 Sep 2022 20:06:00 -0700 Subject: [PATCH 2/2] Fix linting errors --- .../services/widgets/service/transmission.jsx | 7 +++--- src/utils/proxies/transmission.js | 24 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/services/widgets/service/transmission.jsx b/src/components/services/widgets/service/transmission.jsx index 22d75288c..f8f1f25d4 100644 --- a/src/components/services/widgets/service/transmission.jsx +++ b/src/components/services/widgets/service/transmission.jsx @@ -28,16 +28,17 @@ export default function Transmission({ service }) { ); } - const torrents = torrentData.arguments.torrents; + const { torrents } = torrentData.arguments; let rateDl = 0; let rateUl = 0; let completed = 0; - for (let torrent of torrents) { + for (let i = 0; i < torrents.length; i += 1) { + const torrent = torrents[i]; rateDl += torrent.rateDownload; rateUl += torrent.rateUpload; if (torrent.percentDone === 1) { - completed++; + completed += 1; } } diff --git a/src/utils/proxies/transmission.js b/src/utils/proxies/transmission.js index 250b235c2..cd909a68b 100644 --- a/src/utils/proxies/transmission.js +++ b/src/utils/proxies/transmission.js @@ -1,6 +1,5 @@ import { httpProxy } from "utils/http"; import { formatApiCall } from "utils/api-helpers"; - import getServiceWidget from "utils/service-helpers"; export default async function transmissionProxyHandler(req, res) { @@ -20,6 +19,7 @@ export default async function transmissionProxyHandler(req, res) { const csrfHeaderName = "x-transmission-session-id"; const method = "POST"; + const auth = `${widget.username}:${widget.password}`; const body = JSON.stringify({ method: "torrent-get", arguments: { @@ -27,27 +27,27 @@ export default async function transmissionProxyHandler(req, res) { } }); - const reqHeaders = { + const headers = { "content-type": "application/json", }; let [status, contentType, data, responseHeaders] = await httpProxy(url, { - method: method, - auth: `${widget.username}:${widget.password}`, - body: body, - headers: reqHeaders, + method, + auth, + body, + headers, }); if (status === 409) { // Transmission is rejecting the request, but returning a CSRF token - reqHeaders[csrfHeaderName] = responseHeaders[csrfHeaderName]; + headers[csrfHeaderName] = responseHeaders[csrfHeaderName]; // retry the request, now with the CSRF token - [status, contentType, data] = await httpProxy(url, { - method: method, - auth: `${widget.username}:${widget.password}`, - body: body, - headers: reqHeaders, + [status, contentType, data, responseHeaders] = await httpProxy(url, { + method, + auth, + body, + headers, }); }