mirror of
https://github.com/DI0IK/homepage-plus.git
synced 2025-07-11 23:58:46 +00:00
Merge branch 'main' into kubernetes
This commit is contained in:
commit
174cb651b4
81 changed files with 2717 additions and 342 deletions
|
@ -14,6 +14,11 @@ export default function Component({ service }) {
|
|||
if (error) {
|
||||
return <Container error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Container service={service} />;
|
||||
}
|
||||
|
||||
const totalObserved = Object.keys(data).length;
|
||||
let diffsDetected = 0;
|
||||
|
||||
|
|
|
@ -17,11 +17,12 @@ export default function Component({ service }) {
|
|||
{ label: t("coinmarketcap.30days"), value: "30d" },
|
||||
];
|
||||
|
||||
const [dateRange, setDateRange] = useState(dateRangeOptions[0].value);
|
||||
|
||||
const { widget } = service;
|
||||
const { symbols } = widget;
|
||||
const currencyCode = widget.currency ?? "USD";
|
||||
const interval = widget.defaultinterval ?? dateRangeOptions[0].value;
|
||||
|
||||
const [dateRange, setDateRange] = useState(interval);
|
||||
|
||||
const { data: statsData, error: statsError } = useWidgetAPI(widget, "v1/cryptocurrency/quotes/latest", {
|
||||
symbol: `${symbols.join(",")}`,
|
||||
|
|
|
@ -7,9 +7,12 @@ const components = {
|
|||
bazarr: dynamic(() => import("./bazarr/component")),
|
||||
changedetectionio: dynamic(() => import("./changedetectionio/component")),
|
||||
coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
|
||||
deluge: dynamic(() => import("./deluge/component")),
|
||||
diskstation: dynamic(() => import("./diskstation/component")),
|
||||
docker: dynamic(() => import("./docker/component")),
|
||||
kubernetes: dynamic(() => import("./kubernetes/component")),
|
||||
emby: dynamic(() => import("./emby/component")),
|
||||
flood: dynamic(() => import("./flood/component")),
|
||||
gluetun: dynamic(() => import("./gluetun/component")),
|
||||
gotify: dynamic(() => import("./gotify/component")),
|
||||
hdhomerun: dynamic(() => import("./hdhomerun/component")),
|
||||
|
@ -24,6 +27,7 @@ const components = {
|
|||
nzbget: dynamic(() => import("./nzbget/component")),
|
||||
ombi: dynamic(() => import("./ombi/component")),
|
||||
overseerr: dynamic(() => import("./overseerr/component")),
|
||||
paperlessngx: dynamic(() => import("./paperlessngx/component")),
|
||||
pihole: dynamic(() => import("./pihole/component")),
|
||||
plex: dynamic(() => import("./plex/component")),
|
||||
portainer: dynamic(() => import("./portainer/component")),
|
||||
|
@ -35,6 +39,7 @@ const components = {
|
|||
readarr: dynamic(() => import("./readarr/component")),
|
||||
rutorrent: dynamic(() => import("./rutorrent/component")),
|
||||
sabnzbd: dynamic(() => import("./sabnzbd/component")),
|
||||
scrutiny: dynamic(() => import("./scrutiny/component")),
|
||||
sonarr: dynamic(() => import("./sonarr/component")),
|
||||
speedtest: dynamic(() => import("./speedtest/component")),
|
||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||
|
|
52
src/widgets/deluge/component.jsx
Normal file
52
src/widgets/deluge/component.jsx
Normal file
|
@ -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 <Container error={torrentError} />;
|
||||
}
|
||||
|
||||
if (!torrentData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="deluge.leech" />
|
||||
<Block label="deluge.download" />
|
||||
<Block label="deluge.seed" />
|
||||
<Block label="deluge.upload" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Container service={service}>
|
||||
<Block label="deluge.leech" value={t("common.number", { value: leech })} />
|
||||
<Block label="deluge.download" value={t("common.bitrate", { value: rateDl })} />
|
||||
<Block label="deluge.seed" value={t("common.number", { value: completed })} />
|
||||
<Block label="deluge.upload" value={t("common.bitrate", { value: rateUl })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
63
src/widgets/deluge/proxy.js
Normal file
63
src/widgets/deluge/proxy.js
Normal file
|
@ -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) {
|
||||
const [status, contentType, data] = await sendJsonRpcRequest(url, method, params);
|
||||
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, password) {
|
||||
return sendRpc(url, loginMethod, [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);
|
||||
if (status === 403) {
|
||||
[status, contentType, data] = await login(url, 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);
|
||||
}
|
||||
|
||||
return res.status(status).end(data);
|
||||
}
|
8
src/widgets/deluge/widget.js
Normal file
8
src/widgets/deluge/widget.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
import delugeProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/json",
|
||||
proxyHandler: delugeProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
41
src/widgets/diskstation/component.jsx
Normal file
41
src/widgets/diskstation/component.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
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: listData, error: listError } = useWidgetAPI(widget, "list");
|
||||
|
||||
if (listError) {
|
||||
return <Container error={listError} />;
|
||||
}
|
||||
|
||||
const tasks = listData?.data?.tasks;
|
||||
if (!tasks) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="diskstation.leech" />
|
||||
<Block label="diskstation.download" />
|
||||
<Block label="diskstation.seed" />
|
||||
<Block label="diskstation.upload" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const rateDl = tasks.reduce((acc, task) => acc + (task?.additional?.transfer?.speed_download ?? 0), 0);
|
||||
const rateUl = tasks.reduce((acc, task) => acc + (task?.additional?.transfer?.speed_upload ?? 0), 0);
|
||||
const completed = tasks.filter((task) => task?.additional?.transfer?.size_downloaded === task?.size)?.length || 0;
|
||||
const leech = tasks.length - completed || 0;
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="diskstation.leech" value={t("common.number", { value: leech })} />
|
||||
<Block label="diskstation.download" value={t("common.bitrate", { value: rateDl })} />
|
||||
<Block label="diskstation.seed" value={t("common.number", { value: completed })} />
|
||||
<Block label="diskstation.upload" value={t("common.bitrate", { value: rateUl })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
70
src/widgets/diskstation/proxy.js
Normal file
70
src/widgets/diskstation/proxy.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import createLogger from "utils/logger";
|
||||
import widgets from "widgets/widgets";
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
|
||||
const logger = createLogger("diskstationProxyHandler");
|
||||
const authApi = "{url}/webapi/auth.cgi?api=SYNO.API.Auth&version=2&method=login&account={username}&passwd={password}&session=DownloadStation&format=cookie"
|
||||
|
||||
async function login(widget) {
|
||||
const loginUrl = formatApiCall(authApi, widget);
|
||||
const [status, contentType, data] = await httpProxy(loginUrl);
|
||||
if (status !== 200) {
|
||||
return [status, contentType, data];
|
||||
}
|
||||
|
||||
const json = JSON.parse(data.toString());
|
||||
if (json?.success !== true) {
|
||||
// from https://global.download.synology.com/download/Document/Software/DeveloperGuide/Package/DownloadStation/All/enu/Synology_Download_Station_Web_API.pdf
|
||||
/*
|
||||
Code Description
|
||||
400 No such account or incorrect password
|
||||
401 Account disabled
|
||||
402 Permission denied
|
||||
403 2-step verification code required
|
||||
404 Failed to authenticate 2-step verification code
|
||||
*/
|
||||
let message = "Authentication failed.";
|
||||
if (json?.error?.code >= 403) message += " 2FA enabled.";
|
||||
logger.warn("Unable to login. Code: %d", json?.error?.code);
|
||||
return [401, "application/json", JSON.stringify({ code: json?.error?.code, message })];
|
||||
}
|
||||
|
||||
return [status, contentType, data];
|
||||
}
|
||||
|
||||
export default async function diskstationProxyHandler(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);
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
if (!api) {
|
||||
return res.status(403).json({ error: "Service does not support API calls" });
|
||||
}
|
||||
|
||||
const url = formatApiCall(api, { endpoint, ...widget });
|
||||
let [status, contentType, data] = await httpProxy(url);
|
||||
if (status !== 200) {
|
||||
logger.debug("Error %d calling endpoint %s", status, url);
|
||||
return res.status(status, data);
|
||||
}
|
||||
|
||||
const json = JSON.parse(data.toString());
|
||||
if (json?.success !== true) {
|
||||
logger.debug("Logging in to DiskStation");
|
||||
[status, contentType, data] = await login(widget);
|
||||
if (status !== 200) {
|
||||
return res.status(status).end(data)
|
||||
}
|
||||
|
||||
[status, contentType, data] = await httpProxy(url);
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
14
src/widgets/diskstation/widget.js
Normal file
14
src/widgets/diskstation/widget.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import diskstationProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/webapi/DownloadStation/task.cgi?api=SYNO.DownloadStation.Task&version=1&method={endpoint}",
|
||||
proxyHandler: diskstationProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"list": {
|
||||
endpoint: "list&additional=transfer",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -46,7 +46,9 @@ export default function Component({ service }) {
|
|||
return (
|
||||
<Container service={service}>
|
||||
<Block label="docker.cpu" value={t("common.percent", { value: calculateCPUPercent(statsData.stats) })} />
|
||||
<Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
||||
{statsData.stats.memory_stats.usage &&
|
||||
<Block label="docker.mem" value={t("common.bytes", { value: statsData.stats.memory_stats.usage })} />
|
||||
}
|
||||
{network && (
|
||||
<>
|
||||
<Block label="docker.rx" value={t("common.bytes", { value: network.rx_bytes })} />
|
||||
|
|
53
src/widgets/flood/component.jsx
Normal file
53
src/widgets/flood/component.jsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
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, "torrents");
|
||||
|
||||
if (torrentError || !torrentData?.torrents) {
|
||||
return <Container error={torrentError ?? {message: "No torrent data returned"}} />;
|
||||
}
|
||||
|
||||
if (!torrentData || !torrentData.torrents) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="flood.leech" />
|
||||
<Block label="flood.download" />
|
||||
<Block label="flood.seed" />
|
||||
<Block label="flood.upload" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
let rateDl = 0;
|
||||
let rateUl = 0;
|
||||
let completed = 0;
|
||||
let leech = 0;
|
||||
|
||||
Object.values(torrentData.torrents).forEach(torrent => {
|
||||
rateDl += torrent.downRate;
|
||||
rateUl += torrent.upRate;
|
||||
if(torrent.status.includes('complete')){
|
||||
completed += 1;
|
||||
}
|
||||
if(torrent.status.includes('downloading')){
|
||||
leech += 1;
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="flood.leech" value={t("common.number", { value: leech })} />
|
||||
<Block label="flood.download" value={t("common.bitrate", { value: rateDl })} />
|
||||
<Block label="flood.seed" value={t("common.number", { value: completed })} />
|
||||
<Block label="flood.upload" value={t("common.bitrate", { value: rateUl })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
66
src/widgets/flood/proxy.js
Normal file
66
src/widgets/flood/proxy.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
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";
|
||||
|
||||
const logger = createLogger("floodProxyHandler");
|
||||
|
||||
async function login(widget) {
|
||||
logger.debug("flood is rejecting the request, logging in.");
|
||||
const loginUrl = new URL(`${widget.url}/api/auth/authenticate`).toString();
|
||||
|
||||
const loginParams = {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: null
|
||||
};
|
||||
|
||||
if (widget.username && widget.password) {
|
||||
loginParams.body = JSON.stringify({
|
||||
"username": widget.username,
|
||||
"password": widget.password
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [status, contentType, data] = await httpProxy(loginUrl, loginParams);
|
||||
return [status, data];
|
||||
}
|
||||
|
||||
export default async function floodProxyHandler(req, res) {
|
||||
const { group, service, endpoint } = 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 url = new URL(formatApiCall("{url}/api/{endpoint}", { endpoint, ...widget }));
|
||||
const params = { method: "GET", headers: {} };
|
||||
|
||||
let [status, contentType, data] = await httpProxy(url, params);
|
||||
if (status === 401) {
|
||||
[status, data] = await login(widget);
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d logging in to flood. Data: %s", status, data);
|
||||
return res.status(status).end(data);
|
||||
}
|
||||
|
||||
[status, contentType, data] = await httpProxy(url, params);
|
||||
}
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d getting data from flood. Data: %s", status, data);
|
||||
}
|
||||
|
||||
if (contentType) res.setHeader("Content-Type", contentType);
|
||||
return res.status(status).send(data);
|
||||
}
|
7
src/widgets/flood/widget.js
Normal file
7
src/widgets/flood/widget.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
import floodProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
proxyHandler: floodProxyHandler,
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -20,12 +20,18 @@ async function login(loginUrl, username, password) {
|
|||
});
|
||||
|
||||
const status = authResponse[0];
|
||||
const data = JSON.parse(Buffer.from(authResponse[2]).toString());
|
||||
let data = authResponse[2];
|
||||
|
||||
if (status === 200) {
|
||||
cache.put(tokenCacheKey, data.token);
|
||||
try {
|
||||
data = JSON.parse(Buffer.from(authResponse[2]).toString());
|
||||
|
||||
if (status === 200) {
|
||||
const expiration = new Date(data.expires) - Date.now();
|
||||
cache.put(tokenCacheKey, data.token, expiration - (5 * 60 * 1000)); // expiration -5 minutes
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error ${status} logging into npm`, authResponse[2]);
|
||||
}
|
||||
|
||||
return [status, data.token ?? data];
|
||||
}
|
||||
|
||||
|
@ -51,8 +57,8 @@ export default async function npmProxyHandler(req, res) {
|
|||
if (!token) {
|
||||
[status, token] = await login(loginUrl, widget.username, widget.password);
|
||||
if (status !== 200) {
|
||||
logger.debug(`HTTTP ${status} logging into npm api: ${data}`);
|
||||
return res.status(status).send(data);
|
||||
logger.debug(`HTTTP ${status} logging into npm api: ${token}`);
|
||||
return res.status(status).send(token);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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" });
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -15,6 +15,7 @@ export default function Component({ service }) {
|
|||
return (
|
||||
<Container service={service}>
|
||||
<Block label="overseerr.pending" />
|
||||
<Block label="overseerr.processing" />
|
||||
<Block label="overseerr.approved" />
|
||||
<Block label="overseerr.available" />
|
||||
</Container>
|
||||
|
@ -24,6 +25,7 @@ export default function Component({ service }) {
|
|||
return (
|
||||
<Container service={service}>
|
||||
<Block label="overseerr.pending" value={statsData.pending} />
|
||||
<Block label="overseerr.processing" value={statsData.processing} />
|
||||
<Block label="overseerr.approved" value={statsData.approved} />
|
||||
<Block label="overseerr.available" value={statsData.available} />
|
||||
</Container>
|
||||
|
|
|
@ -9,6 +9,7 @@ const widget = {
|
|||
endpoint: "request/count",
|
||||
validate: [
|
||||
"pending",
|
||||
"processing",
|
||||
"approved",
|
||||
"available",
|
||||
],
|
||||
|
|
29
src/widgets/paperlessngx/component.jsx
Normal file
29
src/widgets/paperlessngx/component.jsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
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 { widget } = service;
|
||||
|
||||
const { data: statisticsData, error: statisticsError } = useWidgetAPI(widget, "statistics");
|
||||
|
||||
if (statisticsError) {
|
||||
return <Container error={statisticsError} />;
|
||||
}
|
||||
|
||||
if (!statisticsData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="paperlessngx.inbox" />
|
||||
<Block label="paperlessngx.total" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
{statisticsData.documents_inbox !== undefined && <Block label="paperlessngx.inbox" value={statisticsData.documents_inbox} />}
|
||||
<Block label="paperlessngx.total" value={statisticsData.documents_total} />
|
||||
</Container>
|
||||
);
|
||||
}
|
17
src/widgets/paperlessngx/widget.js
Normal file
17
src/widgets/paperlessngx/widget.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"statistics": {
|
||||
endpoint: "statistics/?format=json",
|
||||
validate: [
|
||||
"documents_total"
|
||||
]
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -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);
|
||||
|
|
|
@ -11,9 +11,14 @@ export default async function rutorrentProxyHandler(req, res) {
|
|||
if (widget) {
|
||||
const constructedUrl = new URL(widget.url);
|
||||
|
||||
let rtPort = constructedUrl.port;
|
||||
if (rtPort === '') {
|
||||
rtPort = constructedUrl.protocol === "https:" ? 443 : 80;
|
||||
}
|
||||
|
||||
const rutorrent = new RuTorrent({
|
||||
host: constructedUrl.hostname,
|
||||
port: constructedUrl.port,
|
||||
port: rtPort,
|
||||
path: constructedUrl.pathname,
|
||||
ssl: constructedUrl.protocol === "https:",
|
||||
username: widget.username,
|
||||
|
|
62
src/widgets/scrutiny/component.jsx
Normal file
62
src/widgets/scrutiny/component.jsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import Container from "components/services/widget/container";
|
||||
import Block from "components/services/widget/block";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
|
||||
// @see https://github.com/AnalogJ/scrutiny/blob/d8d56f77f9e868127c4849dac74d65512db658e8/webapp/frontend/src/app/shared/device-status.pipe.ts
|
||||
const DeviceStatus = {
|
||||
passed: 0,
|
||||
failed_smart: 1,
|
||||
failed_scrutiny: 2,
|
||||
failed_both: 3,
|
||||
|
||||
isFailed(s){ return s > this.passed && s <= this.failed_both},
|
||||
isUnknown(s){ return s < this.passed || s > this.failed_both}
|
||||
}
|
||||
|
||||
// @see https://github.com/AnalogJ/scrutiny/blob/d8d56f77f9e868127c4849dac74d65512db658e8/webapp/frontend/src/app/core/config/app.config.ts
|
||||
const DeviceStatusThreshold = {
|
||||
smart: 1,
|
||||
scrutiny: 2,
|
||||
both: 3
|
||||
}
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { widget } = service;
|
||||
|
||||
const { data: scrutinySettings, error: scrutinySettingsError } = useWidgetAPI(widget, "settings");
|
||||
const { data: scrutinyData, error: scrutinyError } = useWidgetAPI(widget, "summary");
|
||||
|
||||
if (scrutinyError || scrutinySettingsError) {
|
||||
const finalError = scrutinyError ?? scrutinySettingsError;
|
||||
return <Container error={finalError} />;
|
||||
}
|
||||
|
||||
if (!scrutinyData || !scrutinySettings) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="scrutiny.passed" />
|
||||
<Block label="scrutiny.failed" />
|
||||
<Block label="scrutiny.unknown" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const deviceIds = Object.values(scrutinyData.data.summary);
|
||||
const statusThreshold = scrutinySettings.settings.metrics.status_threshold;
|
||||
|
||||
const failed = deviceIds.filter(deviceId => (DeviceStatus.isFailed(deviceId.device.device_status) && statusThreshold === DeviceStatusThreshold.both) || [statusThreshold, DeviceStatus.failed_both].includes(deviceId.device.device_status))?.length || 0;
|
||||
const unknown = deviceIds.filter(deviceId => DeviceStatus.isUnknown(deviceId.device.device_status))?.length || 0;
|
||||
const passed = deviceIds.length - (failed + unknown);
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="scrutiny.passed" value={passed} />
|
||||
<Block label="scrutiny.failed" value={failed} />
|
||||
<Block label="scrutiny.unknown" value={unknown} />
|
||||
</Container>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
23
src/widgets/scrutiny/widget.js
Normal file
23
src/widgets/scrutiny/widget.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: genericProxyHandler,
|
||||
|
||||
mappings: {
|
||||
summary: {
|
||||
endpoint: "summary",
|
||||
validate: [
|
||||
"data",
|
||||
]
|
||||
},
|
||||
settings: {
|
||||
endpoint: "settings",
|
||||
validate: [
|
||||
"settings",
|
||||
]
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -4,7 +4,10 @@ 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 diskstation from "./diskstation/widget";
|
||||
import emby from "./emby/widget";
|
||||
import flood from "./flood/widget";
|
||||
import gluetun from "./gluetun/widget";
|
||||
import gotify from "./gotify/widget";
|
||||
import hdhomerun from "./hdhomerun/widget";
|
||||
|
@ -18,6 +21,7 @@ import npm from "./npm/widget";
|
|||
import nzbget from "./nzbget/widget";
|
||||
import ombi from "./ombi/widget";
|
||||
import overseerr from "./overseerr/widget";
|
||||
import paperlessngx from "./paperlessngx/widget";
|
||||
import pihole from "./pihole/widget";
|
||||
import plex from "./plex/widget";
|
||||
import portainer from "./portainer/widget";
|
||||
|
@ -29,6 +33,7 @@ import radarr from "./radarr/widget";
|
|||
import readarr from "./readarr/widget";
|
||||
import rutorrent from "./rutorrent/widget";
|
||||
import sabnzbd from "./sabnzbd/widget";
|
||||
import scrutiny from "./scrutiny/widget";
|
||||
import sonarr from "./sonarr/widget";
|
||||
import speedtest from "./speedtest/widget";
|
||||
import strelaysrv from "./strelaysrv/widget";
|
||||
|
@ -47,7 +52,10 @@ const widgets = {
|
|||
bazarr,
|
||||
changedetectionio,
|
||||
coinmarketcap,
|
||||
deluge,
|
||||
diskstation,
|
||||
emby,
|
||||
flood,
|
||||
gluetun,
|
||||
gotify,
|
||||
hdhomerun,
|
||||
|
@ -62,6 +70,7 @@ const widgets = {
|
|||
nzbget,
|
||||
ombi,
|
||||
overseerr,
|
||||
paperlessngx,
|
||||
pihole,
|
||||
plex,
|
||||
portainer,
|
||||
|
@ -73,6 +82,7 @@ const widgets = {
|
|||
readarr,
|
||||
rutorrent,
|
||||
sabnzbd,
|
||||
scrutiny,
|
||||
sonarr,
|
||||
speedtest,
|
||||
strelaysrv,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue