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 3dd2ed0d1..75d02e7fd 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 49394c8ea..73e30e541 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 b1097904d..1261c2930 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 c2a6705ed..f6e075d8a 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 000000000..731a9550d --- /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 000000000..c03566c8e --- /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 000000000..e35664236 --- /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 a4cab76b0..cd7f5b19a 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,