mirror of
https://github.com/DI0IK/homepage-plus.git
synced 2025-07-15 09:20:32 +00:00
Merge branch 'benphelps:main' into kubernetes
This commit is contained in:
commit
1ca61114ef
49 changed files with 1855 additions and 436 deletions
|
@ -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")),
|
||||
|
|
35
src/widgets/gluetun/component.jsx
Normal file
35
src/widgets/gluetun/component.jsx
Normal 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>
|
||||
);
|
||||
}
|
14
src/widgets/gluetun/widget.js
Normal file
14
src/widgets/gluetun/widget.js
Normal 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;
|
56
src/widgets/navidrome/component.jsx
Normal file
56
src/widgets/navidrome/component.jsx
Normal 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>
|
||||
);
|
||||
}
|
14
src/widgets/navidrome/widget.js
Normal file
14
src/widgets/navidrome/widget.js
Normal 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;
|
|
@ -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")} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
35
src/widgets/pyload/component.jsx
Normal file
35
src/widgets/pyload/component.jsx
Normal 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
102
src/widgets/pyload/proxy.js
Normal 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' });
|
||||
}
|
14
src/widgets/pyload/widget.js
Normal file
14
src/widgets/pyload/widget.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import pyloadProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/api/{endpoint}",
|
||||
proxyHandler: pyloadProxyHandler,
|
||||
|
||||
mappings: {
|
||||
"status": {
|
||||
endpoint: "statusServer",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default widget;
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue