diff --git a/package-lock.json b/package-lock.json index e5b3fa7..e5d87c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.1.21", + "version": "1.1.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.1.21", + "version": "1.1.22", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index 688a96e..9940c47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.1.21", + "version": "1.1.22", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/scripts/debrid_service_smoke.ts b/scripts/debrid_service_smoke.ts index 6d23470..1e60515 100644 --- a/scripts/debrid_service_smoke.ts +++ b/scripts/debrid_service_smoke.ts @@ -1,5 +1,6 @@ import { DebridService } from "../src/main/debrid"; import { defaultSettings } from "../src/main/constants"; +import { MegaWebFallback } from "../src/main/mega-web-fallback"; const links = [ "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 { - 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) { try { const result = await service.unrestrictLink(link); @@ -35,6 +42,7 @@ async function main(): Promise { console.log(`[FAIL] ${String(error)}`); } } + megaWeb.dispose(); } void main(); diff --git a/scripts/provider_smoke_check.mjs b/scripts/provider_smoke_check.mjs index 6b019e0..3ce78a1 100644 --- a/scripts/provider_smoke_check.mjs +++ b/scripts/provider_smoke_check.mjs @@ -5,12 +5,14 @@ const RAPIDGATOR_LINKS = [ ]; 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 allDebridToken = process.env.ALLDEBRID_TOKEN || ""; +let megaCookie = ""; -if (!rdToken && !megaToken && !bestToken && !allDebridToken) { - console.error("No provider token configured. Set RD_TOKEN and/or MEGA_TOKEN and/or BEST_TOKEN and/or ALLDEBRID_TOKEN."); +if (!rdToken && !(megaLogin && megaPassword) && !bestToken && !allDebridToken) { + 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); } @@ -65,32 +67,78 @@ async function callRealDebrid(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", headers: { "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 payload = asRecord(safeJson(text)); - if (!response.ok) { - return { ok: false, error: parseResponseError(response.status, text, payload) }; + const html = await debridRes.text(); + const code = html.match(/processDebrid\(\d+,'([^']+)',0\)/i)?.[1] || ""; + if (!code) { + return { ok: false, error: "Mega-Web returned no processDebrid code" }; } - const code = pickString(payload, ["response_code"]); - if (code && code.toLowerCase() !== "ok") { - return { ok: false, error: pickString(payload, ["response_text"]) || code }; + + for (let attempt = 1; attempt <= 40; attempt += 1) { + 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"]); - if (!direct) { - return { ok: false, error: "Mega-Debrid returned no debridLink" }; - } - return { - ok: true, - direct, - fileName: pickString(payload, ["filename", "fileName"]) - }; + return { ok: false, error: "Mega-Web timeout while generating link" }; } async function callBestDebrid(link) { @@ -196,7 +244,7 @@ async function main() { if (rdToken) { providers.push({ name: "Real-Debrid", run: callRealDebrid }); } - if (megaToken) { + if (megaLogin && megaPassword) { providers.push({ name: "Mega-Debrid", run: callMegaDebrid }); } if (bestToken) { diff --git a/src/main/constants.ts b/src/main/constants.ts index 4e7771f..c9b490f 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -3,7 +3,7 @@ import os from "node:os"; import { AppSettings } from "../shared/types"; 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 DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"; diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 525c89d..75e34be 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -755,6 +755,19 @@ export class DownloadManager extends EventEmitter { let windowBytes = 0; let windowStarted = nowMs(); + const waitDrain = (): Promise => 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 { const body = response.body; if (!body) { @@ -784,7 +797,9 @@ export class DownloadManager extends EventEmitter { const buffer = Buffer.from(chunk); await this.applySpeedLimit(buffer.length, windowBytes, windowStarted); - stream.write(buffer); + if (!stream.write(buffer)) { + await waitDrain(); + } written += buffer.length; windowBytes += buffer.length; this.session.totalDownloadedBytes += buffer.length; @@ -806,8 +821,18 @@ export class DownloadManager extends EventEmitter { this.emitState(); } } finally { - await new Promise((resolve) => { - stream.end(() => resolve()); + await new Promise((resolve, reject) => { + 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(); }); } diff --git a/src/main/integrity.ts b/src/main/integrity.ts index 1de12aa..9c5a849 100644 --- a/src/main/integrity.ts +++ b/src/main/integrity.ts @@ -83,8 +83,9 @@ async function hashFile(filePath: string, algorithm: "crc32" | "md5" | "sha1"): const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 }); return await new Promise((resolve, reject) => { let crc = 0; - stream.on("data", (chunk: Buffer) => { - crc = crc32Buffer(chunk, crc); + stream.on("data", (chunk: string | Buffer) => { + const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk; + crc = crc32Buffer(buffer, crc); }); stream.on("error", reject); stream.on("end", () => resolve(((crc >>> 0).toString(16)).padStart(8, "0").toLowerCase())); diff --git a/src/main/mega-web-fallback.ts b/src/main/mega-web-fallback.ts index 2790bee..2462546 100644 --- a/src/main/mega-web-fallback.ts +++ b/src/main/mega-web-fallback.ts @@ -20,9 +20,23 @@ function normalizeLink(link: string): string { 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 - .split(",") + .split(/,(?=[^;=]+?=)/g) .map((chunk) => chunk.split(";")[0].trim()) .filter(Boolean) .join("; "); @@ -144,10 +158,25 @@ export class MegaWebFallback { redirect: "manual" }); - const cookie = parseSetCookie(response.headers.get("set-cookie") || ""); + const cookie = parseSetCookieFromHeaders(response.headers); if (!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.cookieSetAt = Date.now(); } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 8bddbe9..1adc015 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -170,26 +170,36 @@ export function App(): ReactElement { }; const onAddLinks = async (): Promise => { - await window.rd.updateSettings(settingsDraft); - const result = await window.rd.addLinks({ rawText: linksRaw, packageName: settingsDraft.packageName }); - if (result.addedLinks > 0) { - setStatusToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`); - setLinksRaw(""); - } else { - setStatusToast("Keine gültigen Links gefunden"); + try { + await window.rd.updateSettings(settingsDraft); + const result = await window.rd.addLinks({ rawText: linksRaw, packageName: settingsDraft.packageName }); + if (result.addedLinks > 0) { + setStatusToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`); + setLinksRaw(""); + } 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 => { - const files = await window.rd.pickContainers(); - if (files.length === 0) { - return; + try { + const files = await window.rd.pickContainers(); + 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): Promise => { @@ -202,10 +212,15 @@ export function App(): ReactElement { if (dlc.length === 0) { return; } - await window.rd.updateSettings(settingsDraft); - const result = await window.rd.addContainers(dlc); - setStatusToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`); - setTimeout(() => setStatusToast(""), 2200); + try { + await window.rd.updateSettings(settingsDraft); + const result = await window.rd.addContainers(dlc); + 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 => { @@ -220,6 +235,15 @@ export function App(): ReactElement { setSettingsDraft((prev: AppSettings) => ({ ...prev, [key]: value })); }; + const performQuickAction = async (action: () => Promise): Promise => { + try { + await action(); + } catch (error) { + setStatusToast(`Fehler: ${String(error)}`); + setTimeout(() => setStatusToast(""), 2600); + } + }; + return (
@@ -239,15 +263,17 @@ export function App(): ReactElement { className="btn accent" disabled={!snapshot.canStart} onClick={async () => { - await window.rd.updateSettings(settingsDraft); - await window.rd.start(); + await performQuickAction(async () => { + await window.rd.updateSettings(settingsDraft); + await window.rd.start(); + }); }} >Start - - - + +