diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 7c60dbe1..577e9422 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 2bebe84a..e4a67d25 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 d1a08a27..8c777a70 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 00000000..22d75288
--- /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 0a444029..7c64e435 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 340ffaaa..2085fafc 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 5f32f1a1..76e882ed 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 00000000..250b235c
--- /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);
+}