mirror of
				https://github.com/karl0ss/homepage.git
				synced 2025-11-04 08:20:58 +00:00 
			
		
		
		
	Merge pull request #366 from shamoon/plex-service-widget
Feature: Plex service widget without Tautulli
This commit is contained in:
		
						commit
						5198b056cc
					
				@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user