Add Mega-Debrid unrestrict workarounds and bump to 1.1.18
Some checks are pending
Build and Release / build (push) Waiting to run

This commit is contained in:
Sucukdeluxe 2026-02-27 05:06:05 +01:00
parent 7ac61ce64a
commit 704826b421
5 changed files with 138 additions and 38 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.17", "version": "1.1.18",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.17", "version": "1.1.18",
"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.17", "version": "1.1.18",
"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

@ -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.17"; export const APP_VERSION = "1.1.18";
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

@ -1,4 +1,5 @@
import { AppSettings, DebridProvider } from "../shared/types"; import { AppSettings, DebridProvider } from "../shared/types";
import { createHash } from "node:crypto";
import { REQUEST_RETRIES } from "./constants"; import { REQUEST_RETRIES } from "./constants";
import { RealDebridClient, UnrestrictedLink } from "./realdebrid"; import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils"; import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils";
@ -194,49 +195,109 @@ class MegaDebridClient {
this.token = token; this.token = token;
} }
private normalizeMegaCandidates(link: string): string[] {
const result = new Set<string>();
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<UnrestrictedLink> {
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<UnrestrictedLink> { public async unrestrictLink(link: string): Promise<UnrestrictedLink> {
let lastError = ""; let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) { for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
try { try {
const body = new URLSearchParams({ link }); const candidates = this.normalizeMegaCandidates(link);
const response = await fetch(`${MEGA_DEBRID_API}?action=getLink&token=${encodeURIComponent(this.token)}`, { const variants = [
method: "POST", { includePasswordField: false, useGetLinkParam: false },
headers: { { includePasswordField: true, useGetLinkParam: false },
"Content-Type": "application/x-www-form-urlencoded", { includePasswordField: false, useGetLinkParam: true },
"User-Agent": "RD-Node-Downloader/1.1.12" { includePasswordField: true, useGetLinkParam: true }
}, ];
body
});
const text = await response.text();
const payload = asRecord(parseJson(text));
if (!response.ok) { for (const candidate of candidates) {
const reason = parseError(response.status, text, payload); for (const variant of variants) {
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) { try {
await sleep(retryDelay(attempt)); const out = await this.requestMega(candidate, variant.includePasswordField, variant.useGetLinkParam);
continue; out.retriesUsed = attempt - 1;
return out;
} catch (error) {
lastError = compactErrorText(error);
}
} }
throw new Error(reason);
} }
const responseCode = pickString(payload, ["response_code"]); if (/token error|vip_end/i.test(lastError)) {
if (responseCode && responseCode.toLowerCase() !== "ok") { throw new Error(lastError);
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: attempt - 1
};
} catch (error) { } catch (error) {
lastError = compactErrorText(error); lastError = compactErrorText(error);
if (attempt >= REQUEST_RETRIES) { if (attempt >= REQUEST_RETRIES) {

View File

@ -143,6 +143,45 @@ describe("debrid service", () => {
expect(result.fileSize).toBe(4096); 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<Response> => {
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 () => { it("respects provider selection and does not append hidden fallback providers", async () => {
const settings = { const settings = {
...defaultSettings(), ...defaultSettings(),