diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js index b73ec40b..43a9a3d0 100644 --- a/src/pages/api/services/proxy.js +++ b/src/pages/api/services/proxy.js @@ -1,3 +1,5 @@ +import { URLSearchParams } from "next/dist/compiled/@edge-runtime/primitives/url"; + import createLogger from "utils/logger"; import genericProxyHandler from "utils/proxies/generic"; import widgets from "widgets/widgets"; @@ -15,20 +17,30 @@ export default async function handler(req, res) { } const serviceProxyHandler = widget.proxyHandler || genericProxyHandler; + req.method = "GET"; if (serviceProxyHandler instanceof Function) { // map opaque endpoints to their actual endpoint const mapping = widget?.mappings?.[req.query.endpoint]; + const mappingParams = mapping.params; const map = mapping?.map; const endpoint = mapping?.endpoint; - const endpointProxy = mapping?.proxyHandler; + const endpointProxy = mapping?.proxyHandler || serviceProxyHandler; + req.method = mapping?.method || "GET"; if (!endpoint) { logger.debug("Unsupported service endpoint: %s", type); return res.status(403).json({ error: "Unsupported service endpoint" }); } - req.query.endpoint = endpoint; + if (req.query.params) { + const queryParams = JSON.parse(req.query.params); + const query = new URLSearchParams(mappingParams.map(p => [p, queryParams[p]])); + req.query.endpoint = `${endpoint}?${query}`; + } + else { + req.query.endpoint = endpoint; + } if (endpointProxy instanceof Function) { return endpointProxy(req, res, map); diff --git a/src/utils/api-helpers.js b/src/utils/api-helpers.js index c0a2314a..6865824f 100644 --- a/src/utils/api-helpers.js +++ b/src/utils/api-helpers.js @@ -2,8 +2,6 @@ // emby: `{url}/emby/{endpoint}?api_key={key}`, // jellyfin: `{url}/emby/{endpoint}?api_key={key}`, // pihole: `{url}/admin/{endpoint}`, -// radarr: `{url}/api/v3/{endpoint}?apikey={key}`, -// sonarr: `{url}/api/v3/{endpoint}?apikey={key}`, // speedtest: `{url}/api/{endpoint}`, // tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`, // traefik: `{url}/api/{endpoint}`, @@ -12,18 +10,14 @@ // transmission: `{url}/transmission/rpc`, // qbittorrent: `{url}/api/v2/{endpoint}`, // jellyseerr: `{url}/api/v1/{endpoint}`, -// overseerr: `{url}/api/v1/{endpoint}`, // ombi: `{url}/api/v1/{endpoint}`, // npm: `{url}/api/{endpoint}`, // lidarr: `{url}/api/v1/{endpoint}?apikey={key}`, // readarr: `{url}/api/v1/{endpoint}?apikey={key}`, -// bazarr: `{url}/api/{endpoint}/wanted?apikey={key}`, // sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`, -// coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`, // gotify: `{url}/{endpoint}`, // prowlarr: `{url}/api/v1/{endpoint}`, // jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`, -// adguard: `{url}/control/{endpoint}`, // strelaysrv: `{url}/{endpoint}`, // mastodon: `{url}/api/v1/{endpoint}`, // }; @@ -38,13 +32,16 @@ export function formatApiCall(url, args) { return url.replace(find, replace); } -export function formatProxyUrl(widget, endpoint) { +export function formatProxyUrl(widget, endpoint, endpointParams) { const params = new URLSearchParams({ type: widget.type, group: widget.service_group, service: widget.service_name, endpoint, }); + if (endpointParams) { + params.append("params", JSON.stringify(endpointParams)); + } return `/api/services/proxy?${params.toString()}`; } diff --git a/src/widgets/adguard/component.jsx b/src/widgets/adguard/component.jsx new file mode 100644 index 00000000..edbf14e9 --- /dev/null +++ b/src/widgets/adguard/component.jsx @@ -0,0 +1,44 @@ +import useSWR from "swr"; +import { useTranslation } from "next-i18next"; + +import Widget from "components/services/widgets/widget"; +import Block from "components/services/widgets/block"; +import { formatProxyUrl } from "utils/api-helpers"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const config = service.widget; + + const { data: adguardData, error: adguardError } = useSWR(formatProxyUrl(config, "stats")); + + if (adguardError) { + return ; + } + + if (!adguardData) { + return ( + + + + + + + ); + } + + const filtered = + adguardData.num_replaced_safebrowsing + adguardData.num_replaced_safesearch + adguardData.num_replaced_parental; + + return ( + + + + + + + ); +} diff --git a/src/widgets/adguard/widget.js b/src/widgets/adguard/widget.js new file mode 100644 index 00000000..ecbdf12a --- /dev/null +++ b/src/widgets/adguard/widget.js @@ -0,0 +1,14 @@ +import genericProxyHandler from "utils/proxies/generic"; + +const widget = { + api: "{url}/control/{endpoint}", + proxyHandler: genericProxyHandler, + + mappings: { + "stats": { + endpoint: "stats", + }, + }, +}; + +export default widget; diff --git a/src/widgets/bazarr/component.jsx b/src/widgets/bazarr/component.jsx new file mode 100644 index 00000000..c2e3b7cb --- /dev/null +++ b/src/widgets/bazarr/component.jsx @@ -0,0 +1,35 @@ +import useSWR from "swr"; +import { useTranslation } from "next-i18next"; + +import Widget from "components/services/widgets/widget"; +import Block from "components/services/widgets/block"; +import { formatProxyUrl } from "utils/api-helpers"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const config = service.widget; + + const { data: episodesData, error: episodesError } = useSWR(formatProxyUrl(config, "episodes")); + const { data: moviesData, error: moviesError } = useSWR(formatProxyUrl(config, "movies")); + + if (episodesError || moviesError) { + return ; + } + + if (!episodesData || !moviesData) { + return ( + + + + + ); + } + + return ( + + + + + ); +} diff --git a/src/widgets/bazarr/widget.js b/src/widgets/bazarr/widget.js new file mode 100644 index 00000000..9f12c18e --- /dev/null +++ b/src/widgets/bazarr/widget.js @@ -0,0 +1,24 @@ +import genericProxyHandler from "utils/proxies/generic"; +import { asJson } from "utils/api-helpers"; + +const widget = { + api: "{url}/api/{endpoint}/wanted?apikey={key}", + proxyHandler: genericProxyHandler, + + mappings: { + "movies": { + endpoint: "movies", + map: (data) => ({ + total: asJson(data).total, + }), + }, + "episodes": { + endpoint: "episodes", + map: (data) => ({ + total: asJson(data).total, + }), + }, + }, +}; + +export default widget; diff --git a/src/widgets/coinmarketcap/component.jsx b/src/widgets/coinmarketcap/component.jsx new file mode 100644 index 00000000..7cdb03af --- /dev/null +++ b/src/widgets/coinmarketcap/component.jsx @@ -0,0 +1,92 @@ +import useSWR from "swr"; +import { useState } from "react"; +import { useTranslation } from "next-i18next"; +import classNames from "classnames"; + +import Widget from "components/services/widgets/widget"; +import Block from "components/services/widgets/block"; +import Dropdown from "components/services/dropdown"; +import { formatProxyUrl } from "utils/api-helpers"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const dateRangeOptions = [ + { label: t("coinmarketcap.1hour"), value: "1h" }, + { label: t("coinmarketcap.1day"), value: "24h" }, + { label: t("coinmarketcap.7days"), value: "7d" }, + { label: t("coinmarketcap.30days"), value: "30d" }, + ]; + + const [dateRange, setDateRange] = useState(dateRangeOptions[0].value); + + const config = service.widget; + const currencyCode = config.currency ?? "USD"; + const { symbols } = config; + + const { data: statsData, error: statsError } = useSWR( + formatProxyUrl(config, "v1/cryptocurrency/quotes/latest", { + symbol: `${symbols.join(",")}`, + convert: `${currencyCode}` + }) + ); + + if (!symbols || symbols.length === 0) { + return ( + + + + ); + } + + if (statsError) { + return ; + } + + if (!statsData || !dateRange) { + return ( + + + + ); + } + + const { data } = statsData; + + return ( + +
+ +
+ +
+ {symbols.map((symbol) => ( +
+
{data[symbol].name}
+
+
+ {t("common.number", { + value: data[symbol].quote[currencyCode].price, + style: "currency", + currency: currencyCode, + })} +
+
0 + ? "text-emerald-300" + : "text-rose-300" + }`} + > + {data[symbol].quote[currencyCode][`percent_change_${dateRange}`].toFixed(2)}% +
+
+
+ ))} +
+
+ ); +} diff --git a/src/widgets/coinmarketcap/widget.js b/src/widgets/coinmarketcap/widget.js new file mode 100644 index 00000000..f493e62f --- /dev/null +++ b/src/widgets/coinmarketcap/widget.js @@ -0,0 +1,15 @@ +import credentialedProxyHandler from "utils/proxies/credentialed"; + +const widget = { + api: "https://pro-api.coinmarketcap.com/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + "v1/cryptocurrency/quotes/latest": { + endpoint: "v1/cryptocurrency/quotes/latest", + params: ["symbol", "convert"], + }, + }, +}; + +export default widget; diff --git a/src/widgets/components.js b/src/widgets/components.js index 3cef8aa4..5827a575 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -1,6 +1,9 @@ import dynamic from "next/dynamic"; const components = { + adguard: dynamic(() => import("./adguard/component")), + bazarr: dynamic(() => import("./bazarr/component")), + coinmarketcap: dynamic(() => import("./coinmarketcap/component")), overseerr: dynamic(() => import("./overseerr/component")), radarr: dynamic(() => import("./radarr/component")), sonarr: dynamic(() => import("./sonarr/component")), diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 96eb2929..03a8e4a5 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -1,8 +1,14 @@ +import adguard from "./adguard/widget"; +import bazarr from "./bazarr/widget"; +import coinmarketcap from "./coinmarketcap/widget"; import overseerr from "./overseerr/widget"; import radarr from "./radarr/widget"; import sonarr from "./sonarr/widget" const widgets = { + adguard, + bazarr, + coinmarketcap, overseerr, radarr, sonarr,