From 09bc354c18f75a11d02ca270e1e52445ee1b0d96 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Mon, 2 Mar 2026 15:28:23 +0100 Subject: [PATCH] Detect dead links as permanent errors, fix last-episode extraction race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dead link detection: - Mega-Web: parse hoster error messages (hosterNotAvailable, etc.) from HTML and throw specific error instead of returning null - MegaDebridClient: stop retrying on permanent hoster errors - download-manager: isPermanentLinkError() immediately fails items with dead links instead of retrying forever Extraction race condition: - package_done cleanup policy checked if all items were "completed" (downloaded) but not if they were "extracted" — removing the package before the last episode could be extracted - Both applyCompletedCleanupPolicy and applyPackageDoneCleanup now guard against premature removal when autoExtract is enabled Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/debrid.ts | 4 +++ src/main/download-manager.ts | 50 +++++++++++++++++++++++++++++++++++ src/main/mega-web-fallback.ts | 47 ++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f53af75..4334c3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.89", + "version": "1.4.90", "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 c85c559..b3cbbc2 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -505,6 +505,10 @@ class MegaDebridClient { if (!lastError) { lastError = web ? "Mega-Web Antwort ohne Download-Link" : "Mega-Web Antwort leer"; } + // Don't retry permanent hoster errors (dead link, file removed, etc.) + if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(lastError)) { + break; + } if (attempt < REQUEST_RETRIES) { await sleepWithSignal(retryDelay(attempt), signal); } diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 96818b5..d8f3683 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -213,6 +213,20 @@ function isFetchFailure(errorText: string): boolean { return text.includes("fetch failed") || text.includes("socket hang up") || text.includes("econnreset") || text.includes("network error"); } +function isPermanentLinkError(errorText: string): boolean { + const text = String(errorText || "").toLowerCase(); + return text.includes("permanent ungültig") + || text.includes("hosternotavailable") + || /file.?not.?found/.test(text) + || /file.?unavailable/.test(text) + || /link.?is.?dead/.test(text) + || text.includes("file has been removed") + || text.includes("file has been deleted") + || text.includes("file is no longer available") + || text.includes("file was removed") + || text.includes("file was deleted"); +} + function isUnrestrictFailure(errorText: string): boolean { const text = String(errorText || "").toLowerCase(); return text.includes("unrestrict") || text.includes("mega-web") || text.includes("mega-debrid") @@ -3780,6 +3794,21 @@ export class DownloadManager extends EventEmitter { return; } + // Permanent link errors (dead link, file removed, hoster unavailable) → fail immediately + if (isPermanentLinkError(errorText)) { + logger.error(`Link permanent ungültig: item=${item.fileName || item.id}, error=${errorText}, link=${item.url.slice(0, 80)}`); + item.status = "failed"; + this.recordRunOutcome(item.id, "failed"); + item.lastError = errorText; + item.fullStatus = `Link ungültig: ${errorText}`; + item.speedBps = 0; + item.updatedAt = nowMs(); + this.retryStateByItem.delete(item.id); + this.persistSoon(); + this.emitState(); + return; + } + if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) { active.unrestrictRetries += 1; item.retries += 1; @@ -5252,6 +5281,17 @@ export class DownloadManager extends EventEmitter { return; } + // With autoExtract: only remove once ALL items are extracted, not just downloaded + if (this.settings.autoExtract) { + const allExtracted = pkg.itemIds.every((itemId) => { + const item = this.session.items[itemId]; + return !item || isExtractedLabel(item.fullStatus || ""); + }); + if (!allExtracted) { + return; + } + } + this.removePackageFromSession(packageId, [...pkg.itemIds]); } @@ -5292,6 +5332,16 @@ export class DownloadManager extends EventEmitter { return item != null && item.status !== "completed"; }); if (!hasOpen) { + // With autoExtract: only remove once ALL items are extracted, not just downloaded + if (this.settings.autoExtract) { + const allExtracted = pkg.itemIds.every((id) => { + const item = this.session.items[id]; + return !item || isExtractedLabel(item.fullStatus || ""); + }); + if (!allExtracted) { + return; + } + } this.removePackageFromSession(packageId, [...pkg.itemIds]); } } diff --git a/src/main/mega-web-fallback.ts b/src/main/mega-web-fallback.ts index 89700d2..f9c1b04 100644 --- a/src/main/mega-web-fallback.ts +++ b/src/main/mega-web-fallback.ts @@ -42,6 +42,45 @@ function parseSetCookieFromHeaders(headers: Headers): string { .join("; "); } +const PERMANENT_HOSTER_ERRORS = [ + "hosternotavailable", + "filenotfound", + "file_unavailable", + "file not found", + "link is dead", + "file has been removed", + "file has been deleted", + "file was deleted", + "file was removed", + "not available", + "file is no longer available" +]; + +function parsePageErrors(html: string): string[] { + const errors: string[] = []; + const errorRegex = /class=["'][^"']*\berror\b[^"']*["'][^>]*>([^<]+)]*class=['"][^'"]*acp-box[^'"]*['"][^>]*>[\s\S]*?<\/div>/gi; @@ -306,6 +345,14 @@ export class MegaWebFallback { }); const html = await page.text(); + + // Check for permanent hoster errors before looking for debrid codes + const pageErrors = parsePageErrors(html); + const permanentError = isPermanentHosterError(pageErrors); + if (permanentError) { + throw new Error(`Mega-Web: Link permanent ungültig (${permanentError})`); + } + const code = pickCode(parseCodes(html), link); if (!code) { return null;