From 722fe071cc707a6f19edda741fad321761244dc8 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Tue, 10 Mar 2026 18:27:26 +0100 Subject: [PATCH] Harden Debrid-Link completion recovery --- src/main/download-manager.ts | 6 +++ tests/download-manager.test.ts | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index be6750e..b697e57 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -7778,6 +7778,12 @@ export class DownloadManager extends EventEmitter { const directLinkRetryMatch = errorText.match(/^(?:Error:\s*)?direct_link_retry_exhausted:(.+)$/); if (directLinkRetryMatch) { const exhaustedReason = compactErrorText(directLinkRetryMatch[1] || errorText).replace(/^Error:\s*/i, ""); + if (item.provider === "debridlink") { + await sleep(450); + if (this.tryFinalizeItemFromDisk(pkg, item, "DebridLink-Settle-Recovery", exhaustedReason)) { + return; + } + } if (isHttp416Text(exhaustedReason) && active.genericErrorRetries < maxHttp416Retries) { this.scheduleHttp416Retry(item, active, retryDisplayLimit, exhaustedReason, claimedTargetPath); this.persistSoon(); diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 5a2996d..0a39c6b 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -1564,6 +1564,73 @@ describe("download manager", () => { expect(fs.statSync(item.targetPath).size).toBe(binary.length); }); + it("finalizes Debrid-Link items in-session when the file is already complete on disk", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); + tempDirs.push(root); + const binary = Buffer.alloc(60 * 1024, 57); + let unrestrictCalls = 0; + let downloadCalls = 0; + + globalThis.fetch = async (input: RequestInfo | URL): Promise => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("debrid-link.com/api/v2/downloader/add")) { + unrestrictCalls += 1; + return new Response( + JSON.stringify({ + success: true, + value: { + downloadUrl: `https://dummy/debridlink-direct-complete-${unrestrictCalls}`, + name: "debridlink-complete.part10.rar", + size: binary.length + } + }), + { + status: 200, + headers: { "Content-Type": "application/json" } + } + ); + } + throw new Error(`unexpected fetch ${url}`); + }; + + const manager = new DownloadManager( + { + ...defaultSettings(), + debridLinkApiKeys: "dl-test-key", + providerOrder: ["debridlink"], + providerPrimary: "debridlink", + providerSecondary: "none", + providerTertiary: "none", + outputDir: path.join(root, "downloads"), + extractDir: path.join(root, "extract"), + retryLimit: 2, + autoExtract: false + }, + emptySession(), + createStoragePaths(path.join(root, "state")) + ); + + (manager as any).downloadToFile = async (_active: unknown, _directUrl: string, targetPath: string) => { + downloadCalls += 1; + fs.mkdirSync(path.dirname(targetPath), { recursive: true }); + fs.writeFileSync(targetPath, binary); + throw new Error("direct_link_retry_exhausted:download_underflow:61440/61440"); + }; + + manager.addPackages([{ name: "debridlink-complete", links: ["https://dummy/debridlink-complete"] }]); + await manager.start(); + await waitFor(() => !manager.getSnapshot().session.running, 12000); + + const item = Object.values(manager.getSnapshot().session.items)[0]; + expect(item?.status).toBe("completed"); + expect(item?.progressPercent).toBe(100); + expect(item?.downloadedBytes).toBe(binary.length); + expect(unrestrictCalls).toBe(1); + expect(downloadCalls).toBe(1); + expect(fs.existsSync(item.targetPath)).toBe(true); + expect(fs.statSync(item.targetPath).size).toBe(binary.length); + }); + it("queues Debrid-Link cooldown retries when wrapped unrestrict errors carry the cooldown marker", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); tempDirs.push(root);