diff --git a/package-lock.json b/package-lock.json index 5bfe3ea..08861a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.1.17", + "version": "1.1.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.1.17", + "version": "1.1.18", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index 5e75634..bed5ce5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.1.17", + "version": "1.1.18", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/constants.ts b/src/main/constants.ts index ec01871..fe57b43 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.17"; +export const APP_VERSION = "1.1.18"; 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/debrid.ts b/src/main/debrid.ts index 9b6cd14..8e6574f 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -1,4 +1,5 @@ import { AppSettings, DebridProvider } from "../shared/types"; +import { createHash } from "node:crypto"; import { REQUEST_RETRIES } from "./constants"; import { RealDebridClient, UnrestrictedLink } from "./realdebrid"; import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils"; @@ -194,49 +195,109 @@ class MegaDebridClient { this.token = token; } + private normalizeMegaCandidates(link: string): string[] { + const result = new Set(); + const trimmed = link.trim(); + if (trimmed) { + result.add(trimmed); + } + + try { + const parsed = new URL(trimmed); + const host = parsed.hostname.toLowerCase(); + if (host.includes("rapidgator.net")) { + const parts = parsed.pathname.split("/").filter(Boolean); + const fileIdx = parts.findIndex((part) => part.toLowerCase() === "file"); + if (fileIdx >= 0 && parts[fileIdx + 1]) { + const hash = parts[fileIdx + 1]; + result.add(`https://rapidgator.net/file/${hash}`); + result.add(`http://rapidgator.net/file/${hash}`); + if (parts[fileIdx + 2]) { + const name = parts[fileIdx + 2].replace(/\.html$/i, ""); + result.add(`https://rapidgator.net/file/${hash}/${name}.html`); + result.add(`http://rapidgator.net/file/${hash}/${name}.html`); + } + } + } + } catch { + // ignore malformed URL + } + + return [...result]; + } + + private async requestMega(link: string, includePasswordField: boolean, useGetLinkParam: boolean): Promise { + const url = `${MEGA_DEBRID_API}?action=getLink&token=${encodeURIComponent(this.token)}${useGetLinkParam ? `&link=${encodeURIComponent(link)}` : ""}`; + const body = new URLSearchParams(); + if (!useGetLinkParam) { + body.set("link", link); + } + if (includePasswordField) { + body.set("password", createHash("md5").update("").digest("hex")); + } + + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "RD-Node-Downloader/1.1.17" + }, + body + }); + const text = await response.text(); + const payload = asRecord(parseJson(text)); + + if (!response.ok) { + throw new Error(parseError(response.status, text, payload)); + } + + const responseCode = pickString(payload, ["response_code"]); + if (responseCode && responseCode.toLowerCase() !== "ok") { + throw new Error(pickString(payload, ["response_text"]) || responseCode); + } + + const directUrl = pickString(payload, ["debridLink", "download", "link"]); + if (!directUrl) { + throw new Error("Mega-Debrid Antwort ohne debridLink"); + } + + const fileName = pickString(payload, ["filename", "fileName"]) || filenameFromUrl(link); + const fileSize = pickNumber(payload, ["filesize", "size"]); + return { + fileName, + directUrl, + fileSize, + retriesUsed: 0 + }; + } + public async unrestrictLink(link: string): Promise { let lastError = ""; for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) { try { - const body = new URLSearchParams({ link }); - const response = await fetch(`${MEGA_DEBRID_API}?action=getLink&token=${encodeURIComponent(this.token)}`, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "RD-Node-Downloader/1.1.12" - }, - body - }); - const text = await response.text(); - const payload = asRecord(parseJson(text)); + const candidates = this.normalizeMegaCandidates(link); + const variants = [ + { includePasswordField: false, useGetLinkParam: false }, + { includePasswordField: true, useGetLinkParam: false }, + { includePasswordField: false, useGetLinkParam: true }, + { includePasswordField: true, useGetLinkParam: true } + ]; - if (!response.ok) { - const reason = parseError(response.status, text, payload); - if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) { - await sleep(retryDelay(attempt)); - continue; + for (const candidate of candidates) { + for (const variant of variants) { + try { + const out = await this.requestMega(candidate, variant.includePasswordField, variant.useGetLinkParam); + out.retriesUsed = attempt - 1; + return out; + } catch (error) { + lastError = compactErrorText(error); + } } - throw new Error(reason); } - const responseCode = pickString(payload, ["response_code"]); - if (responseCode && responseCode.toLowerCase() !== "ok") { - throw new Error(pickString(payload, ["response_text"]) || responseCode); + if (/token error|vip_end/i.test(lastError)) { + throw new Error(lastError); } - - const directUrl = pickString(payload, ["debridLink", "download", "link"]); - if (!directUrl) { - throw new Error("Mega-Debrid Antwort ohne debridLink"); - } - - const fileName = pickString(payload, ["filename", "fileName"]) || filenameFromUrl(link); - const fileSize = pickNumber(payload, ["filesize", "size"]); - return { - fileName, - directUrl, - fileSize, - retriesUsed: attempt - 1 - }; } catch (error) { lastError = compactErrorText(error); if (attempt >= REQUEST_RETRIES) { diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 6f14c28..e1c94b5 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -143,6 +143,45 @@ describe("debrid service", () => { expect(result.fileSize).toBe(4096); }); + it("retries Mega-Debrid with alternate request variants", async () => { + const settings = { + ...defaultSettings(), + token: "", + megaToken: "mega-token", + bestToken: "", + allDebridToken: "", + providerPrimary: "megadebrid" as const, + providerSecondary: "megadebrid" as const, + providerTertiary: "megadebrid" as const, + autoProviderFallback: true + }; + + let calls = 0; + globalThis.fetch = (async (input: RequestInfo | URL): Promise => { + calls += 1; + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("mega-debrid.eu/api.php?action=getLink") && !url.includes("&link=")) { + return new Response(JSON.stringify({ response_code: "UNRESTRICTING_ERROR_1", response_text: "UNRESTRICTING_ERROR_1" }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + } + if (url.includes("mega-debrid.eu/api.php?action=getLink") && url.includes("&link=")) { + return new Response(JSON.stringify({ response_code: "ok", debridLink: "https://mega.example/file2.bin", filename: "file2.bin" }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + } + return new Response("not-found", { status: 404 }); + }) as typeof fetch; + + const service = new DebridService(settings); + const result = await service.unrestrictLink("https://rapidgator.net/file/abc/name.part1.rar.html"); + expect(result.provider).toBe("megadebrid"); + expect(result.fileName).toBe("file2.bin"); + expect(calls).toBeGreaterThan(1); + }); + it("respects provider selection and does not append hidden fallback providers", async () => { const settings = { ...defaultSettings(),