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); +}