From 938b84392d1ed1de2f4d7d3acab07702959776af Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 17 Jun 2026 00:23:50 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Tote=20Mega-Debrid-Links=20(frz.=20"Fich?= =?UTF-8?q?ier=20supprim=C3=A9")=20vergiften=20nicht=20mehr=20die=20ganze?= =?UTF-8?q?=20Queue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mega-Debrid liefert bei geloeschten Hoster-Dateien franzoesische Fehlertexte ("Fichier supprime chez l'hebergeur"). Diese wurden weder in classifyAccountFailure (debrid.ts) noch in isPermanentLinkError (download-manager.ts) erkannt und landeten im generischen "temporaer"-Zweig: 30s Account-Cooldown + (bei retryLimit 0) endlose Wiederholung. Mit nur einem Account (als API UND Web) blockierte ein einziger toter Link ueber den Cooldown auch alle gesunden Links (SKIP_COOLDOWN) -- die Queue stand. Beleg aus dem Support-Bundle: 479 von 983 Zeilen im Rotations-Log waren dieser Fehler. - shared/dead-link.ts: zentrale Tot-Link-Erkennung (frz./dt./engl.), eine Quelle fuer beide Schichten, damit die Muster nicht auseinanderdriften. - classifyAccountFailure: toter Link -> fatal/skip, KEIN Account-Cooldown (Account ist gesund, nur die Datei ist weg). - isPermanentLinkError: toter Link -> Item sofort als gescheitert markiert, kein Endlos-Retry. Greift auch im aggregierten Provider-Ketten-Fehlerstring. - Provider-Kette + interner Web-Fallback: toter Link -> kein Fallback auf weitere Provider/Modi (spart den 60s-Web-Timeout; saubere Meldung erreicht isPermanentLinkError unveraendert). - Anzeige: deutsche Klartext-Meldung "Link tot - Datei beim Hoster geloescht" statt frz. - Account-Liste: Hinweis, dass Mega-Debrid API + Web derselbe Account in zwei Modi sind. Tests: tests/dead-link.test.ts (inkl. aggregierter Provider-Ketten-String + akzentfreie Variante). Volle Suite 823/823 gruen. --- src/main/debrid.ts | 13 ++++++++- src/main/download-manager.ts | 14 ++++------ src/renderer/App.tsx | 6 +++++ src/renderer/styles.css | 14 ++++++++++ src/shared/dead-link.ts | 36 +++++++++++++++++++++++++ tests/dead-link.test.ts | 52 ++++++++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 src/shared/dead-link.ts create mode 100644 tests/dead-link.test.ts diff --git a/src/main/debrid.ts b/src/main/debrid.ts index a609551..d31a6e2 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -2,6 +2,7 @@ import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; import { parseMegaDebridAccounts, type MegaDebridAccountEntry } from "../shared/mega-debrid-accounts"; import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostLimitInfo, DebridProvider } from "../shared/types"; import { isDebridLinkApiKeyDailyLimitReached, isMegaDebridAccountDisabled, isMegaDebridAccountDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits"; +import { isDeadLinkErrorText } from "../shared/dead-link"; import { APP_VERSION, REQUEST_RETRIES } from "./constants"; import { logger } from "./logger"; import { logAccountRotation } from "./account-rotation-log"; @@ -1888,6 +1889,9 @@ class MegaDebridClient { if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) { throw error; } + if (isDeadLinkErrorText(errorText)) { + throw error; + } if (!this.allowApiFallback) { throw error; } @@ -2139,7 +2143,7 @@ class MegaDebridClient { }; } - if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(errorText)) { + if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(errorText) || isDeadLinkErrorText(errorText)) { return { fatal: true, cooldownMs: 0, message: errorText, category: "skip" }; } @@ -3676,6 +3680,9 @@ export class DebridService { if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) { throw error; } + if (isDeadLinkErrorText(errorText)) { + throw error; + } if (!settings.autoProviderFallback) { throw new Error(`Hoster-Zuordnung fehlgeschlagen (${hosterKey} → ${PROVIDER_LABELS[routedProvider]}): ${errorText}`); } @@ -3796,6 +3803,10 @@ export class DebridService { if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) { throw error; } + if (isDeadLinkErrorText(errorText)) { + logger.warn(`Provider-Kette: ${PROVIDER_LABELS[provider]} meldet toten Link (${errorText}), kein Fallback auf weitere Provider`); + throw error; + } const nextProvider = order.slice(order.indexOf(provider) + 1).find((candidate) => this.isProviderSelectableFor(settings, candidate)); if (nextProvider) { logger.warn(`Provider-Kette: ${PROVIDER_LABELS[provider]} fehlgeschlagen (${errorText}), Fallback auf ${PROVIDER_LABELS[nextProvider]}`); diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 6c88be9..8a48bd2 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -22,6 +22,7 @@ import { StartConflictResolutionResult, UiSnapshot, DebridAccountStatus } from "../shared/types"; import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; +import { isDeadLinkErrorText, germanDeadLinkReason } from "../shared/dead-link"; import { addDebridLinkApiKeyDailyUsageBytes, addDebridLinkApiKeyTotalUsageBytes, @@ -609,14 +610,7 @@ export function getAuthoritativeRealDebridTotal( function isPermanentLinkError(errorText: string): boolean { const text = String(errorText || "").toLowerCase(); return text.includes("permanent ungültig") - || /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"); + || isDeadLinkErrorText(text); } function isUnrestrictFailure(errorText: string): boolean { @@ -9226,7 +9220,9 @@ export class DownloadManager extends EventEmitter { item.status = "failed"; this.recordRunOutcome(item.id, "failed"); item.lastError = errorText; - item.fullStatus = `Link ungültig: ${errorText}`; + item.fullStatus = isDeadLinkErrorText(errorText) + ? `Link tot – ${germanDeadLinkReason(errorText)}` + : `Link ungültig: ${errorText}`; item.speedBps = 0; item.updatedAt = nowMs(); this.retryStateByItem.delete(item.id); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index b03b232..d56fb7e 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -5411,6 +5411,12 @@ export function App(): ReactElement { {availableAccountOptions.length} weitere Typen verfügbar + {configuredAccountServices.has("megadebrid-api") && configuredAccountServices.has("megadebrid-web") && ( +
+ Mega-Debrid API + Web sind derselbe Account, nur zwei Zugriffsarten: API (schnell) wird zuerst versucht, Web dient als Fallback. Gleicher Login, kein zweites Konto — beide Zeilen teilen sich dasselbe Tageslimit und denselben Premium-Status. +
+ )} + {accountRows.length === 0 && (
Noch keine Accounts hinterlegt diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 1739eab..0ef92dd 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -2088,6 +2088,20 @@ body, line-height: 1.5; } +.account-mode-note { + padding: 10px 12px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent); + background: color-mix(in srgb, var(--accent) 7%, transparent); + color: var(--muted); + font-size: 12.5px; + line-height: 1.5; +} + +.account-mode-note strong { + color: var(--text); +} + .account-dl-key-limit-list { display: grid; gap: 8px; diff --git a/src/shared/dead-link.ts b/src/shared/dead-link.ts new file mode 100644 index 0000000..3f08dca --- /dev/null +++ b/src/shared/dead-link.ts @@ -0,0 +1,36 @@ +export function isDeadLinkErrorText(errorText: string): boolean { + const text = String(errorText || "").toLowerCase(); + return /supprim/.test(text) + || text.includes("introuvable") + || text.includes("n'existe plus") + || text.includes("n existe plus") + || text.includes("fichier inexistant") + || /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") + || text.includes("datei wurde gelöscht") + || text.includes("datei geloescht") + || text.includes("datei gelöscht"); +} + +export function germanDeadLinkReason(errorText: string): string { + const text = String(errorText || "").toLowerCase(); + if (/supprim/.test(text) && /h[eé]bergeur/.test(text)) { + return "Datei beim Hoster gelöscht"; + } + if (/supprim/.test(text)) { + return "Datei gelöscht"; + } + if (text.includes("introuvable") || text.includes("fichier inexistant")) { + return "Datei beim Hoster nicht gefunden"; + } + if (text.includes("n'existe plus") || text.includes("n existe plus")) { + return "Datei existiert nicht mehr"; + } + return "Datei nicht mehr verfügbar"; +} diff --git a/tests/dead-link.test.ts b/tests/dead-link.test.ts new file mode 100644 index 0000000..a3a93e1 --- /dev/null +++ b/tests/dead-link.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { isDeadLinkErrorText, germanDeadLinkReason } from "../src/shared/dead-link"; + +describe("isDeadLinkErrorText", () => { + it("matches the real Mega-Debrid French dead-link phrase", () => { + expect(isDeadLinkErrorText("Mega-Debrid API: Fichier supprimé chez l'hébergeur")).toBe(true); + }); + + it("matches inside the aggregated provider-chain error (api dead | web timeout)", () => { + const aggregated = "Unrestrict fehlgeschlagen: Mega-Debrid API: Mega-Debrid (API): Fichier supprimé chez l'hébergeur | Mega-Debrid Web: Mega-Debrid (Web): Abbruch/Timeout nach 60s"; + expect(isDeadLinkErrorText(aggregated)).toBe(true); + }); + + it("matches the de-accented variant (encoding may strip accents)", () => { + expect(isDeadLinkErrorText("Fichier supprime chez l'hebergeur")).toBe(true); + }); + + it("matches other clearly-dead French phrases", () => { + expect(isDeadLinkErrorText("Fichier introuvable")).toBe(true); + expect(isDeadLinkErrorText("Le fichier n'existe plus")).toBe(true); + }); + + it("still matches the existing English dead-link phrases", () => { + expect(isDeadLinkErrorText("file not found")).toBe(true); + expect(isDeadLinkErrorText("File has been deleted")).toBe(true); + expect(isDeadLinkErrorText("file is no longer available")).toBe(true); + expect(isDeadLinkErrorText("link is dead")).toBe(true); + }); + + it("does NOT match temporary/transient failures", () => { + expect(isDeadLinkErrorText("Abbruch/Timeout nach 60s")).toBe(false); + expect(isDeadLinkErrorText("Quota/Limit erreicht")).toBe(false); + expect(isDeadLinkErrorText("Rate-Limit (429)")).toBe(false); + expect(isDeadLinkErrorText("Kein Server fuer diesen Hoster")).toBe(false); + expect(isDeadLinkErrorText("queue-timeout")).toBe(false); + expect(isDeadLinkErrorText("hosternotavailable")).toBe(false); + }); +}); + +describe("germanDeadLinkReason", () => { + it("renders the deleted-at-host phrase in German", () => { + expect(germanDeadLinkReason("Mega-Debrid API: Fichier supprimé chez l'hébergeur")).toBe("Datei beim Hoster gelöscht"); + }); + + it("renders a plain deletion in German", () => { + expect(germanDeadLinkReason("fichier supprimé")).toBe("Datei gelöscht"); + }); + + it("renders not-found in German", () => { + expect(germanDeadLinkReason("Fichier introuvable")).toBe("Datei beim Hoster nicht gefunden"); + }); +});