Merge pull request #1608 from karl0ss/benphelpsJdownloader
Working Jdownloader
This commit is contained in:
commit
7afe62df2a
7 changed files with 307 additions and 1 deletions
|
@ -653,5 +653,10 @@
|
||||||
"whatsupdocker": {
|
"whatsupdocker": {
|
||||||
"monitoring": "Monitoring",
|
"monitoring": "Monitoring",
|
||||||
"updates": "Updates"
|
"updates": "Updates"
|
||||||
|
},
|
||||||
|
"jdownloader": {
|
||||||
|
"downloadCount": "Queue Count",
|
||||||
|
"downloadQueueSize": "Queue Size",
|
||||||
|
"downloadSpeed": "Download Speed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
src/widgets/jdownloader/component.jsx
Normal file
37
src/widgets/jdownloader/component.jsx
Normal file
|
@ -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
src/widgets/jdownloader/proxy.js
Normal file
191
src/widgets/jdownloader/proxy.js
Normal file
|
@ -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
src/widgets/jdownloader/tools.js
Normal file
55
src/widgets/jdownloader/tools.js
Normal file
|
@ -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
src/widgets/jdownloader/widget.js
Normal file
15
src/widgets/jdownloader/widget.js
Normal file
|
@ -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;
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue