diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 2cf3f1ba..2d86809f 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -661,6 +661,11 @@ "monitoring": "Monitoring", "updates": "Updates" }, + "calibreweb": { + "books": "Books", + "authors": "Authors", + "series": "Series" + }, "jdownloader": { "downloadCount": "Queue", "downloadBytesRemaining": "Remaining", diff --git a/src/widgets/calibreweb/component.jsx b/src/widgets/calibreweb/component.jsx new file mode 100644 index 00000000..450297af --- /dev/null +++ b/src/widgets/calibreweb/component.jsx @@ -0,0 +1,37 @@ +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: booksData, error: booksError } = useWidgetAPI(widget, "books"); + const { data: authorsData, error: authorsError } = useWidgetAPI(widget, "authors"); + const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series"); + + if (booksError || authorsError || seriesError) { + const finalError = booksError ?? authorsError ?? seriesError; + return ; + } + + if (!booksData || !authorsData || !seriesData) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/calibreweb/proxy.js b/src/widgets/calibreweb/proxy.js new file mode 100644 index 00000000..4328e43c --- /dev/null +++ b/src/widgets/calibreweb/proxy.js @@ -0,0 +1,73 @@ +import { xml2json } from "xml-js"; + +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 = "calibreWebProxyHandler"; +const logger = createLogger(proxyName); + +async function getWidget(req) { + const { group, service } = req.query; + + if (!group || !service) { + logger.debug("Invalid or missing service '%s' or group '%s'", service, group); + return null; + } + + const widget = await getServiceWidget(group, service); + + if (!widget) { + logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); + return null; + } + + return widget; +} + +async function apiCall(widget, endpoint) { + const { api } = widgets[widget.type]; + const apiUrl = new URL(formatApiCall(api, { endpoint, ...widget })); + const headers = { + Authorization: `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}` + }; + + const [status, contentType, data] = await httpProxy(apiUrl, { + withCredentials: true, + credentials: "include", + headers, + }); + + if (status !== 200) { + logger.error("Error getting data from CalibreWeb: %s status %d. Data: %s", apiUrl, status, data); + return { status, contentType, data: null }; + } + + try { + const dataDecoded = xml2json(data.toString(), { compact: true }); + return {status, data: JSON.parse(dataDecoded), contentType}; + } catch (e) { + logger.error("Error decoding CalibreWeb API data. Data: %s", data.toString()); + return {status, data: null, contentType}; + } +} + +export default async function calibreWebProxyHandler(req, res) { + const widget = await getWidget(req); + + const { endpoint } = req.query; + + if (!widget) { + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const { status, data } = await apiCall(widget, endpoint); + + if (status !== 200) { + return res.status(status).json({error: {message: "HTTP error communicating with CalibreWeb API", data: Buffer.from(data).toString()}}); + } + + return res.status(status).json(data); +} diff --git a/src/widgets/calibreweb/widget.js b/src/widgets/calibreweb/widget.js new file mode 100644 index 00000000..ea898dd1 --- /dev/null +++ b/src/widgets/calibreweb/widget.js @@ -0,0 +1,20 @@ +import calibreWebProxyHandler from "./proxy"; + +const widget = { + api: "{url}/{endpoint}", + proxyHandler: calibreWebProxyHandler, + + mappings: { + books: { + endpoint: "opds/books/letter/00", + }, + authors: { + endpoint: "opds/author/letter/00", + }, + series: { + endpoint: "opds/series/letter/00", + }, + }, +}; + +export default widget; diff --git a/src/widgets/components.js b/src/widgets/components.js index f3242ce4..5c9155c3 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -8,6 +8,7 @@ const components = { azuredevops: dynamic(() => import("./azuredevops/component")), bazarr: dynamic(() => import("./bazarr/component")), caddy: dynamic(() => import("./caddy/component")), + calibreweb: dynamic(() => import("./calibreweb/component")), changedetectionio: dynamic(() => import("./changedetectionio/component")), channelsdvrserver: dynamic(() => import("./channelsdvrserver/component")), cloudflared: dynamic(() => import("./cloudflared/component")), diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 1b7d9f1b..3af06123 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -5,6 +5,7 @@ import autobrr from "./autobrr/widget"; import azuredevops from "./azuredevops/widget"; import bazarr from "./bazarr/widget"; import caddy from "./caddy/widget"; +import calibreweb from "./calibreweb/widget"; import changedetectionio from "./changedetectionio/widget"; import channelsdvrserver from "./channelsdvrserver/widget"; import cloudflared from "./cloudflared/widget"; @@ -101,6 +102,7 @@ const widgets = { azuredevops, bazarr, caddy, + calibreweb, changedetectionio, channelsdvrserver, cloudflared,