Harden Mega web flow and smooth download runtime
Some checks are pending
Build and Release / build (push) Waiting to run

This commit is contained in:
Sucukdeluxe 2026-02-27 06:17:15 +01:00
parent 40bfda2ad7
commit 583d74fcc9
9 changed files with 198 additions and 61 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.21", "version": "1.1.22",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.21", "version": "1.1.22",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.21", "version": "1.1.22",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -1,5 +1,6 @@
import { DebridService } from "../src/main/debrid"; import { DebridService } from "../src/main/debrid";
import { defaultSettings } from "../src/main/constants"; import { defaultSettings } from "../src/main/constants";
import { MegaWebFallback } from "../src/main/mega-web-fallback";
const links = [ const links = [
"https://rapidgator.net/file/837ef967aede4935e3e0374c4e663b40/GTHDERTPIIP7P401.part1.rar.html", "https://rapidgator.net/file/837ef967aede4935e3e0374c4e663b40/GTHDERTPIIP7P401.part1.rar.html",
@ -26,7 +27,13 @@ if (!settings.token && !(settings.megaLogin && settings.megaPassword) && !settin
} }
async function main(): Promise<void> { async function main(): Promise<void> {
const service = new DebridService(settings); const megaWeb = new MegaWebFallback(() => ({
login: settings.megaLogin,
password: settings.megaPassword
}));
const service = new DebridService(settings, {
megaWebUnrestrict: (link) => megaWeb.unrestrict(link)
});
for (const link of links) { for (const link of links) {
try { try {
const result = await service.unrestrictLink(link); const result = await service.unrestrictLink(link);
@ -35,6 +42,7 @@ async function main(): Promise<void> {
console.log(`[FAIL] ${String(error)}`); console.log(`[FAIL] ${String(error)}`);
} }
} }
megaWeb.dispose();
} }
void main(); void main();

View File

@ -5,12 +5,14 @@ const RAPIDGATOR_LINKS = [
]; ];
const rdToken = process.env.RD_TOKEN || ""; const rdToken = process.env.RD_TOKEN || "";
const megaToken = process.env.MEGA_TOKEN || ""; const megaLogin = process.env.MEGA_LOGIN || "";
const megaPassword = process.env.MEGA_PASSWORD || "";
const bestToken = process.env.BEST_TOKEN || ""; const bestToken = process.env.BEST_TOKEN || "";
const allDebridToken = process.env.ALLDEBRID_TOKEN || ""; const allDebridToken = process.env.ALLDEBRID_TOKEN || "";
let megaCookie = "";
if (!rdToken && !megaToken && !bestToken && !allDebridToken) { if (!rdToken && !(megaLogin && megaPassword) && !bestToken && !allDebridToken) {
console.error("No provider token configured. Set RD_TOKEN and/or MEGA_TOKEN and/or BEST_TOKEN and/or ALLDEBRID_TOKEN."); console.error("No provider credentials configured. Set RD_TOKEN and/or MEGA_LOGIN+MEGA_PASSWORD and/or BEST_TOKEN and/or ALLDEBRID_TOKEN.");
process.exit(1); process.exit(1);
} }
@ -65,32 +67,78 @@ async function callRealDebrid(link) {
} }
async function callMegaDebrid(link) { async function callMegaDebrid(link) {
const response = await fetch(`https://www.mega-debrid.eu/api.php?action=getLink&token=${encodeURIComponent(megaToken)}`, { if (!megaCookie) {
const loginRes = await fetch("https://www.mega-debrid.eu/index.php?form=login", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0"
},
body: new URLSearchParams({ login: megaLogin, password: megaPassword, remember: "on" }),
redirect: "manual"
});
megaCookie = (loginRes.headers.get("set-cookie") || "")
.split(",")
.map((chunk) => chunk.split(";")[0].trim())
.filter(Boolean)
.join("; ");
if (!megaCookie) {
return { ok: false, error: "Mega-Web login failed" };
}
}
const debridRes = await fetch("https://www.mega-debrid.eu/index.php?form=debrid", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "RD-Node-Downloader/1.1.12" "User-Agent": "Mozilla/5.0",
Cookie: megaCookie,
Referer: "https://www.mega-debrid.eu/index.php?page=debrideur&lang=de"
}, },
body: new URLSearchParams({ link }) body: new URLSearchParams({ links: link, password: "", showLinks: "1" })
}); });
const text = await response.text(); const html = await debridRes.text();
const payload = asRecord(safeJson(text)); const code = html.match(/processDebrid\(\d+,'([^']+)',0\)/i)?.[1] || "";
if (!response.ok) { if (!code) {
return { ok: false, error: parseResponseError(response.status, text, payload) }; return { ok: false, error: "Mega-Web returned no processDebrid code" };
} }
const code = pickString(payload, ["response_code"]);
if (code && code.toLowerCase() !== "ok") { for (let attempt = 1; attempt <= 40; attempt += 1) {
return { ok: false, error: pickString(payload, ["response_text"]) || code }; const ajaxRes = await fetch("https://www.mega-debrid.eu/index.php?ajax=debrid&json", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0",
Cookie: megaCookie,
Referer: "https://www.mega-debrid.eu/index.php?page=debrideur&lang=de"
},
body: new URLSearchParams({ code, autodl: "0" })
});
const txt = (await ajaxRes.text()).trim();
if (txt === "reload") {
await new Promise((resolve) => setTimeout(resolve, 650));
continue;
}
if (txt === "false") {
return { ok: false, error: "Mega-Web returned false" };
}
const payload = safeJson(txt);
const direct = String(payload?.link || "");
if (!direct) {
const msg = String(payload?.text || txt || "Mega-Web no link");
if (/hoster does not respond correctly|could not be done for this moment/i.test(msg)) {
await new Promise((resolve) => setTimeout(resolve, 1200));
continue;
}
return { ok: false, error: msg };
}
return {
ok: true,
direct,
fileName: pickString(asRecord(payload), ["filename"]) || ""
};
} }
const direct = pickString(payload, ["debridLink", "download", "link"]); return { ok: false, error: "Mega-Web timeout while generating link" };
if (!direct) {
return { ok: false, error: "Mega-Debrid returned no debridLink" };
}
return {
ok: true,
direct,
fileName: pickString(payload, ["filename", "fileName"])
};
} }
async function callBestDebrid(link) { async function callBestDebrid(link) {
@ -196,7 +244,7 @@ async function main() {
if (rdToken) { if (rdToken) {
providers.push({ name: "Real-Debrid", run: callRealDebrid }); providers.push({ name: "Real-Debrid", run: callRealDebrid });
} }
if (megaToken) { if (megaLogin && megaPassword) {
providers.push({ name: "Mega-Debrid", run: callMegaDebrid }); providers.push({ name: "Mega-Debrid", run: callMegaDebrid });
} }
if (bestToken) { if (bestToken) {

View File

@ -3,7 +3,7 @@ import os from "node:os";
import { AppSettings } from "../shared/types"; import { AppSettings } from "../shared/types";
export const APP_NAME = "Debrid Download Manager"; export const APP_NAME = "Debrid Download Manager";
export const APP_VERSION = "1.1.21"; export const APP_VERSION = "1.1.22";
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0"; export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"; export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";

View File

@ -755,6 +755,19 @@ export class DownloadManager extends EventEmitter {
let windowBytes = 0; let windowBytes = 0;
let windowStarted = nowMs(); let windowStarted = nowMs();
const waitDrain = (): Promise<void> => new Promise((resolve, reject) => {
const onDrain = (): void => {
stream.off("error", onError);
resolve();
};
const onError = (error: Error): void => {
stream.off("drain", onDrain);
reject(error);
};
stream.once("drain", onDrain);
stream.once("error", onError);
});
try { try {
const body = response.body; const body = response.body;
if (!body) { if (!body) {
@ -784,7 +797,9 @@ export class DownloadManager extends EventEmitter {
const buffer = Buffer.from(chunk); const buffer = Buffer.from(chunk);
await this.applySpeedLimit(buffer.length, windowBytes, windowStarted); await this.applySpeedLimit(buffer.length, windowBytes, windowStarted);
stream.write(buffer); if (!stream.write(buffer)) {
await waitDrain();
}
written += buffer.length; written += buffer.length;
windowBytes += buffer.length; windowBytes += buffer.length;
this.session.totalDownloadedBytes += buffer.length; this.session.totalDownloadedBytes += buffer.length;
@ -806,8 +821,18 @@ export class DownloadManager extends EventEmitter {
this.emitState(); this.emitState();
} }
} finally { } finally {
await new Promise<void>((resolve) => { await new Promise<void>((resolve, reject) => {
stream.end(() => resolve()); const onFinish = (): void => {
stream.off("error", onError);
resolve();
};
const onError = (error: Error): void => {
stream.off("finish", onFinish);
reject(error);
};
stream.once("finish", onFinish);
stream.once("error", onError);
stream.end();
}); });
} }

View File

@ -83,8 +83,9 @@ async function hashFile(filePath: string, algorithm: "crc32" | "md5" | "sha1"):
const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 }); const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 });
return await new Promise<string>((resolve, reject) => { return await new Promise<string>((resolve, reject) => {
let crc = 0; let crc = 0;
stream.on("data", (chunk: Buffer) => { stream.on("data", (chunk: string | Buffer) => {
crc = crc32Buffer(chunk, crc); const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
crc = crc32Buffer(buffer, crc);
}); });
stream.on("error", reject); stream.on("error", reject);
stream.on("end", () => resolve(((crc >>> 0).toString(16)).padStart(8, "0").toLowerCase())); stream.on("end", () => resolve(((crc >>> 0).toString(16)).padStart(8, "0").toLowerCase()));

View File

@ -20,9 +20,23 @@ function normalizeLink(link: string): string {
return link.trim().toLowerCase(); return link.trim().toLowerCase();
} }
function parseSetCookie(raw: string): string { function parseSetCookieFromHeaders(headers: Headers): string {
const getSetCookie = (headers as unknown as { getSetCookie?: () => string[] }).getSetCookie;
if (typeof getSetCookie === "function") {
const values = getSetCookie.call(headers)
.map((entry) => entry.split(";")[0].trim())
.filter(Boolean);
if (values.length > 0) {
return values.join("; ");
}
}
const raw = headers.get("set-cookie") || "";
if (!raw) {
return "";
}
return raw return raw
.split(",") .split(/,(?=[^;=]+?=)/g)
.map((chunk) => chunk.split(";")[0].trim()) .map((chunk) => chunk.split(";")[0].trim())
.filter(Boolean) .filter(Boolean)
.join("; "); .join("; ");
@ -144,10 +158,25 @@ export class MegaWebFallback {
redirect: "manual" redirect: "manual"
}); });
const cookie = parseSetCookie(response.headers.get("set-cookie") || ""); const cookie = parseSetCookieFromHeaders(response.headers);
if (!cookie) { if (!cookie) {
throw new Error("Mega-Web Login liefert kein Session-Cookie"); throw new Error("Mega-Web Login liefert kein Session-Cookie");
} }
const verify = await fetch(DEBRID_REFERER, {
method: "GET",
headers: {
"User-Agent": "Mozilla/5.0",
Cookie: cookie,
Referer: DEBRID_REFERER
}
});
const verifyHtml = await verify.text();
const hasDebridForm = /id=["']debridForm["']/i.test(verifyHtml) || /name=["']links["']/i.test(verifyHtml);
if (!hasDebridForm) {
throw new Error("Mega-Web Login ungültig oder Session blockiert");
}
this.cookie = cookie; this.cookie = cookie;
this.cookieSetAt = Date.now(); this.cookieSetAt = Date.now();
} }

View File

@ -170,26 +170,36 @@ export function App(): ReactElement {
}; };
const onAddLinks = async (): Promise<void> => { const onAddLinks = async (): Promise<void> => {
await window.rd.updateSettings(settingsDraft); try {
const result = await window.rd.addLinks({ rawText: linksRaw, packageName: settingsDraft.packageName }); await window.rd.updateSettings(settingsDraft);
if (result.addedLinks > 0) { const result = await window.rd.addLinks({ rawText: linksRaw, packageName: settingsDraft.packageName });
setStatusToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`); if (result.addedLinks > 0) {
setLinksRaw(""); setStatusToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`);
} else { setLinksRaw("");
setStatusToast("Keine gültigen Links gefunden"); } else {
setStatusToast("Keine gültigen Links gefunden");
}
setTimeout(() => setStatusToast(""), 2200);
} catch (error) {
setStatusToast(`Fehler beim Hinzufügen: ${String(error)}`);
setTimeout(() => setStatusToast(""), 2600);
} }
setTimeout(() => setStatusToast(""), 2200);
}; };
const onImportDlc = async (): Promise<void> => { const onImportDlc = async (): Promise<void> => {
const files = await window.rd.pickContainers(); try {
if (files.length === 0) { const files = await window.rd.pickContainers();
return; if (files.length === 0) {
return;
}
await window.rd.updateSettings(settingsDraft);
const result = await window.rd.addContainers(files);
setStatusToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
setTimeout(() => setStatusToast(""), 2200);
} catch (error) {
setStatusToast(`Fehler beim DLC-Import: ${String(error)}`);
setTimeout(() => setStatusToast(""), 2600);
} }
await window.rd.updateSettings(settingsDraft);
const result = await window.rd.addContainers(files);
setStatusToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
setTimeout(() => setStatusToast(""), 2200);
}; };
const onDrop = async (event: DragEvent<HTMLTextAreaElement>): Promise<void> => { const onDrop = async (event: DragEvent<HTMLTextAreaElement>): Promise<void> => {
@ -202,10 +212,15 @@ export function App(): ReactElement {
if (dlc.length === 0) { if (dlc.length === 0) {
return; return;
} }
await window.rd.updateSettings(settingsDraft); try {
const result = await window.rd.addContainers(dlc); await window.rd.updateSettings(settingsDraft);
setStatusToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`); const result = await window.rd.addContainers(dlc);
setTimeout(() => setStatusToast(""), 2200); setStatusToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
setTimeout(() => setStatusToast(""), 2200);
} catch (error) {
setStatusToast(`Fehler bei Drag-and-Drop: ${String(error)}`);
setTimeout(() => setStatusToast(""), 2600);
}
}; };
const setBool = (key: keyof AppSettings, value: boolean): void => { const setBool = (key: keyof AppSettings, value: boolean): void => {
@ -220,6 +235,15 @@ export function App(): ReactElement {
setSettingsDraft((prev: AppSettings) => ({ ...prev, [key]: value })); setSettingsDraft((prev: AppSettings) => ({ ...prev, [key]: value }));
}; };
const performQuickAction = async (action: () => Promise<unknown>): Promise<void> => {
try {
await action();
} catch (error) {
setStatusToast(`Fehler: ${String(error)}`);
setTimeout(() => setStatusToast(""), 2600);
}
};
return ( return (
<div className="app-shell"> <div className="app-shell">
<header className="top-header"> <header className="top-header">
@ -239,15 +263,17 @@ export function App(): ReactElement {
className="btn accent" className="btn accent"
disabled={!snapshot.canStart} disabled={!snapshot.canStart}
onClick={async () => { onClick={async () => {
await window.rd.updateSettings(settingsDraft); await performQuickAction(async () => {
await window.rd.start(); await window.rd.updateSettings(settingsDraft);
await window.rd.start();
});
}} }}
>Start</button> >Start</button>
<button className="btn" disabled={!snapshot.canPause} onClick={() => window.rd.togglePause()}> <button className="btn" disabled={!snapshot.canPause} onClick={() => { void performQuickAction(() => window.rd.togglePause()); }}>
{snapshot.session.paused ? "Resume" : "Pause"} {snapshot.session.paused ? "Resume" : "Pause"}
</button> </button>
<button className="btn" disabled={!snapshot.canStop} onClick={() => window.rd.stop()}>Stop</button> <button className="btn" disabled={!snapshot.canStop} onClick={() => { void performQuickAction(() => window.rd.stop()); }}>Stop</button>
<button className="btn" onClick={() => window.rd.clearAll()}>Alles leeren</button> <button className="btn" onClick={() => { void performQuickAction(() => window.rd.clearAll()); }}>Alles leeren</button>
</div> </div>
<div className="speed-config"> <div className="speed-config">
<label> <label>
@ -310,7 +336,7 @@ export function App(): ReactElement {
key={pkg.id} key={pkg.id}
pkg={pkg} pkg={pkg}
items={pkg.itemIds.map((id) => snapshot.session.items[id]).filter(Boolean)} items={pkg.itemIds.map((id) => snapshot.session.items[id]).filter(Boolean)}
onCancel={() => window.rd.cancelPackage(pkg.id)} onCancel={() => { void performQuickAction(() => window.rd.cancelPackage(pkg.id)); }}
/> />
))} ))}
</section> </section>