mirror of
https://github.com/karl0ss/homepage.git
synced 2025-04-29 12:03:41 +01:00
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
@ -1,3 +1,5 @@
|
|||||||
|
import { URLSearchParams } from "next/dist/compiled/@edge-runtime/primitives/url";
|
||||||
|
|
||||||
import createLogger from "utils/logger";
|
import createLogger from "utils/logger";
|
||||||
import genericProxyHandler from "utils/proxies/generic";
|
import genericProxyHandler from "utils/proxies/generic";
|
||||||
import widgets from "widgets/widgets";
|
import widgets from "widgets/widgets";
|
||||||
@ -15,20 +17,30 @@ export default async function handler(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const serviceProxyHandler = widget.proxyHandler || genericProxyHandler;
|
const serviceProxyHandler = widget.proxyHandler || genericProxyHandler;
|
||||||
|
req.method = "GET";
|
||||||
|
|
||||||
if (serviceProxyHandler instanceof Function) {
|
if (serviceProxyHandler instanceof Function) {
|
||||||
// map opaque endpoints to their actual endpoint
|
// map opaque endpoints to their actual endpoint
|
||||||
const mapping = widget?.mappings?.[req.query.endpoint];
|
const mapping = widget?.mappings?.[req.query.endpoint];
|
||||||
|
const mappingParams = mapping.params;
|
||||||
const map = mapping?.map;
|
const map = mapping?.map;
|
||||||
const endpoint = mapping?.endpoint;
|
const endpoint = mapping?.endpoint;
|
||||||
const endpointProxy = mapping?.proxyHandler;
|
const endpointProxy = mapping?.proxyHandler || serviceProxyHandler;
|
||||||
|
req.method = mapping?.method || "GET";
|
||||||
|
|
||||||
if (!endpoint) {
|
if (!endpoint) {
|
||||||
logger.debug("Unsupported service endpoint: %s", type);
|
logger.debug("Unsupported service endpoint: %s", type);
|
||||||
return res.status(403).json({ error: "Unsupported service endpoint" });
|
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) {
|
if (endpointProxy instanceof Function) {
|
||||||
return endpointProxy(req, res, map);
|
return endpointProxy(req, res, map);
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
// emby: `{url}/emby/{endpoint}?api_key={key}`,
|
// emby: `{url}/emby/{endpoint}?api_key={key}`,
|
||||||
// jellyfin: `{url}/emby/{endpoint}?api_key={key}`,
|
// jellyfin: `{url}/emby/{endpoint}?api_key={key}`,
|
||||||
// pihole: `{url}/admin/{endpoint}`,
|
// pihole: `{url}/admin/{endpoint}`,
|
||||||
// radarr: `{url}/api/v3/{endpoint}?apikey={key}`,
|
|
||||||
// sonarr: `{url}/api/v3/{endpoint}?apikey={key}`,
|
|
||||||
// speedtest: `{url}/api/{endpoint}`,
|
// speedtest: `{url}/api/{endpoint}`,
|
||||||
// tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`,
|
// tautulli: `{url}/api/v2?apikey={key}&cmd={endpoint}`,
|
||||||
// traefik: `{url}/api/{endpoint}`,
|
// traefik: `{url}/api/{endpoint}`,
|
||||||
@ -12,18 +10,14 @@
|
|||||||
// transmission: `{url}/transmission/rpc`,
|
// transmission: `{url}/transmission/rpc`,
|
||||||
// qbittorrent: `{url}/api/v2/{endpoint}`,
|
// qbittorrent: `{url}/api/v2/{endpoint}`,
|
||||||
// jellyseerr: `{url}/api/v1/{endpoint}`,
|
// jellyseerr: `{url}/api/v1/{endpoint}`,
|
||||||
// overseerr: `{url}/api/v1/{endpoint}`,
|
|
||||||
// ombi: `{url}/api/v1/{endpoint}`,
|
// ombi: `{url}/api/v1/{endpoint}`,
|
||||||
// npm: `{url}/api/{endpoint}`,
|
// npm: `{url}/api/{endpoint}`,
|
||||||
// lidarr: `{url}/api/v1/{endpoint}?apikey={key}`,
|
// lidarr: `{url}/api/v1/{endpoint}?apikey={key}`,
|
||||||
// readarr: `{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}`,
|
// sabnzbd: `{url}/api/?apikey={key}&output=json&mode={endpoint}`,
|
||||||
// coinmarketcap: `https://pro-api.coinmarketcap.com/{endpoint}`,
|
|
||||||
// gotify: `{url}/{endpoint}`,
|
// gotify: `{url}/{endpoint}`,
|
||||||
// prowlarr: `{url}/api/v1/{endpoint}`,
|
// prowlarr: `{url}/api/v1/{endpoint}`,
|
||||||
// jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`,
|
// jackett: `{url}/api/v2.0/{endpoint}?apikey={key}&configured=true`,
|
||||||
// adguard: `{url}/control/{endpoint}`,
|
|
||||||
// strelaysrv: `{url}/{endpoint}`,
|
// strelaysrv: `{url}/{endpoint}`,
|
||||||
// mastodon: `{url}/api/v1/{endpoint}`,
|
// mastodon: `{url}/api/v1/{endpoint}`,
|
||||||
// };
|
// };
|
||||||
@ -38,13 +32,16 @@ export function formatApiCall(url, args) {
|
|||||||
return url.replace(find, replace);
|
return url.replace(find, replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatProxyUrl(widget, endpoint) {
|
export function formatProxyUrl(widget, endpoint, endpointParams) {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
type: widget.type,
|
type: widget.type,
|
||||||
group: widget.service_group,
|
group: widget.service_group,
|
||||||
service: widget.service_name,
|
service: widget.service_name,
|
||||||
endpoint,
|
endpoint,
|
||||||
});
|
});
|
||||||
|
if (endpointParams) {
|
||||||
|
params.append("params", JSON.stringify(endpointParams));
|
||||||
|
}
|
||||||
return `/api/services/proxy?${params.toString()}`;
|
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";
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
|
adguard: dynamic(() => import("./adguard/component")),
|
||||||
|
bazarr: dynamic(() => import("./bazarr/component")),
|
||||||
|
coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
|
||||||
overseerr: dynamic(() => import("./overseerr/component")),
|
overseerr: dynamic(() => import("./overseerr/component")),
|
||||||
radarr: dynamic(() => import("./radarr/component")),
|
radarr: dynamic(() => import("./radarr/component")),
|
||||||
sonarr: dynamic(() => import("./sonarr/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 overseerr from "./overseerr/widget";
|
||||||
import radarr from "./radarr/widget";
|
import radarr from "./radarr/widget";
|
||||||
import sonarr from "./sonarr/widget"
|
import sonarr from "./sonarr/widget"
|
||||||
|
|
||||||
const widgets = {
|
const widgets = {
|
||||||
|
adguard,
|
||||||
|
bazarr,
|
||||||
|
coinmarketcap,
|
||||||
overseerr,
|
overseerr,
|
||||||
radarr,
|
radarr,
|
||||||
sonarr,
|
sonarr,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user