Add AdGuard, Bazarr, and Coin Market Cap widgets
- Allow setting HTTP method in widget.js - Allow sending allow listed query params to proxy
This commit is contained in:
parent
f999f4a467
commit
03fa2f86d7
10 changed files with 251 additions and 9 deletions
|
@ -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);
|
||||
|
|
|
@ -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()}`;
|
||||
}
|
||||
|
||||
|
|
44
src/widgets/adguard/component.jsx
Normal file
44
src/widgets/adguard/component.jsx
Normal file
|
@ -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 <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!adguardData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("adguard.queries")} />
|
||||
<Block label={t("adguard.blocked")} />
|
||||
<Block label={t("adguard.filtered")} />
|
||||
<Block label={t("adguard.latency")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const filtered =
|
||||
adguardData.num_replaced_safebrowsing + adguardData.num_replaced_safesearch + adguardData.num_replaced_parental;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("adguard.queries")} value={t("common.number", { value: adguardData.num_dns_queries })} />
|
||||
<Block label={t("adguard.blocked")} value={t("common.number", { value: adguardData.num_blocked_filtering })} />
|
||||
<Block label={t("adguard.filtered")} value={t("common.number", { value: filtered })} />
|
||||
<Block
|
||||
label={t("adguard.latency")}
|
||||
value={t("common.ms", { value: adguardData.avg_processing_time * 1000, style: "unit", unit: "millisecond" })}
|
||||
/>
|
||||
</Widget>
|
||||
);
|
||||
}
|
14
src/widgets/adguard/widget.js
Normal file
14
src/widgets/adguard/widget.js
Normal file
|
@ -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;
|
35
src/widgets/bazarr/component.jsx
Normal file
35
src/widgets/bazarr/component.jsx
Normal file
|
@ -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 <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!episodesData || !moviesData) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("bazarr.missingEpisodes")} />
|
||||
<Block label={t("bazarr.missingMovies")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Block label={t("bazarr.missingEpisodes")} value={t("common.number", { value: episodesData.total })} />
|
||||
<Block label={t("bazarr.missingMovies")} value={t("common.number", { value: moviesData.total })} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
24
src/widgets/bazarr/widget.js
Normal file
24
src/widgets/bazarr/widget.js
Normal file
|
@ -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;
|
92
src/widgets/coinmarketcap/component.jsx
Normal file
92
src/widgets/coinmarketcap/component.jsx
Normal file
|
@ -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 (
|
||||
<Widget>
|
||||
<Block value={t("coinmarketcap.configure")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
if (statsError) {
|
||||
return <Widget error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!statsData || !dateRange) {
|
||||
return (
|
||||
<Widget>
|
||||
<Block value={t("coinmarketcap.configure")} />
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
const { data } = statsData;
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<div className={classNames(service.description ? "-top-10" : "-top-8", "absolute right-1")}>
|
||||
<Dropdown options={dateRangeOptions} value={dateRange} setValue={setDateRange} />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col w-full">
|
||||
{symbols.map((symbol) => (
|
||||
<div
|
||||
key={data[symbol].symbol}
|
||||
className="bg-theme-200/50 dark:bg-theme-900/20 rounded m-1 flex-1 flex flex-row items-center justify-between p-1 text-xs"
|
||||
>
|
||||
<div className="font-thin pl-2">{data[symbol].name}</div>
|
||||
<div className="flex flex-row text-right">
|
||||
<div className="font-bold mr-2">
|
||||
{t("common.number", {
|
||||
value: data[symbol].quote[currencyCode].price,
|
||||
style: "currency",
|
||||
currency: currencyCode,
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={`font-bold w-10 mr-2 ${
|
||||
data[symbol].quote[currencyCode][`percent_change_${dateRange}`] > 0
|
||||
? "text-emerald-300"
|
||||
: "text-rose-300"
|
||||
}`}
|
||||
>
|
||||
{data[symbol].quote[currencyCode][`percent_change_${dateRange}`].toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Widget>
|
||||
);
|
||||
}
|
15
src/widgets/coinmarketcap/widget.js
Normal file
15
src/widgets/coinmarketcap/widget.js
Normal file
|
@ -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;
|
|
@ -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")),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue