Add qBittorrent Widget

- extract cookie jar functionality into its own file
- use i18n for more strings in existing widgets

completes: #152
associated: #123
pull/192/head
Jason Fischer 2 years ago
parent bedeab686e
commit 6da1e98c83
No known key found for this signature in database

@ -70,6 +70,12 @@
"leech": "Leech",
"seed": "Seed"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"seed": "Seed"
},
"sonarr": {
"wanted": "Wanted",
"queued": "Queued",

@ -11,6 +11,7 @@ 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 QBittorrent from "./widgets/service/qbittorrent";
import Docker from "./widgets/service/docker";
import Pihole from "./widgets/service/pihole";
import Rutorrent from "./widgets/service/rutorrent";
@ -41,6 +42,7 @@ const widgetMappings = {
nzbget: Nzbget,
sabnzbd: SABnzbd,
transmission: Transmission,
qbittorrent: QBittorrent,
pihole: Pihole,
rutorrent: Rutorrent,
speedtest: Speedtest,

@ -29,8 +29,8 @@ export default function Bazarr({ service }) {
return (
<Widget>
<Block label={t("bazarr.missingEpisodes")} value={episodesData.total} />
<Block label={t("bazarr.missingMovies")} value={moviesData.total} />
<Block label={t("bazarr.missingEpisodes")} value={t("common.number", { value: episodesData.total })} />
<Block label={t("bazarr.missingMovies")} value={t("common.number", { value: moviesData.total })} />
</Widget>
);
}

@ -30,8 +30,8 @@ export default function Jackett({ service }) {
return (
<Widget>
<Block label={t("jackett.configured")} value={indexersData.length} />
<Block label={t("jackett.errored")} value={errored.length} />
<Block label={t("jackett.configured")} value={t("common.number", { value: indexersData.length })} />
<Block label={t("jackett.errored")} value={t("common.number", { value: errored.length })} />
</Widget>
);
}

@ -33,9 +33,9 @@ export default function Lidarr({ service }) {
return (
<Widget>
<Block label={t("lidarr.wanted")} value={wantedData.totalRecords} />
<Block label={t("lidarr.queued")} value={queueData.totalCount} />
<Block label={t("lidarr.albums")} value={have.length} />
<Block label={t("lidarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
<Block label={t("lidarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
<Block label={t("lidarr.albums")} value={t("common.number", { value: have.length })} />
</Widget>
);
}

@ -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 QBittorrent ({ service }) {
const { t } = useTranslation();
const config = service.widget;
const { data: torrentData, error: torrentError } = useSWR(formatApiUrl(config, "torrents/info"));
if (torrentError) {
return <Widget error={t("widget.api_error")} />;
}
if (!torrentData) {
return (
<Widget>
<Block label={t("qbittorrent.leech")} />
<Block label={t("qbittorrent.download")} />
<Block label={t("qbittorrent.seed")} />
<Block label={t("qbittorrent.upload")} />
</Widget>
);
}
let rateDl = 0;
let rateUl = 0;
let completed = 0;
for (let i = 0; i < torrentData.length; i += 1) {
const torrent = torrentData[i];
rateDl += torrent.dlspeed;
rateUl += torrent.upspeed;
if (torrent.progress === 1) {
completed += 1;
}
}
const leech = torrentData.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 (
<Widget>
<Block label={t("qbittorrent.leech")} value={t("common.number", { value: leech })} />
<Block label={t("qbittorrent.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
<Block label={t("qbittorrent.seed")} value={t("common.number", { value: completed })} />
<Block label={t("qbittorrent.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
</Widget>
);
}

@ -33,9 +33,9 @@ export default function Readarr({ service }) {
return (
<Widget>
<Block label={t("readarr.wanted")} value={wantedData.totalRecords} />
<Block label={t("readarr.queued")} value={queueData.totalCount} />
<Block label={t("readarr.books")} value={have.length} />
<Block label={t("readarr.wanted")} value={t("common.number", { value: wantedData.totalRecords })} />
<Block label={t("readarr.queued")} value={t("common.number", { value: queueData.totalCount })} />
<Block label={t("readarr.books")} value={t("common.number", { value: have.length })} />
</Widget>
);
}

@ -30,7 +30,7 @@ export default function SABnzbd({ service }) {
return (
<Widget>
<Block label={t("sabnzbd.rate")} value={`${queueData.queue.speed}B/s`} />
<Block label={t("sabnzbd.queue")} value={queueData.queue.noofslots} />
<Block label={t("sabnzbd.queue")} value={t("common.number", { value: queueData.queue.noofslots })} />
<Block label={t("sabnzbd.timeleft")} value={queueData.queue.timeleft} />
</Widget>
);

@ -61,9 +61,9 @@ export default function Transmission({ service }) {
return (
<Widget>
<Block label={t("transmission.leech")} value={leech} />
<Block label={t("transmission.leech")} value={t("common.number", { value: leech })} />
<Block label={t("transmission.download")} value={`${rateDl.toFixed(2)} ${unitsDl}`} />
<Block label={t("transmission.seed")} value={completed} />
<Block label={t("transmission.seed")} value={t("common.number", { value: completed })} />
<Block label={t("transmission.upload")} value={`${rateUl.toFixed(2)} ${unitsUl}`} />
</Widget>
);

@ -4,6 +4,7 @@ import rutorrentProxyHandler from "utils/proxies/rutorrent";
import nzbgetProxyHandler from "utils/proxies/nzbget";
import npmProxyHandler from "utils/proxies/npm";
import transmissionProxyHandler from "utils/proxies/transmission";
import qbittorrentProxyHandler from "utils/proxies/qbittorrent";
const serviceProxyHandlers = {
// uses query param auth
@ -34,6 +35,7 @@ const serviceProxyHandlers = {
nzbget: nzbgetProxyHandler,
npm: npmProxyHandler,
transmission: transmissionProxyHandler,
qbittorrent: qbittorrentProxyHandler,
};
export default async function handler(req, res) {

@ -10,6 +10,7 @@ const formats = {
portainer: `{url}/api/endpoints/{env}/{endpoint}`,
rutorrent: `{url}/plugins/httprpc/action.php`,
transmission: `{url}/transmission/rpc`,
qbittorrent: `{url}/api/v2/{endpoint}`,
jellyseerr: `{url}/api/v1/{endpoint}`,
overseerr: `{url}/api/v1/{endpoint}`,
ombi: `{url}/api/v1/{endpoint}`,

@ -0,0 +1,34 @@
/* eslint-disable no-param-reassign */
import { Cookie, CookieJar } from 'tough-cookie';
const cookieJar = new CookieJar();
export function setCookieHeader(url, params) {
// add cookie header, if we have one in the jar
const existingCookie = cookieJar.getCookieStringSync(url.toString());
if (existingCookie) {
params.headers = params.headers ?? {};
params.headers.Cookie = existingCookie;
}
}
export function addCookieToJar(url, headers) {
let cookieHeader = headers['set-cookie'];
if (headers instanceof Headers) {
cookieHeader = headers.get('set-cookie');
}
if (!cookieHeader || cookieHeader.length === 0) return;
let cookies = null;
if (cookieHeader instanceof Array) {
cookies = cookieHeader.map(Cookie.parse);
}
else {
cookies = [Cookie.parse(cookieHeader)];
}
for (let i = 0; i < cookies.length; i += 1) {
cookieJar.setCookieSync(cookies[i], url.toString());
}
}

@ -1,39 +1,15 @@
/* eslint-disable prefer-promise-reject-errors */
/* eslint-disable no-param-reassign */
import { http, https } from "follow-redirects";
import { Cookie, CookieJar } from 'tough-cookie';
const cookieJar = new CookieJar();
function setCookieHeader(url, params) {
// add cookie header, if we have one in the jar
const existingCookie = cookieJar.getCookieStringSync(url.toString());
if (existingCookie) {
params.headers = params.headers ?? {};
params.headers.Cookie = existingCookie;
}
}
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
function addCookieHandler(url, params) {
setCookieHeader(url, params);
// handle cookies during redirects
params.beforeRedirect = (options, responseInfo) => {
const cookieHeader = responseInfo.headers['set-cookie'];
if (!cookieHeader || cookieHeader.length === 0) return;
let cookies = null;
if (cookieHeader instanceof Array) {
cookies = cookieHeader.map(Cookie.parse);
}
else {
cookies = [Cookie.parse(cookieHeader)];
}
for (let i = 0; i < cookies.length; i += 1) {
cookieJar.setCookieSync(cookies[i], options.href);
}
addCookieToJar(options.href, responseInfo.headers);
setCookieHeader(options.href, options);
};
}
@ -49,6 +25,7 @@ export function httpsRequest(url, params) {
});
response.on("end", () => {
addCookieToJar(url, response.headers);
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
});
});
@ -76,6 +53,7 @@ export function httpRequest(url, params) {
});
response.on("end", () => {
addCookieToJar(url, response.headers);
resolve([response.statusCode, response.headers["content-type"], Buffer.concat(data), response.headers]);
});
});

@ -0,0 +1,58 @@
import { formatApiCall } from "utils/api-helpers";
import { addCookieToJar, setCookieHeader } from "utils/cookie-jar";
import { httpProxy } from "utils/http";
import getServiceWidget from "utils/service-helpers";
async function login(widget, params) {
const loginUrl = new URL(`${widget.url}/api/v2/auth/login`);
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, {
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]));
}
export default async function qbittorrentProxyHandler(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 params = { method: "GET", headers: {} };
setCookieHeader(url, params);
if (!params.headers.Cookie) {
const [status, data] = await login(widget, params);
if (status !== 200) {
return res.status(status).end(data);
}
if (data.toString() !== 'Ok.') {
return res.status(401).end(data);
}
}
const [status, contentType, data] = await httpProxy(url, params);
if (contentType) res.setHeader("Content-Type", contentType);
return res.status(status).send(data);
}
Loading…
Cancel
Save