Browse Source

Merge pull request #1608 from karl0ss/benphelpsJdownloader

Working Jdownloader
shamoon 2 years ago
parent
commit
7afe62df2a

+ 5 - 0
public/locales/en/common.json

@@ -653,5 +653,10 @@
     "whatsupdocker": {
     "whatsupdocker": {
         "monitoring": "Monitoring",
         "monitoring": "Monitoring",
         "updates": "Updates"
         "updates": "Updates"
+    },
+    "jdownloader": {
+        "downloadCount": "Queue Count",
+        "downloadQueueSize": "Queue Size",
+        "downloadSpeed": "Download Speed"
     }
     }
 }
 }

+ 2 - 1
src/widgets/components.js

@@ -31,6 +31,7 @@ const components = {
   healthchecks: dynamic(() => import("./healthchecks/component")),
   healthchecks: dynamic(() => import("./healthchecks/component")),
   immich: dynamic(() => import("./immich/component")),
   immich: dynamic(() => import("./immich/component")),
   jackett: dynamic(() => import("./jackett/component")),
   jackett: dynamic(() => import("./jackett/component")),
+  jdownloader: dynamic(() => import("./jdownloader/component")),
   jellyfin: dynamic(() => import("./emby/component")),
   jellyfin: dynamic(() => import("./emby/component")),
   jellyseerr: dynamic(() => import("./jellyseerr/component")),
   jellyseerr: dynamic(() => import("./jellyseerr/component")),
   komga: dynamic(() => import("./komga/component")),
   komga: dynamic(() => import("./komga/component")),
@@ -91,4 +92,4 @@ const components = {
   xteve: dynamic(() => import("./xteve/component")),
   xteve: dynamic(() => import("./xteve/component")),
 };
 };
 
 
-export default components;
+export default components;

+ 37 - 0
src/widgets/jdownloader/component.jsx

@@ -0,0 +1,37 @@
+import { useTranslation } from "next-i18next";
+
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+export default function Component({ service }) {
+    const { t } = useTranslation();
+
+    const { widget } = service;
+
+    const { data: jdownloaderData, error: jdownloaderAPIError } = useWidgetAPI(widget, "unified", {
+        refreshInterval: 30000,
+    });
+
+    if (jdownloaderAPIError) {
+        return <Container service={service} error={jdownloaderAPIError} />;
+    }
+
+    if (!jdownloaderData) {
+        return (
+            <Container service={service}>
+                <Block label="jdownloader.downloadCount" />
+                <Block label="jdownloader.downloadQueueSize" />
+                <Block label="jdownloader.downloadSpeed" />
+            </Container>
+        );
+    }
+
+    return (
+        <Container service={service}>
+            <Block label="jdownloader.downloadCount" value={t("common.number", { value: jdownloaderData.downloadCount })} />
+            <Block label="jdownloader.downloadQueueSize" value={t("common.bytes", { value: jdownloaderData.totalBytes })} />
+            <Block label="jdownloader.downloadSpeed" value={t("common.byterate", { value: jdownloaderData.totalSpeed })} />
+        </Container>
+    );
+}

+ 191 - 0
src/widgets/jdownloader/proxy.js

@@ -0,0 +1,191 @@
+/* eslint-disable no-underscore-dangle */
+import crypto from 'crypto';
+import querystring from 'querystring';
+
+import { sha256, uniqueRid, validateRid, createEncryptionToken, decrypt, encrypt } from "./tools"
+
+import getServiceWidget from "utils/config/service-helpers";
+import { httpProxy } from "utils/proxy/http";
+import createLogger from "utils/logger";
+
+const proxyName = "jdownloaderProxyHandler";
+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 login(loginSecret, deviceSecret, params) {
+    const rid = uniqueRid();
+    const path = `/my/connect?${querystring.stringify({...params, rid})}`;
+    
+    const signature = crypto
+        .createHmac('sha256', loginSecret)
+        .update(path)
+        .digest('hex');
+    const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
+
+    const [status, contentType, data] = await httpProxy(url, {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+    });
+
+    if (status !== 200) {
+        logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
+        return [status, data];
+    }
+
+    try {
+        const decryptedData = JSON.parse(decrypt(data.toString(), loginSecret))
+        const sessionToken = decryptedData.sessiontoken;
+        validateRid(decryptedData, rid);
+        const serverEncryptionToken = createEncryptionToken(loginSecret, sessionToken);
+        const deviceEncryptionToken = createEncryptionToken(deviceSecret, sessionToken);
+        return [status, decryptedData, contentType, serverEncryptionToken, deviceEncryptionToken, sessionToken];
+    } catch (e) {
+        logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
+        return [status, null];
+    }
+}
+
+
+async function getDevice(serverEncryptionToken, deviceName, params) {
+    const rid = uniqueRid();
+    const path = `/my/listdevices?${querystring.stringify({...params, rid})}`;
+    const signature = crypto
+        .createHmac('sha256', serverEncryptionToken)
+        .update(path)
+        .digest('hex');
+    const url = `${new URL(`https://api.jdownloader.org${path}&signature=${signature}`)}`
+
+    const [status, , data] = await httpProxy(url, {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+        },
+    });
+
+    if (status !== 200) {
+        logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
+        return [status, data];
+    }
+
+    try {
+        const decryptedData = JSON.parse(decrypt(data.toString(), serverEncryptionToken))
+        const filteredDevice = decryptedData.list.filter(device => device.name === deviceName);
+        return [status, filteredDevice[0].id];
+    } catch (e) {
+        logger.error("Error decoding jdownloader API data. Data: %s", data.toString());
+        return [status, null];
+    }
+
+}
+
+function createBody(rid, query, params) {
+    const baseBody = {
+        apiVer: 1,
+        rid,
+        url: query
+    };
+    return params ? {...baseBody, params: [JSON.stringify(params)] } : baseBody;
+}
+
+async function queryPackages(deviceEncryptionToken, deviceId, sessionToken, params) {
+    const rid = uniqueRid();
+    const body = encrypt(JSON.stringify(createBody(rid, '/downloadsV2/queryPackages', params)), deviceEncryptionToken);
+    const url = `${new URL(`https://api.jdownloader.org/t_${encodeURI(sessionToken)}_${encodeURI(deviceId)}/downloadsV2/queryPackages`)}`
+    const [status, , data] = await httpProxy(url, {
+        method: 'POST',
+        body,
+    });
+
+    if (status !== 200) {
+        logger.error("HTTP %d communicating with jdownloader. Data: %s", status, data.toString());
+        return [status, data];
+    }
+
+    try {
+        const decryptedData = JSON.parse(decrypt(data.toString(), deviceEncryptionToken))
+        return decryptedData.data;
+    } catch (e) {
+        logger.error("Error decoding JDRss jdownloader data. Data: %s", data.toString());
+        return [status, null];
+    }
+
+}
+
+
+export default async function jdownloaderProxyHandler(req, res) {
+    const widget = await getWidget(req);
+
+    if (!widget) {
+        return res.status(400).json({ error: "Invalid proxy service type" });
+    }
+    logger.debug("Getting data from JDRss API");
+    const {username} = widget
+    const {password} = widget
+    
+    const appKey = "homepage"
+    const loginSecret = sha256(`${username}${password}server`)
+    const deviceSecret = sha256(`${username}${password}device`)
+    const email = username;
+
+    const loginData = await login(loginSecret, deviceSecret, {
+        appKey,
+        email
+    })
+
+    const deviceData = await getDevice(loginData[3], widget.client, {
+        sessiontoken: loginData[5]
+    })
+
+    const packageStatus = await queryPackages(loginData[4], deviceData[1], loginData[5], {
+        "bytesLoaded": false,
+        "bytesTotal": true,
+        "comment": false,
+        "enabled": true,
+        "eta": false,
+        "priority": false,
+        "finished": true,
+        "running": true,
+        "speed": true,
+        "status": true,
+        "childCount": false,
+        "hosts": false,
+        "saveTo": false,
+        "maxResults": -1,
+        "startAt": 0,
+    }
+    )
+
+    let totalBytes = 0;
+    let totalSpeed = 0;
+    packageStatus.forEach(file => {
+        totalBytes += file.bytesTotal;
+        if (file.speed) {
+            totalSpeed += file.speed;
+        }
+    });
+
+    const data = {
+        downloadCount: packageStatus.length,
+        totalBytes,
+        totalSpeed
+    };
+
+    return res.send(data);
+
+}

+ 55 - 0
src/widgets/jdownloader/tools.js

@@ -0,0 +1,55 @@
+import crypto from 'crypto';
+
+export function sha256(data) {
+    return crypto
+        .createHash('sha256')
+        .update(data)
+        .digest();
+}
+
+export function uniqueRid() {
+    return Math.floor(Math.random() * 10e12);
+}
+
+export function validateRid(decryptedData, rid) {
+    if (decryptedData.rid !== rid) {
+        throw new Error('RequestID mismatch');
+    }
+    return decryptedData;
+
+}
+
+export function decrypt(data, ivKey) {
+    const iv = ivKey.slice(0, ivKey.length / 2);
+    const key = ivKey.slice(ivKey.length / 2, ivKey.length);
+    const cipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
+    return Buffer.concat([
+        cipher.update(data, 'base64'),
+        cipher.final()
+    ]).toString();
+}
+
+export function createEncryptionToken(oldTokenBuff, updateToken) {
+    const updateTokenBuff = Buffer.from(updateToken, 'hex');
+    const mergedBuffer = Buffer.concat([oldTokenBuff, updateTokenBuff], oldTokenBuff.length + updateTokenBuff.length);
+    return sha256(mergedBuffer);
+}
+
+export function encrypt(data, ivKey) {
+    if (typeof data !== 'string') {
+        throw new Error('data no es un string');
+    }
+    if (!(ivKey instanceof Buffer)) {
+        throw new Error('ivKey no es un buffer');
+    }
+    if (ivKey.length !== 32) {
+        throw new Error('ivKey tiene que tener tamaño 32');
+    }
+    const stringIVKey = ivKey.toString('hex');
+    const stringIV = stringIVKey.substring(0, stringIVKey.length / 2);
+    const stringKey = stringIVKey.substring(stringIVKey.length / 2, stringIVKey.length);
+    const iv = Buffer.from(stringIV, 'hex');
+    const key = Buffer.from(stringKey, 'hex');
+    const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
+    return cipher.update(data, 'utf8', 'base64') + cipher.final('base64');
+}

+ 15 - 0
src/widgets/jdownloader/widget.js

@@ -0,0 +1,15 @@
+import jdownloaderProxyHandler from "./proxy";
+
+const widget = {
+    api: "https://api.jdownloader.org/{endpoint}/&signature={signature}",
+    proxyHandler: jdownloaderProxyHandler,
+
+    mappings: {
+        unified: {
+            endpoint: "/",
+            signature: "",
+        },
+    },
+};
+
+export default widget;

+ 2 - 0
src/widgets/widgets.js

@@ -27,6 +27,7 @@ import healthchecks from "./healthchecks/widget";
 import immich from "./immich/widget";
 import immich from "./immich/widget";
 import jackett from "./jackett/widget";
 import jackett from "./jackett/widget";
 import jellyseerr from "./jellyseerr/widget";
 import jellyseerr from "./jellyseerr/widget";
+import jdownloader from "./jdownloader/widget";
 import komga from "./komga/widget";
 import komga from "./komga/widget";
 import kopia from "./kopia/widget";
 import kopia from "./kopia/widget";
 import lidarr from "./lidarr/widget";
 import lidarr from "./lidarr/widget";
@@ -113,6 +114,7 @@ const widgets = {
   healthchecks,
   healthchecks,
   immich,
   immich,
   jackett,
   jackett,
+  jdownloader,
   jellyfin: emby,
   jellyfin: emby,
   jellyseerr,
   jellyseerr,
   komga,
   komga,