From 2440da8e08ba90c9230b7b31c9fba1d4ceff9432 Mon Sep 17 00:00:00 2001
From: Michael Shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 10 Oct 2022 00:31:50 -0700
Subject: [PATCH] Plex service widget without Tautulli
---
package.json | 3 +-
pnpm-lock.yaml | 13 ++++
public/locales/en/common.json | 5 ++
src/widgets/components.js | 1 +
src/widgets/plex/component.jsx | 37 +++++++++++
src/widgets/plex/proxy.js | 108 +++++++++++++++++++++++++++++++++
src/widgets/plex/widget.js | 14 +++++
src/widgets/widgets.js | 2 +
8 files changed, 182 insertions(+), 1 deletion(-)
create mode 100644 src/widgets/plex/component.jsx
create mode 100644 src/widgets/plex/proxy.js
create mode 100644 src/widgets/plex/widget.js
diff --git a/package.json b/package.json
index 3dd2ed0d..75d02e7f 100644
--- a/package.json
+++ b/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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 49394c8e..73e30e54 100644
--- a/pnpm-lock.yaml
+++ b/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'}
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index b1097904..1261c293 100644
--- a/public/locales/en/common.json
+++ b/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",
diff --git a/src/widgets/components.js b/src/widgets/components.js
index c2a6705e..f6e075d8 100644
--- a/src/widgets/components.js
+++ b/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")),
diff --git a/src/widgets/plex/component.jsx b/src/widgets/plex/component.jsx
new file mode 100644
index 00000000..731a9550
--- /dev/null
+++ b/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 ;
+ }
+
+ if (!plexData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/plex/proxy.js b/src/widgets/plex/proxy.js
new file mode 100644
index 00000000..c03566c8
--- /dev/null
+++ b/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);
+}
diff --git a/src/widgets/plex/widget.js b/src/widgets/plex/widget.js
new file mode 100644
index 00000000..e3566423
--- /dev/null
+++ b/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;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index a4cab76b..cd7f5b19 100644
--- a/src/widgets/widgets.js
+++ b/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,