Jelajahi Sumber

Plex service widget without Tautulli

Michael Shamoon 2 tahun lalu
induk
melakukan
2440da8e08

+ 2 - 1
package.json

@@ -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 - 0
pnpm-lock.yaml

@@ -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'}

+ 5 - 0
public/locales/en/common.json

@@ -66,6 +66,11 @@
         "remaining": "Remaining",
         "downloaded": "Downloaded"
     },
+    "plex": {
+        "streams": "Active Streams",
+        "movies": "Movies",
+        "tv": "TV Shows"
+    },
     "sabnzbd": {
         "rate": "Rate",
         "queue": "Queue",

+ 1 - 0
src/widgets/components.js

@@ -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")),

+ 37 - 0
src/widgets/plex/component.jsx

@@ -0,0 +1,37 @@
+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>
+  );
+}

+ 108 - 0
src/widgets/plex/proxy.js

@@ -0,0 +1,108 @@
+import cache from "memory-cache";
+
+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";
+import { xml2json } from "xml-js";
+
+// const udmpPrefix = "/proxy/network";
+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 res.status(403).json({ error: "Service does not support API calls" });
+  }
+
+  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)];
+  } 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 == undefined) {
+    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 == undefined || tv == undefined) {
+    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 librarySection = apiData.MediaContainer._attributes;
+        if (library._attributes.type == 'movie') {
+          movies += parseInt(librarySection.size);
+        } else if (library._attributes.type == 'show') {
+          tv += parseInt(librarySection.size);
+        }
+      }
+      cache.put(tvCacheKey, tv, 1000 * 60 * 10);
+      cache.put(moviesCacheKey, movies, 1000 * 60 * 10);
+    });
+  }
+  
+  const data = {
+    streams: streams,
+    tv: tv,
+    movies: movies
+  };
+
+  return res.status(status).send(data);
+}

+ 14 - 0
src/widgets/plex/widget.js

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

+ 2 - 0
src/widgets/widgets.js

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