diff --git a/src/main/debrid.ts b/src/main/debrid.ts index d31a6e2..9f8e0c1 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -2,7 +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 { isMegaDebridResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors"; import { APP_VERSION, REQUEST_RETRIES } from "./constants"; import { logger } from "./logger"; import { logAccountRotation } from "./account-rotation-log"; @@ -1889,9 +1889,6 @@ 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; } @@ -2143,7 +2140,11 @@ class MegaDebridClient { }; } - if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(errorText) || isDeadLinkErrorText(errorText)) { + if (isMegaDebridResolveFailure(errorText)) { + return { fatal: false, cooldownMs: 0, message: germanMegaDebridResolveReason(errorText), category: "temporary" }; + } + + if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(errorText)) { return { fatal: true, cooldownMs: 0, message: errorText, category: "skip" }; } @@ -3680,9 +3681,6 @@ 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}`); } @@ -3803,10 +3801,6 @@ 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 8a48bd2..6c88be9 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -22,7 +22,6 @@ import { StartConflictResolutionResult, UiSnapshot, DebridAccountStatus } from "../shared/types"; import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; -import { isDeadLinkErrorText, germanDeadLinkReason } from "../shared/dead-link"; import { addDebridLinkApiKeyDailyUsageBytes, addDebridLinkApiKeyTotalUsageBytes, @@ -610,7 +609,14 @@ export function getAuthoritativeRealDebridTotal( function isPermanentLinkError(errorText: string): boolean { const text = String(errorText || "").toLowerCase(); return text.includes("permanent ungültig") - || isDeadLinkErrorText(text); + || /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 { @@ -9220,9 +9226,7 @@ export class DownloadManager extends EventEmitter { item.status = "failed"; this.recordRunOutcome(item.id, "failed"); item.lastError = errorText; - item.fullStatus = isDeadLinkErrorText(errorText) - ? `Link tot – ${germanDeadLinkReason(errorText)}` - : `Link ungültig: ${errorText}`; + item.fullStatus = `Link ungültig: ${errorText}`; item.speedBps = 0; item.updatedAt = nowMs(); this.retryStateByItem.delete(item.id); diff --git a/src/shared/dead-link.ts b/src/shared/dead-link.ts deleted file mode 100644 index 3f08dca..0000000 --- a/src/shared/dead-link.ts +++ /dev/null @@ -1,36 +0,0 @@ -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/src/shared/mega-debrid-errors.ts b/src/shared/mega-debrid-errors.ts new file mode 100644 index 0000000..80b9bb8 --- /dev/null +++ b/src/shared/mega-debrid-errors.ts @@ -0,0 +1,16 @@ +export function isMegaDebridResolveFailure(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"); +} + +export function germanMegaDebridResolveReason(errorText: string): string { + const text = String(errorText || "").toLowerCase(); + if (text.includes("introuvable") || text.includes("fichier inexistant") || text.includes("n'existe plus") || text.includes("n existe plus")) { + return "Datei beim Hoster nicht gefunden"; + } + return "Datei beim Hoster gerade nicht abrufbar"; +} diff --git a/tests/dead-link.test.ts b/tests/dead-link.test.ts deleted file mode 100644 index a3a93e1..0000000 --- a/tests/dead-link.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -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"); - }); -}); diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 3667949..670b366 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -1303,6 +1303,42 @@ describe("debrid service", () => { expect(megaWeb).toHaveBeenCalledTimes(0); }); + it("treats a Mega-Debrid 'Fichier supprimé' as transient: no account cooldown, German message, retryable", async () => { + const settings = { + ...defaultSettings(), + token: "", + bestToken: "", + allDebridToken: "", + megaLogin: "user", + megaPassword: "pass", + megaCredentials: "user:pass", + megaDebridApiEnabled: true, + megaDebridWebEnabled: false, + providerPrimary: "megadebrid-api" as const, + providerSecondary: "none" as const, + providerTertiary: "none" as const, + autoProviderFallback: true + }; + + globalThis.fetch = (async (input: RequestInfo | URL): Promise => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + if (url.includes("action=connectUser")) { + return new Response(JSON.stringify({ response_code: "ok", token: "tok", vip_end: Math.floor(Date.now() / 1000) + 999999 }), { status: 200, headers: { "Content-Type": "application/json" } }); + } + if (url.includes("action=getLink")) { + return new Response(JSON.stringify({ response_code: "error", response_text: "Fichier supprimé chez l'hébergeur" }), { status: 200, headers: { "Content-Type": "application/json" } }); + } + return new Response("not-found", { status: 404 }); + }) as typeof fetch; + + const service = new DebridService(settings); + const err = await service.unrestrictLink("https://rapidgator.net/file/maybe-dead.rar.html").then(() => null, (e: unknown) => e); + expect(err).toBeTruthy(); + expect(String(err)).toMatch(/nicht abrufbar/i); + expect(String(err)).not.toMatch(/supprim/i); + expect(getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`)).toBeNull(); + }); + it("uses Mega Web only when it is configured as a separate fallback provider", async () => { const settings = { ...defaultSettings(), diff --git a/tests/mega-debrid-errors.test.ts b/tests/mega-debrid-errors.test.ts new file mode 100644 index 0000000..393d083 --- /dev/null +++ b/tests/mega-debrid-errors.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { isMegaDebridResolveFailure, germanMegaDebridResolveReason } from "../src/shared/mega-debrid-errors"; + +describe("isMegaDebridResolveFailure", () => { + it("detects the real Mega-Debrid French resolve-failure phrase", () => { + expect(isMegaDebridResolveFailure("Mega-Debrid API: Fichier supprimé chez l'hébergeur")).toBe(true); + }); + + it("matches inside the aggregated provider-chain error (api fail | 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(isMegaDebridResolveFailure(aggregated)).toBe(true); + }); + + it("matches the de-accented variant", () => { + expect(isMegaDebridResolveFailure("Fichier supprime chez l'hebergeur")).toBe(true); + }); + + it("matches other Mega-Debrid resolve phrases", () => { + expect(isMegaDebridResolveFailure("Fichier introuvable")).toBe(true); + expect(isMegaDebridResolveFailure("Le fichier n'existe plus")).toBe(true); + }); + + it("does NOT match unrelated/transient text", () => { + expect(isMegaDebridResolveFailure("Abbruch/Timeout nach 60s")).toBe(false); + expect(isMegaDebridResolveFailure("Quota/Limit erreicht")).toBe(false); + }); +}); + +describe("germanMegaDebridResolveReason (transient wording, NOT 'tot')", () => { + it("renders 'supprimé' as a transient, retryable German reason", () => { + const reason = germanMegaDebridResolveReason("Mega-Debrid API: Fichier supprimé chez l'hébergeur"); + expect(reason).toBe("Datei beim Hoster gerade nicht abrufbar"); + expect(reason.toLowerCase()).not.toContain("tot"); + expect(reason.toLowerCase()).not.toContain("gelöscht"); + }); + + it("renders not-found phrases in German", () => { + expect(germanMegaDebridResolveReason("Fichier introuvable")).toBe("Datei beim Hoster nicht gefunden"); + }); +});