Merge branch 'benphelps:main' into kubernetes

This commit is contained in:
James Wynn 2022-11-18 18:02:53 -06:00 committed by GitHub
commit 1ca61114ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1855 additions and 436 deletions

View file

@ -10,6 +10,7 @@ const components = {
docker: dynamic(() => import("./docker/component")),
kubernetes: dynamic(() => import("./kubernetes/component")),
emby: dynamic(() => import("./emby/component")),
gluetun: dynamic(() => import("./gluetun/component")),
gotify: dynamic(() => import("./gotify/component")),
homebridge: dynamic(() => import("./homebridge/component")),
jackett: dynamic(() => import("./jackett/component")),
@ -17,6 +18,7 @@ const components = {
jellyseerr: dynamic(() => import("./jellyseerr/component")),
lidarr: dynamic(() => import("./lidarr/component")),
mastodon: dynamic(() => import("./mastodon/component")),
navidrome: dynamic(() => import("./navidrome/component")),
npm: dynamic(() => import("./npm/component")),
nzbget: dynamic(() => import("./nzbget/component")),
ombi: dynamic(() => import("./ombi/component")),
@ -26,6 +28,7 @@ const components = {
portainer: dynamic(() => import("./portainer/component")),
prowlarr: dynamic(() => import("./prowlarr/component")),
proxmox: dynamic(() => import("./proxmox/component")),
pyload: dynamic(() => import("./pyload/component")),
qbittorrent: dynamic(() => import("./qbittorrent/component")),
radarr: dynamic(() => import("./radarr/component")),
readarr: dynamic(() => import("./readarr/component")),

View file

@ -0,0 +1,35 @@
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: gluetunData, error: gluetunError } = useWidgetAPI(widget, "ip");
if (gluetunError) {
return <Container error={t("widget.api_error")} />;
}
if (!gluetunData) {
return (
<Container service={service}>
<Block label="gluetun.public_ip" />
<Block label="gluetun.region" />
<Block label="gluetun.country" />
</Container>
);
}
return (
<Container service={service}>
<Block label="gluetun.public_ip" value={gluetunData.public_ip} />
<Block label="gluetun.region" value={gluetunData.region} />
<Block label="gluetun.country" value={gluetunData.country} />
</Container>
);
}

View file

@ -0,0 +1,14 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/v1/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
ip: {
endpoint: "publicip/ip",
},
},
};
export default widget;

View file

@ -0,0 +1,56 @@
import { useTranslation } from "next-i18next";
import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function SinglePlayingEntry({ entry }) {
const { username, artist, title, album } = entry;
let fullTitle = title;
if (artist) fullTitle = `${artist} - ${title}`;
if (album) fullTitle += `${album}`;
if (username) fullTitle += ` (${username})`;
return (
<div className="text-theme-700 dark:text-theme-200 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1 flex">
<div className="text-xs z-10 self-center ml-2 relative w-full h-4 grow mr-2">
<div className="absolute w-full whitespace-nowrap text-ellipsis overflow-hidden">{fullTitle}</div>
</div>
</div>
);
}
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: navidromeData, error: navidromeError } = useWidgetAPI(widget, "getNowPlaying");
if (navidromeError || navidromeData?.error || navidromeData?.["subsonic-response"]?.error) {
return <Container error={t("widget.api_error")} />;
}
if (!navidromeData) {
return (
<SinglePlayingEntry entry={{ title: t("navidrome.please_wait") }} />
);
}
const { nowPlaying } = navidromeData["subsonic-response"];
if (!nowPlaying.entry) {
// nothing playing
return (
<SinglePlayingEntry entry={{ title: t("navidrome.nothing_streaming") }} />
);
}
const nowPlayingEntries = Object.values(nowPlaying.entry);
return (
<div className="flex flex-col pb-1 mx-1">
{nowPlayingEntries.map((entry) => (
<SinglePlayingEntry key={entry.id} entry={entry} />
))}
</div>
);
}

View file

@ -0,0 +1,14 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/rest/{endpoint}?u={user}&t={token}&s={salt}&v=1.16.1&c=homepage&f=json",
proxyHandler: genericProxyHandler,
mappings: {
"getNowPlaying": {
endpoint: "getNowPlaying",
},
},
};
export default widget;

View file

@ -11,7 +11,7 @@ export default function Component({ service }) {
const { data: infoData, error: infoError } = useWidgetAPI(widget, "nginx/proxy-hosts");
if (infoError) {
if (infoError || infoData?.error) {
return <Container error={t("widget.api_error")} />;
}

View file

@ -1,6 +1,33 @@
import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
import createLogger from "utils/logger";
const proxyName = "npmProxyHandler";
const tokenCacheKey = `${proxyName}__token`;
const logger = createLogger(proxyName);
async function login(loginUrl, username, password) {
const authResponse = await httpProxy(loginUrl, {
method: "POST",
body: JSON.stringify({ identity: username, secret: password }),
headers: {
"Content-Type": "application/json",
},
});
const status = authResponse[0];
const data = JSON.parse(Buffer.from(authResponse[2]).toString());
if (status === 200) {
cache.put(tokenCacheKey, data.token);
}
return [status, data.token ?? data];
}
export default async function npmProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
@ -14,27 +41,54 @@ export default async function npmProxyHandler(req, res) {
if (widget) {
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const loginUrl = `${widget.url}/api/tokens`;
const body = { identity: widget.username, secret: widget.password };
const authResponse = await fetch(loginUrl, {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
}).then((response) => response.json());
let status;
let contentType;
let data;
let token = cache.get(tokenCacheKey);
if (!token) {
[status, token] = await login(loginUrl, widget.username, widget.password);
if (status !== 200) {
logger.debug(`HTTTP ${status} logging into npm api: ${data}`);
return res.status(status).send(data);
}
}
const apiResponse = await fetch(url, {
[status, contentType, data] = await httpProxy(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authResponse.token}`,
Authorization: `Bearer ${token}`,
},
}).then((response) => response.json());
});
return res.send(apiResponse);
if (status === 403) {
logger.debug(`HTTTP ${status} retrieving data from npm api, logging in and trying again.`);
cache.del(tokenCacheKey);
[status, token] = await login(loginUrl, widget.username, widget.password);
if (status !== 200) {
logger.debug(`HTTTP ${status} logging into npm api: ${data}`);
return res.status(status).send(data);
}
// eslint-disable-next-line no-unused-vars
[status, contentType, data] = await httpProxy(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
}
if (status !== 200) {
return res.status(status).send(data);
}
return res.send(data);
}
}

View file

@ -0,0 +1,35 @@
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: pyloadData, error: pyloadError } = useWidgetAPI(widget, "status");
if (pyloadError || pyloadData?.error) {
return <Container error={t("widget.api_error")} />;
}
if (!pyloadData) {
return (
<Container service={service}>
<Block label="pyload.speed" />
<Block label="pyload.active" />
<Block label="pyload.queue" />
<Block label="pyload.total" />
</Container>
);
}
return (
<Container service={service}>
<Block label="pyload.speed" value={t("common.bitrate", { value: pyloadData.speed })} />
<Block label="pyload.active" value={t("common.number", { value: pyloadData.active })} />
<Block label="pyload.queue" value={t("common.number", { value: pyloadData.queue })} />
<Block label="pyload.total" value={t("common.number", { value: pyloadData.total })} />
</Container>
);
}

102
src/widgets/pyload/proxy.js Normal file
View file

@ -0,0 +1,102 @@
import cache from 'memory-cache';
import getServiceWidget from 'utils/config/service-helpers';
import { formatApiCall } from 'utils/proxy/api-helpers';
import widgets from 'widgets/widgets';
import createLogger from 'utils/logger';
import { httpProxy } from 'utils/proxy/http';
const proxyName = 'pyloadProxyHandler';
const logger = createLogger(proxyName);
const sessionCacheKey = `${proxyName}__sessionId`;
const isNgCacheKey = `${proxyName}__isNg`;
async function fetchFromPyloadAPI(url, sessionId, params) {
const options = {
body: params
? Object.keys(params)
.map((prop) => `${prop}=${params[prop]}`)
.join('&')
: `session=${sessionId}`,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
// see https://github.com/benphelps/homepage/issues/517
const isNg = cache.get(isNgCacheKey);
if (isNg && !params) {
delete options.body;
options.headers.Cookie = cache.get(sessionCacheKey);
}
// eslint-disable-next-line no-unused-vars
const [status, contentType, data, responseHeaders] = await httpProxy(url, options);
let returnData;
try {
returnData = JSON.parse(Buffer.from(data).toString());
} catch(e) {
logger.error(`Error logging into pyload API: ${JSON.stringify(data)}`);
returnData = data;
}
return [status, returnData, responseHeaders];
}
async function login(loginUrl, username, password = '') {
const [status, sessionId, responseHeaders] = await fetchFromPyloadAPI(loginUrl, null, { username, password });
// this API actually returns status 200 even on login failure
if (status !== 200 || sessionId === false) {
logger.error(`HTTP ${status} logging into Pyload API, returned: ${JSON.stringify(sessionId)}`);
} else if (responseHeaders['set-cookie']?.join().includes('pyload_session')) {
// Support pyload-ng, see https://github.com/benphelps/homepage/issues/517
cache.put(isNgCacheKey, true);
const sessionCookie = responseHeaders['set-cookie'][0];
cache.put(sessionCacheKey, sessionCookie, 60 * 60 * 23 * 1000); // cache for 23h
} else {
cache.put(sessionCacheKey, sessionId);
}
return sessionId;
}
export default async function pyloadProxyHandler(req, res) {
const { group, service, endpoint } = req.query;
try {
if (group && service) {
const widget = await getServiceWidget(group, service);
if (widget) {
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
const loginUrl = `${widget.url}/api/login`;
let sessionId = cache.get(sessionCacheKey) ?? await login(loginUrl, widget.username, widget.password);
let [status, data] = await fetchFromPyloadAPI(url, sessionId);
if (status === 403 || status === 401) {
logger.info('Failed to retrieve data from Pyload API, trying to login again...');
cache.del(sessionCacheKey);
sessionId = await login(loginUrl, widget.username, widget.password);
[status, data] = await fetchFromPyloadAPI(url, sessionId);
}
if (data?.error || status !== 200) {
try {
return res.status(status).send(Buffer.from(data).toString());
} catch (e) {
return res.status(status).send(data);
}
}
return res.json(data);
}
}
} catch (e) {
logger.error(e);
return res.status(500).send(e.toString());
}
return res.status(400).json({ error: 'Invalid proxy service type' });
}

View file

@ -0,0 +1,14 @@
import pyloadProxyHandler from "./proxy";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: pyloadProxyHandler,
mappings: {
"status": {
endpoint: "statusServer",
}
}
}
export default widget;

View file

@ -5,12 +5,14 @@ import bazarr from "./bazarr/widget";
import changedetectionio from "./changedetectionio/widget";
import coinmarketcap from "./coinmarketcap/widget";
import emby from "./emby/widget";
import gluetun from "./gluetun/widget";
import gotify from "./gotify/widget";
import homebridge from "./homebridge/widget";
import jackett from "./jackett/widget";
import jellyseerr from "./jellyseerr/widget";
import lidarr from "./lidarr/widget";
import mastodon from "./mastodon/widget";
import navidrome from "./navidrome/widget";
import npm from "./npm/widget";
import nzbget from "./nzbget/widget";
import ombi from "./ombi/widget";
@ -20,6 +22,7 @@ import plex from "./plex/widget";
import portainer from "./portainer/widget";
import prowlarr from "./prowlarr/widget";
import proxmox from "./proxmox/widget";
import pyload from "./pyload/widget";
import qbittorrent from "./qbittorrent/widget";
import radarr from "./radarr/widget";
import readarr from "./readarr/widget";
@ -44,6 +47,7 @@ const widgets = {
changedetectionio,
coinmarketcap,
emby,
gluetun,
gotify,
homebridge,
jackett,
@ -51,6 +55,7 @@ const widgets = {
jellyseerr,
lidarr,
mastodon,
navidrome,
npm,
nzbget,
ombi,
@ -60,6 +65,7 @@ const widgets = {
portainer,
prowlarr,
proxmox,
pyload,
qbittorrent,
radarr,
readarr,