From 5b21444c2ef5ab0a63969f239a2bd27ea8758610 Mon Sep 17 00:00:00 2001 From: stuffinator Date: Sun, 6 Nov 2022 11:05:31 +0100 Subject: [PATCH 1/6] Add Pyload widget --- public/locales/de/common.json | 6 +++++ public/locales/en/common.json | 6 +++++ src/widgets/components.js | 1 + src/widgets/pyload/component.jsx | 27 +++++++++++++++++++++++ src/widgets/pyload/proxy.js | 38 ++++++++++++++++++++++++++++++++ src/widgets/pyload/widget.js | 8 +++++++ src/widgets/widgets.js | 2 ++ 7 files changed, 88 insertions(+) create mode 100644 src/widgets/pyload/component.jsx create mode 100644 src/widgets/pyload/proxy.js create mode 100644 src/widgets/pyload/widget.js diff --git a/public/locales/de/common.json b/public/locales/de/common.json index ec3b6e394..f13cd0282 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -292,5 +292,11 @@ "containers_scanned": "Scanned", "containers_updated": "Updated", "containers_failed": "Failed" + }, + "pyload": { + "speed": "Geschwindigkeit", + "active": "Aktiv", + "queue": "Warteschlange", + "total": "Gesamt" } } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index dd00ff86e..913a3d9ba 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -303,5 +303,11 @@ "rejectedPushes": "Rejected", "filters": "Filters", "indexers": "Indexers" + }, + "pyload": { + "speed": "Speed", + "active": "Active", + "queue": "Queue", + "total": "Total" } } diff --git a/src/widgets/components.js b/src/widgets/components.js index c2b501890..ce5aa41a8 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -25,6 +25,7 @@ const components = { portainer: dynamic(() => import("./portainer/component")), prowlarr: dynamic(() => import("./prowlarr/component")), proxmox: dynamic(() => import("./proxmox/component")), + pyload: dynamic(() => import("./pyload/component")), qbittorrent: dynamic(() => import("./qbittorrent/component")), radarr: dynamic(() => import("./radarr/component")), readarr: dynamic(() => import("./readarr/component")), diff --git a/src/widgets/pyload/component.jsx b/src/widgets/pyload/component.jsx new file mode 100644 index 000000000..a15aab4cd --- /dev/null +++ b/src/widgets/pyload/component.jsx @@ -0,0 +1,27 @@ +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: pyloadData, error: pyloadError } = useWidgetAPI( + widget, + 'statusServer', + ) + + if (pyloadError || !pyloadData) { + return + } + + return ( + + + + + + + ) +} diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js new file mode 100644 index 000000000..35fb7becc --- /dev/null +++ b/src/widgets/pyload/proxy.js @@ -0,0 +1,38 @@ +import getServiceWidget from "utils/config/service-helpers"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import widgets from "widgets/widgets"; + +export default async function pyloadProxyHandler(req, res) { + const { group, service, endpoint } = req.query; + + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + const loginUrl = `${widget.url}/api/login`; + + // Pyload api does not support argument passing as JSON. + const sessionId = await fetch(loginUrl, { + method: "POST", + // Empty passwords are supported. + body: `username=${widget.username}&password=${widget.password ?? ''}`, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }).then((response) => response.json()); + + const apiResponse = await fetch(url, { + method: "POST", + body: `session=${sessionId}`, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }).then((response) => response.json()); + + return res.send(apiResponse); + } + } + + return res.status(400).json({ error: "Invalid proxy service type" }); +} diff --git a/src/widgets/pyload/widget.js b/src/widgets/pyload/widget.js new file mode 100644 index 000000000..3d2f2958f --- /dev/null +++ b/src/widgets/pyload/widget.js @@ -0,0 +1,8 @@ +import pyloadProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: pyloadProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 74f426b36..eb5bec488 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -20,6 +20,7 @@ import plex from "./plex/widget"; import portainer from "./portainer/widget"; import prowlarr from "./prowlarr/widget"; import proxmox from "./proxmox/widget"; +import pyload from "./pyload/widget"; import qbittorrent from "./qbittorrent/widget"; import radarr from "./radarr/widget"; import readarr from "./readarr/widget"; @@ -58,6 +59,7 @@ const widgets = { portainer, prowlarr, proxmox, + pyload, qbittorrent, radarr, readarr, From 02027deb06d0c1419b9aff0498b8170d7e2c8a6a Mon Sep 17 00:00:00 2001 From: stuffinator Date: Sun, 6 Nov 2022 11:45:25 +0100 Subject: [PATCH 2/6] code styling --- src/widgets/pyload/component.jsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/widgets/pyload/component.jsx b/src/widgets/pyload/component.jsx index a15aab4cd..e35bb3b56 100644 --- a/src/widgets/pyload/component.jsx +++ b/src/widgets/pyload/component.jsx @@ -1,19 +1,19 @@ 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' +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 { t } = useTranslation(); + const { widget } = service; const { data: pyloadData, error: pyloadError } = useWidgetAPI( widget, - 'statusServer', - ) + "statusServer", + ); if (pyloadError || !pyloadData) { - return + return ; } return ( @@ -23,5 +23,5 @@ export default function Component({ service }) { - ) + ); } From bbacf4e671e47d233d0850746fb172d971008018 Mon Sep 17 00:00:00 2001 From: stuffinator Date: Sun, 6 Nov 2022 17:07:56 +0100 Subject: [PATCH 3/6] reverted changes to German language file --- public/locales/de/common.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/public/locales/de/common.json b/public/locales/de/common.json index b33c8668c..315ec0e4f 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -304,11 +304,5 @@ "uptime": "Uptime", "alerts": "Alerts", "time": "{{value, number(style: unit; unitDisplay: long;)}}" - }, - "pyload": { - "speed": "Geschwindigkeit", - "active": "Aktiv", - "queue": "Warteschlange", - "total": "Gesamt" } } From 8b2b8d7b358411a8fba8a04b2e9337b26e51a433 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:35:41 -0800 Subject: [PATCH 4/6] Cache Pyload widget login sessionId, refactor --- src/widgets/pyload/component.jsx | 18 ++++++--- src/widgets/pyload/proxy.js | 68 +++++++++++++++++++++++--------- src/widgets/pyload/widget.js | 8 +++- 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/widgets/pyload/component.jsx b/src/widgets/pyload/component.jsx index e35bb3b56..958733c31 100644 --- a/src/widgets/pyload/component.jsx +++ b/src/widgets/pyload/component.jsx @@ -7,15 +7,23 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; - const { data: pyloadData, error: pyloadError } = useWidgetAPI( - widget, - "statusServer", - ); + const { data: pyloadData, error: pyloadError } = useWidgetAPI(widget, "status"); - if (pyloadError || !pyloadData) { + if (pyloadError || pyloadData?.error) { return ; } + if (!pyloadData) { + return ( + + + + + + + ); + } + return ( diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js index 35fb7becc..d96b859c1 100644 --- a/src/widgets/pyload/proxy.js +++ b/src/widgets/pyload/proxy.js @@ -1,6 +1,36 @@ +import cache from "memory-cache"; + import getServiceWidget from "utils/config/service-helpers"; import { formatApiCall } from "utils/proxy/api-helpers"; import widgets from "widgets/widgets"; +import createLogger from "utils/logger"; + +const proxyName = 'pyloadProxyHandler'; +const logger = createLogger(proxyName); +const sessionCacheKey = `${proxyName}__sessionId`; + +async function fetchFromPyloadAPI(url, sessionId, params) { + const options = { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }; + + if (params) { + options.body = Object.keys(params).map(k => `${k}=${params[k]}`).join('&'); + } else { + options.body = `session=${sessionId}` + } + + return fetch(url, options).then((response) => response.json()); +} + +async function login(loginUrl, username, password) { + const sessionId = await fetchFromPyloadAPI(loginUrl, null, { username, password }) + cache.put(sessionCacheKey, sessionId); + return sessionId; +} export default async function pyloadProxyHandler(req, res) { const { group, service, endpoint } = req.query; @@ -12,27 +42,29 @@ export default async function pyloadProxyHandler(req, res) { const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); const loginUrl = `${widget.url}/api/login`; - // Pyload api does not support argument passing as JSON. - const sessionId = await fetch(loginUrl, { - method: "POST", - // Empty passwords are supported. - body: `username=${widget.username}&password=${widget.password ?? ''}`, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }).then((response) => response.json()); - - const apiResponse = await fetch(url, { - method: "POST", - body: `session=${sessionId}`, - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }).then((response) => response.json()); + let sessionId = cache.get(sessionCacheKey); + + if (!sessionId) { + sessionId = await login(loginUrl, widget.username, widget.password); + } + let apiResponse = await fetchFromPyloadAPI(url, sessionId); + + if (apiResponse?.error === 'Forbidden') { + logger.debug("Failed to retrieve data from Pyload API, login and re-try"); + cache.del(sessionCacheKey); + sessionId = await login(loginUrl, widget.username, widget.password); + apiResponse = await fetchFromPyloadAPI(url, sessionId); + } + + if (apiResponse?.error) { + return res.status(500).send(apiResponse); + } + cache.del(sessionCacheKey); + return res.send(apiResponse); } } return res.status(400).json({ error: "Invalid proxy service type" }); -} +} \ No newline at end of file diff --git a/src/widgets/pyload/widget.js b/src/widgets/pyload/widget.js index 3d2f2958f..71073c0f1 100644 --- a/src/widgets/pyload/widget.js +++ b/src/widgets/pyload/widget.js @@ -3,6 +3,12 @@ import pyloadProxyHandler from "./proxy"; const widget = { api: "{url}/api/{endpoint}", proxyHandler: pyloadProxyHandler, -}; + + mappings: { + "status": { + endpoint: "statusServer", + } + } +} export default widget; From 69786600b1c3ca7a8f9506d4279f8a7b910cd82f Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 6 Nov 2022 11:24:59 -0800 Subject: [PATCH 5/6] Pyload widget use httpProxy instead of fetch --- src/widgets/pyload/proxy.js | 69 +++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js index d96b859c1..d130bcfe3 100644 --- a/src/widgets/pyload/proxy.js +++ b/src/widgets/pyload/proxy.js @@ -4,6 +4,7 @@ import getServiceWidget from "utils/config/service-helpers"; import { formatApiCall } from "utils/proxy/api-helpers"; import widgets from "widgets/widgets"; import createLogger from "utils/logger"; +import { httpProxy } from "utils/proxy/http"; const proxyName = 'pyloadProxyHandler'; const logger = createLogger(proxyName); @@ -20,50 +21,60 @@ async function fetchFromPyloadAPI(url, sessionId, params) { if (params) { options.body = Object.keys(params).map(k => `${k}=${params[k]}`).join('&'); } else { - options.body = `session=${sessionId}` + options.body = `session=${sessionId}`; } - return fetch(url, options).then((response) => response.json()); + // eslint-disable-next-line no-unused-vars + const [status, contentType, data] = await httpProxy(url, options); + return [status, JSON.parse(Buffer.from(data).toString())]; } async function login(loginUrl, username, password) { - const sessionId = await fetchFromPyloadAPI(loginUrl, null, { username, password }) - cache.put(sessionCacheKey, sessionId); - return sessionId; + const [status, sessionId] = await fetchFromPyloadAPI(loginUrl, null, { username, password }); + if (status !== 200) { + throw new Error(`HTTP error ${status} logging into Pyload API, returned: ${sessionId}`); + } else { + cache.put(sessionCacheKey, sessionId); + return sessionId; + } } export default async function pyloadProxyHandler(req, res) { const { group, service, endpoint } = req.query; - if (group && service) { - const widget = await getServiceWidget(group, service); - - if (widget) { - const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); - const loginUrl = `${widget.url}/api/login`; - - let sessionId = cache.get(sessionCacheKey); + try { + if (group && service) { + const widget = await getServiceWidget(group, service); + + if (widget) { + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + const loginUrl = `${widget.url}/api/login`; + + let sessionId = cache.get(sessionCacheKey); - if (!sessionId) { - sessionId = await login(loginUrl, widget.username, widget.password); - } + if (!sessionId) { + sessionId = await login(loginUrl, widget.username, widget.password); + } + + let [status, data] = await fetchFromPyloadAPI(url, sessionId); - let apiResponse = await fetchFromPyloadAPI(url, sessionId); + if (status === 403) { + logger.debug("Failed to retrieve data from Pyload API, login and re-try"); + cache.del(sessionCacheKey); + sessionId = await login(loginUrl, widget.username, widget.password); + [status, data] = await fetchFromPyloadAPI(url, sessionId); + } + + if (data?.error || status !== 200) { + return res.status(500).send(Buffer.from(data).toString()); + } - if (apiResponse?.error === 'Forbidden') { - logger.debug("Failed to retrieve data from Pyload API, login and re-try"); - cache.del(sessionCacheKey); - sessionId = await login(loginUrl, widget.username, widget.password); - apiResponse = await fetchFromPyloadAPI(url, sessionId); - } - - if (apiResponse?.error) { - return res.status(500).send(apiResponse); + return res.json(data); } - cache.del(sessionCacheKey); - - return res.send(apiResponse); } + } catch (e) { + logger.error(e); + return res.status(500).send(e.toString()); } return res.status(400).json({ error: "Invalid proxy service type" }); From 1fd9686e411b5365e5d252b0ea976fb43cb8d66f Mon Sep 17 00:00:00 2001 From: stuffinator Date: Tue, 8 Nov 2022 21:47:12 +0100 Subject: [PATCH 6/6] - fixed empty password not working - Airbnb style guide --- src/widgets/pyload/proxy.js | 46 ++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js index d130bcfe3..86989ad05 100644 --- a/src/widgets/pyload/proxy.js +++ b/src/widgets/pyload/proxy.js @@ -1,10 +1,10 @@ -import cache from "memory-cache"; +import cache from 'memory-cache'; -import getServiceWidget from "utils/config/service-helpers"; -import { formatApiCall } from "utils/proxy/api-helpers"; -import widgets from "widgets/widgets"; -import createLogger from "utils/logger"; -import { httpProxy } from "utils/proxy/http"; +import getServiceWidget from 'utils/config/service-helpers'; +import { formatApiCall } from 'utils/proxy/api-helpers'; +import widgets from 'widgets/widgets'; +import createLogger from 'utils/logger'; +import { httpProxy } from 'utils/proxy/http'; const proxyName = 'pyloadProxyHandler'; const logger = createLogger(proxyName); @@ -12,24 +12,23 @@ const sessionCacheKey = `${proxyName}__sessionId`; async function fetchFromPyloadAPI(url, sessionId, params) { const options = { - method: "POST", + body: params + ? Object.keys(params) + .map((prop) => `${prop}=${params[prop]}`) + .join('&') + : `session=${sessionId}`, + method: 'POST', headers: { - "Content-Type": "application/x-www-form-urlencoded", + 'Content-Type': 'application/x-www-form-urlencoded', }, }; - if (params) { - options.body = Object.keys(params).map(k => `${k}=${params[k]}`).join('&'); - } else { - options.body = `session=${sessionId}`; - } - // eslint-disable-next-line no-unused-vars const [status, contentType, data] = await httpProxy(url, options); return [status, JSON.parse(Buffer.from(data).toString())]; } -async function login(loginUrl, username, password) { +async function login(loginUrl, username, password = '') { const [status, sessionId] = await fetchFromPyloadAPI(loginUrl, null, { username, password }); if (status !== 200) { throw new Error(`HTTP error ${status} logging into Pyload API, returned: ${sessionId}`); @@ -45,26 +44,21 @@ export default async function pyloadProxyHandler(req, res) { try { if (group && service) { const widget = await getServiceWidget(group, service); - + if (widget) { const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); const loginUrl = `${widget.url}/api/login`; - - let sessionId = cache.get(sessionCacheKey); - if (!sessionId) { - sessionId = await login(loginUrl, widget.username, widget.password); - } - + let sessionId = cache.get(sessionCacheKey) ?? await login(loginUrl, widget.username, widget.password); let [status, data] = await fetchFromPyloadAPI(url, sessionId); if (status === 403) { - logger.debug("Failed to retrieve data from Pyload API, login and re-try"); + logger.debug('Failed to retrieve data from Pyload API, login and re-try'); cache.del(sessionCacheKey); sessionId = await login(loginUrl, widget.username, widget.password); [status, data] = await fetchFromPyloadAPI(url, sessionId); } - + if (data?.error || status !== 200) { return res.status(500).send(Buffer.from(data).toString()); } @@ -77,5 +71,5 @@ export default async function pyloadProxyHandler(req, res) { return res.status(500).send(e.toString()); } - return res.status(400).json({ error: "Invalid proxy service type" }); -} \ No newline at end of file + return res.status(400).json({ error: 'Invalid proxy service type' }); +}