diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 51259046..f9f76b16 100755 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -614,5 +614,11 @@ "whatsupdocker": { "monitoring": "Monitoring", "updates": "Updates" + }, + "shoko": { + "series": "TV Series", + "movies": "Movies", + "ovas": "OVAs", + "others": "Others" } } diff --git a/src/widgets/components.js b/src/widgets/components.js index 7aeb8b45..c28d0569 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -69,6 +69,7 @@ const components = { rutorrent: dynamic(() => import("./rutorrent/component")), sabnzbd: dynamic(() => import("./sabnzbd/component")), scrutiny: dynamic(() => import("./scrutiny/component")), + shoko: dynamic(() => import("./shoko/component")), sonarr: dynamic(() => import("./sonarr/component")), speedtest: dynamic(() => import("./speedtest/component")), strelaysrv: dynamic(() => import("./strelaysrv/component")), diff --git a/src/widgets/shoko/component.jsx b/src/widgets/shoko/component.jsx new file mode 100644 index 00000000..dd65c483 --- /dev/null +++ b/src/widgets/shoko/component.jsx @@ -0,0 +1,42 @@ +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: shokoData, error: shokoError } = useWidgetAPI(widget); + + if (shokoError) { + return ; + } + + if (!shokoData) { + return ( + + + + + + + ); + } + + const { Series: series, Movie: movies, OVA: ovas, ...others } = shokoData; + + return ( + + + + + total + item) })} + /> + + ); +} diff --git a/src/widgets/shoko/proxy.js b/src/widgets/shoko/proxy.js new file mode 100644 index 00000000..ae700e49 --- /dev/null +++ b/src/widgets/shoko/proxy.js @@ -0,0 +1,78 @@ +import cache from "memory-cache"; + +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"; +import widgets from "widgets/widgets"; + +const proxyName = "shokoProxyHandler"; +const sessionTokenCacheKey = `${proxyName}__sessionToken`; +const logger = createLogger(proxyName); + +async function login(widget, service) { + const endpoint = "auth"; + const api = widgets?.[widget.type]?.api; + const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); + const loginParams = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + user: widget.username, + pass: widget.password, + device: "web-ui", + }), + }; + + // eslint-disable-next-line no-unused-vars + const [status, contentType, data] = await httpProxy(loginUrl, loginParams); + + try { + const { apikey } = JSON.parse(data.toString()); + cache.put(`${sessionTokenCacheKey}.${service}`, apikey); + return { apikey }; + } catch (e) { + logger.error("Unable to login to Shoko API: %s", e); + } + + return { apikey: false }; +} + +export default async function shokoProxyHandler(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" }); + } + + if (!cache.get(`${sessionTokenCacheKey}.${service}`)) { + await login(widget, service); + } + + const endpoint = "v3/Dashboard/SeriesSummary"; + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + const params = { method: "GET", headers: { apikey: cache.get(`${sessionTokenCacheKey}.${service}`) } }; + + let [status, contentType, data] = await httpProxy(url, params); + + if (status === 401) { + logger.debug("Shoko API rejected the request, attempting to obtain new session token"); + const { apikey } = await login(widget, service); + params.headers.apikey = apikey; + + [status, contentType, data] = await httpProxy(url, params); + } + + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(data); +} diff --git a/src/widgets/shoko/widget.js b/src/widgets/shoko/widget.js new file mode 100644 index 00000000..67b9e154 --- /dev/null +++ b/src/widgets/shoko/widget.js @@ -0,0 +1,8 @@ +import shokoProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: shokoProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 85fb62f8..f50b5d08 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -63,6 +63,7 @@ import readarr from "./readarr/widget"; import rutorrent from "./rutorrent/widget"; import sabnzbd from "./sabnzbd/widget"; import scrutiny from "./scrutiny/widget"; +import shoko from "./shoko/widget"; import sonarr from "./sonarr/widget"; import speedtest from "./speedtest/widget"; import strelaysrv from "./strelaysrv/widget"; @@ -147,6 +148,7 @@ const widgets = { rutorrent, sabnzbd, scrutiny, + shoko, sonarr, speedtest, strelaysrv,