From 0de5a59a64885fff9a7393ba084c6b5571d35bac Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 27 Feb 2026 14:45:42 +0100 Subject: [PATCH] Stream filename scan updates and add provider fallback in v1.3.5 --- package.json | 2 +- src/main/debrid.ts | 35 +++++++++++++++++----- src/main/download-manager.ts | 27 ++++++++++------- tests/debrid.test.ts | 57 ++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index b61e46d..a901917 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.3.4", + "version": "1.3.5", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/debrid.ts b/src/main/debrid.ts index c7b41b8..20b9cbb 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -551,21 +551,34 @@ export class DebridService { this.allDebridClient = new AllDebridClient(next.allDebridToken); } - public async resolveFilenames(links: string[]): Promise> { + public async resolveFilenames( + links: string[], + onResolved?: (link: string, fileName: string) => void + ): Promise> { const unresolved = links.filter((link) => looksLikeOpaqueFilename(filenameFromUrl(link))); if (unresolved.length === 0) { return new Map(); } const clean = new Map(); + const reportResolved = (link: string, fileName: string): void => { + const normalized = fileName.trim(); + if (!normalized || looksLikeOpaqueFilename(normalized) || normalized.toLowerCase() === "download.bin") { + return; + } + if (clean.get(link) === normalized) { + return; + } + clean.set(link, normalized); + onResolved?.(link, normalized); + }; + const token = this.settings.allDebridToken.trim(); if (token) { try { const infos = await this.allDebridClient.getLinkInfos(unresolved); for (const [link, fileName] of infos.entries()) { - if (fileName.trim() && !looksLikeOpaqueFilename(fileName.trim())) { - clean.set(link, fileName.trim()); - } + reportResolved(link, fileName); } } catch { // ignore and continue with host page fallback @@ -573,10 +586,18 @@ export class DebridService { } const remaining = unresolved.filter((link) => !clean.has(link) && isRapidgatorLink(link)); - await runWithConcurrency(remaining, 3, async (link) => { + await runWithConcurrency(remaining, 6, async (link) => { const fromPage = await resolveRapidgatorFilename(link); - if (fromPage && !looksLikeOpaqueFilename(fromPage)) { - clean.set(link, fromPage); + reportResolved(link, fromPage); + }); + + const stillUnresolved = unresolved.filter((link) => !clean.has(link)); + await runWithConcurrency(stillUnresolved, 4, async (link) => { + try { + const unrestricted = await this.unrestrictLink(link); + reportResolved(link, unrestricted.fileName || ""); + } catch { + // ignore final fallback errors } }); diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index dd906c8..f579036 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -497,22 +497,21 @@ export class DownloadManager extends EventEmitter { private async resolveQueuedFilenames(unresolvedByLink: Map): Promise { try { - const resolved = await this.debridService.resolveFilenames(Array.from(unresolvedByLink.keys())); - if (resolved.size === 0) { - return; - } - let changed = false; - for (const [link, itemIds] of unresolvedByLink.entries()) { - const fileName = resolved.get(link); + const applyResolvedName = (link: string, fileName: string): void => { + const itemIds = unresolvedByLink.get(link); + if (!itemIds || itemIds.length === 0) { + return; + } if (!fileName || fileName.toLowerCase() === "download.bin") { - continue; + return; } const normalized = sanitizeFilename(fileName); if (!normalized || normalized.toLowerCase() === "download.bin") { - continue; + return; } + let changedForLink = false; for (const itemId of itemIds) { const item = this.session.items[itemId]; if (!item) { @@ -528,8 +527,16 @@ export class DownloadManager extends EventEmitter { item.targetPath = path.join(this.session.packages[item.packageId]?.outputDir || this.settings.outputDir, normalized); item.updatedAt = nowMs(); changed = true; + changedForLink = true; } - } + + if (changedForLink) { + this.persistSoon(); + this.emitState(); + } + }; + + await this.debridService.resolveFilenames(Array.from(unresolvedByLink.keys()), applyResolvedName); if (changed) { this.persistSoon(); diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 3a65f00..e4f8d63 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -310,4 +310,61 @@ describe("debrid service", () => { const resolved = await service.resolveFilenames([link]); expect(resolved.get(link)).toBe("Bulletproof.S01E01.German.DL.DD20.Synced.720p.AmazonHD.h264-GDR.part01.rar"); }); + + it("falls back to provider unrestrict for unresolved filename scan", async () => { + const settings = { + ...defaultSettings(), + token: "rd-token", + providerPrimary: "realdebrid" as const, + providerSecondary: "none" as const, + providerTertiary: "none" as const, + autoProviderFallback: true, + allDebridToken: "" + }; + + const linkFromPage = "https://rapidgator.net/file/11111111111111111111111111111111"; + const linkFromProvider = "https://hoster.example/file/22222222222222222222222222222222"; + + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + + if (url === linkFromPage) { + return new Response("Download file from-page.part1.rar", { + status: 200, + headers: { "Content-Type": "text/html" } + }); + } + + if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) { + const body = init?.body; + const bodyText = body instanceof URLSearchParams ? body.toString() : String(body || ""); + const linkValue = new URLSearchParams(bodyText).get("link") || ""; + if (linkValue === linkFromProvider) { + return new Response(JSON.stringify({ + download: "https://cdn.example/from-provider", + filename: "from-provider.part2.rar", + filesize: 1024 + }), { + status: 200, + headers: { "Content-Type": "application/json" } + }); + } + } + + return new Response("not-found", { status: 404 }); + }) as typeof fetch; + + const service = new DebridService(settings); + const events: Array<{ link: string; fileName: string }> = []; + const resolved = await service.resolveFilenames([linkFromPage, linkFromProvider], (link, fileName) => { + events.push({ link, fileName }); + }); + + expect(resolved.get(linkFromPage)).toBe("from-page.part1.rar"); + expect(resolved.get(linkFromProvider)).toBe("from-provider.part2.rar"); + expect(events).toEqual(expect.arrayContaining([ + { link: linkFromPage, fileName: "from-page.part1.rar" }, + { link: linkFromProvider, fileName: "from-provider.part2.rar" } + ])); + }); });