mirror of
				https://github.com/karl0ss/homepage.git
				synced 2025-11-04 00:10:57 +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",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "deluge": {
 | 
			
		||||
        "download": "Download",
 | 
			
		||||
        "upload": "Upload",
 | 
			
		||||
        "leech": "Leech",
 | 
			
		||||
        "seed": "Seed"
 | 
			
		||||
    },
 | 
			
		||||
    "sonarr": {
 | 
			
		||||
        "wanted": "Wanted",
 | 
			
		||||
        "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")),
 | 
			
		||||
  changedetectionio: dynamic(() => import("./changedetectionio/component")),
 | 
			
		||||
  coinmarketcap: dynamic(() => import("./coinmarketcap/component")),
 | 
			
		||||
  deluge: dynamic(() => import("./deluge/component")),
 | 
			
		||||
  docker: dynamic(() => import("./docker/component")),
 | 
			
		||||
  emby: dynamic(() => import("./emby/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 = {
 | 
			
		||||
  proxyHandler: nzbgetProxyHandler,
 | 
			
		||||
  api: "{url}/jsonrpc",
 | 
			
		||||
  proxyHandler: jsonrpcProxyHandler,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default widget;
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import autobrr from "./autobrr/widget";
 | 
			
		||||
import bazarr from "./bazarr/widget";
 | 
			
		||||
import changedetectionio from "./changedetectionio/widget";
 | 
			
		||||
import coinmarketcap from "./coinmarketcap/widget";
 | 
			
		||||
import deluge from "./deluge/widget";
 | 
			
		||||
import emby from "./emby/widget";
 | 
			
		||||
import gluetun from "./gluetun/widget";
 | 
			
		||||
import gotify from "./gotify/widget";
 | 
			
		||||
@ -47,6 +48,7 @@ const widgets = {
 | 
			
		||||
  bazarr,
 | 
			
		||||
  changedetectionio,
 | 
			
		||||
  coinmarketcap,
 | 
			
		||||
  deluge,
 | 
			
		||||
  emby,
 | 
			
		||||
  gluetun,
 | 
			
		||||
  gotify,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user