mirror of
				https://github.com/karl0ss/homepage.git
				synced 2025-11-04 08:20:58 +00:00 
			
		
		
		
	Add Deluge widget
- Create semi-generic jsonrpc proxy handler - Refactor NZBGet to use jsonrpc proxy handler closes #190
This commit is contained in:
		
							parent
							
								
									92d456dbf4
								
							
						
					
					
						commit
						7266390491
					
				@ -112,6 +112,12 @@
 | 
				
			|||||||
        "leech": "Leech",
 | 
					        "leech": "Leech",
 | 
				
			||||||
        "seed": "Seed"
 | 
					        "seed": "Seed"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "deluge": {
 | 
				
			||||||
 | 
					        "download": "Download",
 | 
				
			||||||
 | 
					        "upload": "Upload",
 | 
				
			||||||
 | 
					        "leech": "Leech",
 | 
				
			||||||
 | 
					        "seed": "Seed"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "sonarr": {
 | 
					    "sonarr": {
 | 
				
			||||||
        "wanted": "Wanted",
 | 
					        "wanted": "Wanted",
 | 
				
			||||||
        "queued": "Queued",
 | 
					        "queued": "Queued",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										82
									
								
								src/utils/proxy/handlers/jsonrpc.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/utils/proxy/handlers/jsonrpc.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
				
			|||||||
 | 
					import { JSONRPCClient, JSONRPCErrorException } from "json-rpc-2.0";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 logger = createLogger("jsonrpcProxyHandler");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function sendJsonRpcRequest(url, method, params, username, password) {
 | 
				
			||||||
 | 
					  const headers = {
 | 
				
			||||||
 | 
					    "content-type": "application/json",
 | 
				
			||||||
 | 
					    "accept": "application/json"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (username && password) {
 | 
				
			||||||
 | 
					    const authorization = Buffer.from(`${username}:${password}`).toString("base64");
 | 
				
			||||||
 | 
					    headers.authorization = `Basic ${authorization}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const client = new JSONRPCClient(async (rpcRequest) => {
 | 
				
			||||||
 | 
					    const httpRequestParams = {
 | 
				
			||||||
 | 
					      method: "POST",
 | 
				
			||||||
 | 
					      headers,
 | 
				
			||||||
 | 
					      body: JSON.stringify(rpcRequest)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line no-unused-vars
 | 
				
			||||||
 | 
					    const [status, contentType, data] = await httpProxy(url, httpRequestParams);
 | 
				
			||||||
 | 
					    const dataString = data.toString();
 | 
				
			||||||
 | 
					    if (status === 200) {
 | 
				
			||||||
 | 
					      const json = JSON.parse(dataString);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // in order to get access to the underlying error object in the JSON response
 | 
				
			||||||
 | 
					      // you must set `result` equal to undefined
 | 
				
			||||||
 | 
					      if (json.error && (json.result === null)) {
 | 
				
			||||||
 | 
					        json.result = undefined;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return client.receive(json);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Promise.reject(new Error(dataString));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await client.request(method, params);
 | 
				
			||||||
 | 
					    return [200, "application/json", JSON.stringify(response)];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  catch (e) {
 | 
				
			||||||
 | 
					    if (e instanceof JSONRPCErrorException) {
 | 
				
			||||||
 | 
					      return [200, "application/json", JSON.stringify({result: null, error: {code: e.code, message: e.message}})];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logger.warn("Error calling JSONPRC endpoint: %s.  %s", url, e);
 | 
				
			||||||
 | 
					    return [500, "application/json", JSON.stringify({result: null, error: {code: 2, message: e.toString()}})];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function jsonrpcProxyHandler(req, res) {
 | 
				
			||||||
 | 
					  const { group, service, endpoint: method } = req.query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (group && service) {
 | 
				
			||||||
 | 
					    const widget = await getServiceWidget(group, service);
 | 
				
			||||||
 | 
					    const api = widgets?.[widget.type]?.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!api) {
 | 
				
			||||||
 | 
					      return res.status(403).json({ error: "Service does not support API calls" });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (widget) {
 | 
				
			||||||
 | 
					      const url = formatApiCall(api, { ...widget });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // eslint-disable-next-line no-unused-vars
 | 
				
			||||||
 | 
					      const [status, contentType, data] = await sendJsonRpcRequest(url, method, null, widget.username, widget.password);
 | 
				
			||||||
 | 
					      res.status(status).end(data);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group);
 | 
				
			||||||
 | 
					  return res.status(400).json({ error: "Invalid proxy service type" });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -7,6 +7,7 @@ const components = {
 | 
				
			|||||||
  bazarr: dynamic(() => import("./bazarr/component")),
 | 
					  bazarr: dynamic(() => import("./bazarr/component")),
 | 
				
			||||||
  changedetectionio: dynamic(() => import("./changedetectionio/component")),
 | 
					  changedetectionio: dynamic(() => import("./changedetectionio/component")),
 | 
				
			||||||
  coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
 | 
					  coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
 | 
				
			||||||
 | 
					  deluge: dynamic(() => import("./deluge/component")),
 | 
				
			||||||
  docker: dynamic(() => import("./docker/component")),
 | 
					  docker: dynamic(() => import("./docker/component")),
 | 
				
			||||||
  emby: dynamic(() => import("./emby/component")),
 | 
					  emby: dynamic(() => import("./emby/component")),
 | 
				
			||||||
  gluetun: dynamic(() => import("./gluetun/component")),
 | 
					  gluetun: dynamic(() => import("./gluetun/component")),
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										52
									
								
								src/widgets/deluge/component.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/widgets/deluge/component.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
				
			|||||||
 | 
					import { useTranslation } from "next-i18next";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Container from "components/services/widget/container";
 | 
				
			||||||
 | 
					import Block from "components/services/widget/block";
 | 
				
			||||||
 | 
					import useWidgetAPI from "utils/proxy/use-widget-api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Component({ service }) {
 | 
				
			||||||
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { widget } = service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { data: torrentData, error: torrentError } = useWidgetAPI(widget);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (torrentError) {
 | 
				
			||||||
 | 
					    return <Container error={torrentError} />;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!torrentData) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Container service={service}>
 | 
				
			||||||
 | 
					        <Block label="deluge.leech" />
 | 
				
			||||||
 | 
					        <Block label="deluge.download" />
 | 
				
			||||||
 | 
					        <Block label="deluge.seed" />
 | 
				
			||||||
 | 
					        <Block label="deluge.upload" />
 | 
				
			||||||
 | 
					      </Container>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { torrents } = torrentData;
 | 
				
			||||||
 | 
					  let count = 0;
 | 
				
			||||||
 | 
					  let rateDl = 0;
 | 
				
			||||||
 | 
					  let rateUl = 0;
 | 
				
			||||||
 | 
					  let completed = 0;
 | 
				
			||||||
 | 
					  for (const key of Object.keys(torrents)) {
 | 
				
			||||||
 | 
					    const torrent = torrents[key];
 | 
				
			||||||
 | 
					    count += 1;
 | 
				
			||||||
 | 
					    rateDl += torrent.download_payload_rate;
 | 
				
			||||||
 | 
					    rateUl += torrent.upload_payload_rate;
 | 
				
			||||||
 | 
					    completed += torrent.total_remaining === 0 ? 1 : 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const leech = count - completed || 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Container service={service}>
 | 
				
			||||||
 | 
					      <Block label="deluge.leech" value={t("common.number", { value: leech })} />
 | 
				
			||||||
 | 
					      <Block label="deluge.download" value={t("common.bitrate", { value: rateDl })} />
 | 
				
			||||||
 | 
					      <Block label="deluge.seed" value={t("common.number", { value: completed })} />
 | 
				
			||||||
 | 
					      <Block label="deluge.upload" value={t("common.bitrate", { value: rateUl })} />
 | 
				
			||||||
 | 
					    </Container>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										63
									
								
								src/widgets/deluge/proxy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/widgets/deluge/proxy.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,63 @@
 | 
				
			|||||||
 | 
					import { formatApiCall } from "utils/proxy/api-helpers";
 | 
				
			||||||
 | 
					import { sendJsonRpcRequest } from "utils/proxy/handlers/jsonrpc";
 | 
				
			||||||
 | 
					import getServiceWidget from "utils/config/service-helpers";
 | 
				
			||||||
 | 
					import createLogger from "utils/logger";
 | 
				
			||||||
 | 
					import widgets from "widgets/widgets";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const logger = createLogger("delugeProxyHandler");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const dataMethod = "web.update_ui";
 | 
				
			||||||
 | 
					const dataParams = [
 | 
				
			||||||
 | 
					  ["queue", "name", "total_wanted", "state", "progress", "download_payload_rate", "upload_payload_rate", "total_remaining"],
 | 
				
			||||||
 | 
					  {}
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					const loginMethod = "auth.login";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function sendRpc(url, method, params, username, password) {
 | 
				
			||||||
 | 
					  const [status, contentType, data] = await sendJsonRpcRequest(url, method, params, username, password);
 | 
				
			||||||
 | 
					  const json = JSON.parse(data.toString());
 | 
				
			||||||
 | 
					  if (json?.error) {
 | 
				
			||||||
 | 
					    if (json.error.code === 1) {
 | 
				
			||||||
 | 
					      return [403, contentType, data];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return [500, contentType, data];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return [status, contentType, data];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function login(url, username, password) {
 | 
				
			||||||
 | 
					  return sendRpc(url, loginMethod, [password], username, password);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function delugeProxyHandler(req, res) {
 | 
				
			||||||
 | 
					  const { group, service } = req.query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!group || !service) {
 | 
				
			||||||
 | 
					    logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
 | 
				
			||||||
 | 
					    return res.status(400).json({ error: "Invalid proxy service type" });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const widget = await getServiceWidget(group, service);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!widget) {
 | 
				
			||||||
 | 
					    logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
 | 
				
			||||||
 | 
					    return res.status(400).json({ error: "Invalid proxy service type" });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const api = widgets?.[widget.type]?.api
 | 
				
			||||||
 | 
					  const url = new URL(formatApiCall(api, { ...widget }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let [status, contentType, data] = await sendRpc(url, dataMethod, dataParams, widget.username, widget.password);
 | 
				
			||||||
 | 
					  if (status === 403) {
 | 
				
			||||||
 | 
					    [status, contentType, data] = await login(url, widget.username, widget.password);
 | 
				
			||||||
 | 
					    if (status !== 200) {
 | 
				
			||||||
 | 
					      return res.status(status).end(data);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line no-unused-vars
 | 
				
			||||||
 | 
					    [status, contentType, data] = await sendRpc(url, dataMethod, dataParams, widget.username, widget.password);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return res.status(status).end(data);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/widgets/deluge/widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/widgets/deluge/widget.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import delugeProxyHandler from "./proxy";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const widget = {
 | 
				
			||||||
 | 
					  api: "{url}/json",
 | 
				
			||||||
 | 
					  proxyHandler: delugeProxyHandler,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default widget;
 | 
				
			||||||
@ -1,40 +0,0 @@
 | 
				
			|||||||
import { JSONRPCClient } from "json-rpc-2.0";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import getServiceWidget from "utils/config/service-helpers";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default async function nzbgetProxyHandler(req, res) {
 | 
					 | 
				
			||||||
  const { group, service, endpoint } = req.query;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (group && service) {
 | 
					 | 
				
			||||||
    const widget = await getServiceWidget(group, service);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (widget) {
 | 
					 | 
				
			||||||
      const constructedUrl = new URL(widget.url);
 | 
					 | 
				
			||||||
      constructedUrl.pathname = "jsonrpc";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const authorization = Buffer.from(`${widget.username}:${widget.password}`).toString("base64");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const client = new JSONRPCClient((jsonRPCRequest) =>
 | 
					 | 
				
			||||||
        fetch(constructedUrl.toString(), {
 | 
					 | 
				
			||||||
          method: "POST",
 | 
					 | 
				
			||||||
          headers: {
 | 
					 | 
				
			||||||
            "content-type": "application/json",
 | 
					 | 
				
			||||||
            authorization: `Basic ${authorization}`,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          body: JSON.stringify(jsonRPCRequest),
 | 
					 | 
				
			||||||
        }).then(async (response) => {
 | 
					 | 
				
			||||||
          if (response.status === 200) {
 | 
					 | 
				
			||||||
            const jsonRPCResponse = await response.json();
 | 
					 | 
				
			||||||
            return client.receive(jsonRPCResponse);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return Promise.reject(new Error(response.statusText));
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return res.send(await client.request(endpoint));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return res.status(400).json({ error: "Invalid proxy service type" });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,7 +1,8 @@
 | 
				
			|||||||
import nzbgetProxyHandler from "./proxy";
 | 
					import jsonrpcProxyHandler from "utils/proxy/handlers/jsonrpc";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const widget = {
 | 
					const widget = {
 | 
				
			||||||
  proxyHandler: nzbgetProxyHandler,
 | 
					  api: "{url}/jsonrpc",
 | 
				
			||||||
 | 
					  proxyHandler: jsonrpcProxyHandler,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default widget;
 | 
					export default widget;
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import autobrr from "./autobrr/widget";
 | 
				
			|||||||
import bazarr from "./bazarr/widget";
 | 
					import bazarr from "./bazarr/widget";
 | 
				
			||||||
import changedetectionio from "./changedetectionio/widget";
 | 
					import changedetectionio from "./changedetectionio/widget";
 | 
				
			||||||
import coinmarketcap from "./coinmarketcap/widget";
 | 
					import coinmarketcap from "./coinmarketcap/widget";
 | 
				
			||||||
 | 
					import deluge from "./deluge/widget";
 | 
				
			||||||
import emby from "./emby/widget";
 | 
					import emby from "./emby/widget";
 | 
				
			||||||
import gluetun from "./gluetun/widget";
 | 
					import gluetun from "./gluetun/widget";
 | 
				
			||||||
import gotify from "./gotify/widget";
 | 
					import gotify from "./gotify/widget";
 | 
				
			||||||
@ -47,6 +48,7 @@ const widgets = {
 | 
				
			|||||||
  bazarr,
 | 
					  bazarr,
 | 
				
			||||||
  changedetectionio,
 | 
					  changedetectionio,
 | 
				
			||||||
  coinmarketcap,
 | 
					  coinmarketcap,
 | 
				
			||||||
 | 
					  deluge,
 | 
				
			||||||
  emby,
 | 
					  emby,
 | 
				
			||||||
  gluetun,
 | 
					  gluetun,
 | 
				
			||||||
  gotify,
 | 
					  gotify,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user