Merge pull request #366 from shamoon/plex-service-widget
Feature: Plex service widget without Tautulli
This commit is contained in:
commit
5198b056cc
8 changed files with 185 additions and 1 deletions
|
@ -32,7 +32,8 @@
|
|||
"shvl": "^3.0.0",
|
||||
"swr": "^1.3.0",
|
||||
"tough-cookie": "^4.1.2",
|
||||
"winston": "^3.8.2"
|
||||
"winston": "^3.8.2",
|
||||
"xml-js": "^1.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
|
|
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
|
@ -40,6 +40,7 @@ specifiers:
|
|||
tough-cookie: ^4.1.2
|
||||
typescript: ^4.8.3
|
||||
winston: ^3.8.2
|
||||
xml-js: ^1.6.11
|
||||
|
||||
dependencies:
|
||||
'@headlessui/react': 1.7.2_biqbaboplfbrettd7655fr4n2y
|
||||
|
@ -65,6 +66,7 @@ dependencies:
|
|||
swr: 1.3.0_react@18.2.0
|
||||
tough-cookie: 4.1.2
|
||||
winston: 3.8.2
|
||||
xml-js: 1.6.11
|
||||
|
||||
devDependencies:
|
||||
'@tailwindcss/forms': 0.5.3_tailwindcss@3.1.8
|
||||
|
@ -2554,6 +2556,10 @@ packages:
|
|||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
|
||||
/sax/1.2.4:
|
||||
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
|
||||
dev: false
|
||||
|
||||
/scheduler/0.23.0:
|
||||
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
||||
dependencies:
|
||||
|
@ -3000,6 +3006,13 @@ packages:
|
|||
/wrappy/1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
/xml-js/1.6.11:
|
||||
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
sax: 1.2.4
|
||||
dev: false
|
||||
|
||||
/xtend/4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
|
|
@ -66,6 +66,11 @@
|
|||
"remaining": "Remaining",
|
||||
"downloaded": "Downloaded"
|
||||
},
|
||||
"plex": {
|
||||
"streams": "Active Streams",
|
||||
"movies": "Movies",
|
||||
"tv": "TV Shows"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"rate": "Rate",
|
||||
"queue": "Queue",
|
||||
|
|
|
@ -18,6 +18,7 @@ const components = {
|
|||
ombi: dynamic(() => import("./ombi/component")),
|
||||
overseerr: dynamic(() => import("./overseerr/component")),
|
||||
pihole: dynamic(() => import("./pihole/component")),
|
||||
plex: dynamic(() => import("./plex/component")),
|
||||
portainer: dynamic(() => import("./portainer/component")),
|
||||
prowlarr: dynamic(() => import("./prowlarr/component")),
|
||||
proxmox: dynamic(() => import("./proxmox/component")),
|
||||
|
|
38
src/widgets/plex/component.jsx
Normal file
38
src/widgets/plex/component.jsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import useSWR from "swr";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
import { formatProxyUrl } from "utils/proxy/api-helpers";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
|
||||
const { data: plexData, error: plexAPIError } = useSWR(formatProxyUrl(widget, "unified"), {
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
if (plexAPIError) {
|
||||
return <Container error={t("widget.api_error")} />;
|
||||
}
|
||||
|
||||
if (!plexData) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="plex.streams" />
|
||||
<Block label="plex.movies" />
|
||||
<Block label="plex.tv" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="plex.streams" value={t("common.number", { value: plexData.streams })} />
|
||||
<Block label="plex.movies" value={t("common.number", { value: plexData.movies })} />
|
||||
<Block label="plex.tv" value={t("common.number", { value: plexData.tv })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
110
src/widgets/plex/proxy.js
Normal file
110
src/widgets/plex/proxy.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
/* eslint-disable no-underscore-dangle */
|
||||
import cache from "memory-cache";
|
||||
import { xml2json } from "xml-js";
|
||||
|
||||
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||
import { httpProxy } from "utils/proxy/http";
|
||||
import getServiceWidget from "utils/config/service-helpers";
|
||||
import createLogger from "utils/logger";
|
||||
import widgets from "widgets/widgets";
|
||||
|
||||
const proxyName = "plexProxyHandler";
|
||||
const librariesCacheKey = `${proxyName}__libraries`;
|
||||
const moviesCacheKey = `${proxyName}__movies`;
|
||||
const tvCacheKey = `${proxyName}__tv`;
|
||||
const logger = createLogger(proxyName);
|
||||
|
||||
async function getWidget(req) {
|
||||
const { group, service } = req.query;
|
||||
|
||||
if (!group || !service) {
|
||||
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
|
||||
const widget = await getServiceWidget(group, service);
|
||||
|
||||
if (!widget) {
|
||||
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||
return null;
|
||||
}
|
||||
|
||||
return widget;
|
||||
}
|
||||
|
||||
async function fetchFromPlexAPI(endpoint, widget) {
|
||||
const api = widgets?.[widget.type]?.api;
|
||||
if (!api) {
|
||||
return [403, null];
|
||||
}
|
||||
|
||||
const url = new URL(formatApiCall(api, { endpoint, ...widget }));
|
||||
|
||||
const [status, contentType, data] = await httpProxy(url);
|
||||
|
||||
if (status !== 200) {
|
||||
logger.error("HTTP %d communicating with Plex. Data: %s", status, data.toString());
|
||||
return [status, data.toString()];
|
||||
}
|
||||
|
||||
try {
|
||||
const dataDecoded = xml2json(data.toString(), { compact: true });
|
||||
return [status, JSON.parse(dataDecoded), contentType];
|
||||
} catch (e) {
|
||||
logger.error("Error decoding Plex API data. Data: %s", data.toString());
|
||||
return [status, null];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function plexProxyHandler(req, res) {
|
||||
const widget = await getWidget(req);
|
||||
if (!widget) {
|
||||
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||
}
|
||||
|
||||
logger.debug("Getting streams from Plex API");
|
||||
let streams;
|
||||
let [status, apiData] = await fetchFromPlexAPI("/status/sessions", widget);
|
||||
if (apiData && apiData.MediaContainer) {
|
||||
streams = apiData.MediaContainer._attributes.size;
|
||||
}
|
||||
|
||||
let libraries = cache.get(librariesCacheKey);
|
||||
if (libraries === null) {
|
||||
logger.debug("Getting libraries from Plex API");
|
||||
[status, apiData] = await fetchFromPlexAPI("/library/sections", widget);
|
||||
if (apiData && apiData.MediaContainer) {
|
||||
libraries = apiData.MediaContainer.Directory;
|
||||
cache.put(librariesCacheKey, libraries, 1000 * 60 * 60 * 6);
|
||||
}
|
||||
}
|
||||
|
||||
let movies = cache.get(moviesCacheKey);
|
||||
let tv = cache.get(tvCacheKey);
|
||||
if (movies === null || tv === null) {
|
||||
movies = 0;
|
||||
tv = 0;
|
||||
logger.debug("Getting movie + tv counts from Plex API");
|
||||
libraries.filter(l => ["movie", "show"].includes(l._attributes.type)).forEach(async (library) => {
|
||||
[status, apiData] = await fetchFromPlexAPI(`/library/sections/${library._attributes.key}/all`, widget);
|
||||
if (apiData && apiData.MediaContainer) {
|
||||
const size = parseInt(apiData.MediaContainer._attributes.size, 10);
|
||||
if (library._attributes.type === "movie") {
|
||||
movies += size;
|
||||
} else if (library._attributes.type === "show") {
|
||||
tv += size;
|
||||
}
|
||||
}
|
||||
cache.put(tvCacheKey, tv, 1000 * 60 * 10);
|
||||
cache.put(moviesCacheKey, movies, 1000 * 60 * 10);
|
||||
});
|
||||
}
|
||||
|
||||
const data = {
|
||||
streams,
|
||||
tv,
|
||||
movies
|
||||
};
|
||||
|
||||
return res.status(status).send(data);
|
||||
}
|
14
src/widgets/plex/widget.js
Normal file
14
src/widgets/plex/widget.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import plexProxyHandler from "./proxy";
|
||||
|
||||
const widget = {
|
||||
api: "{url}{endpoint}?X-Plex-Token={token}",
|
||||
proxyHandler: plexProxyHandler,
|
||||
|
||||
mappings: {
|
||||
unified: {
|
||||
endpoint: "/",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
|
@ -13,6 +13,7 @@ import nzbget from "./nzbget/widget";
|
|||
import ombi from "./ombi/widget";
|
||||
import overseerr from "./overseerr/widget";
|
||||
import pihole from "./pihole/widget";
|
||||
import plex from "./plex/widget";
|
||||
import portainer from "./portainer/widget";
|
||||
import prowlarr from "./prowlarr/widget";
|
||||
import proxmox from "./proxmox/widget";
|
||||
|
@ -46,6 +47,7 @@ const widgets = {
|
|||
ombi,
|
||||
overseerr,
|
||||
pihole,
|
||||
plex,
|
||||
portainer,
|
||||
prowlarr,
|
||||
proxmox,
|
||||
|
|
Loading…
Add table
Reference in a new issue