Add AdGuard, Bazarr, and Coin Market Cap widgets

- Allow setting HTTP method in widget.js
- Allow sending allow listed query params to proxy
pull/302/head
Jason Fischer 2 years ago
parent f999f4a467
commit 03fa2f86d7
No known key found for this signature in database

@ -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()}`;
}

@ -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>
);
}

@ -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;

@ -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>
);
}

@ -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;

@ -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>
);
}

@ -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…
Cancel
Save