Fix: Mega-Debrid "Fichier supprime" ist transient, nicht tot -- korrigiert v1.7.210

v1.7.210 hat den franzoesischen Mega-Debrid-Fehler "Fichier supprime chez
l'hebergeur" als permanent toten Link behandelt (Item sofort gescheitert, kein
Web-Fallback). Die Support-Bundle-Logs widerlegen das: von 18 Links mit diesem
Fehler haben 4 Sekunden spaeter ein OK geliefert -- 1x ueber Web, 3x ueber
API-Retry (7-66s). Der Fehler ist TRANSIENT; v1.7.210 haette diese erholbaren
Links faelschlich dauerhaft gekillt und den rettenden Web-Fallback abgeklemmt.

Korrektur:
- classifyAccountFailure: "supprime"/"introuvable" -> temporaer, cooldownMs 0,
  NICHT fatal. Der eigentliche Stall-Verursacher (30s Account-Cooldown, der bei
  nur einem Account auch alle gesunden Links via SKIP_COOLDOWN blockierte) ist
  damit weg, OHNE die erholbaren Links zu verlieren.
- isPermanentLinkError: franzoesische Phrasen wieder entfernt (nur echte
  permanente engl. Signale fuer RD/AllDebrid bleiben).
- Provider-Kette + interner Web-Fallback: Kurzschluss entfernt -> Web-Fallback
  wird wieder versucht (hat 1/4 der Links gerettet).
- Pacing kommt vom bestehenden 5s-Exponential-Backoff (unrestrictDelayMs) pro
  Item, nicht vom Account-Cooldown -> weit unter Mega-Debrid 50 req/s, kein
  IP-Ban-Risiko.
- Anzeige: deutsche, transient formulierte Meldung "Datei beim Hoster gerade
  nicht abrufbar" statt franzoesisch und statt "tot".
- shared/dead-link.ts -> shared/mega-debrid-errors.ts, Semantik korrekt benannt.

Tests: tests/mega-debrid-errors.test.ts + behavioraler Test in debrid.test.ts
(supprime -> kein Account-Cooldown, deutsche Meldung, retrybar). Volle Suite
822/822 gruen.

Behebt die Queue-Quervergiftung; macht echte tote Links bei retryLimit 0 NICHT
aufgeben (vorbestehend, unveraendert). Parallel-Umwandlung folgt separat.
This commit is contained in:
Sucukdeluxe
2026-06-17 00:45:09 +02:00
parent ec1bdcdfe2
commit db59b59053
7 changed files with 107 additions and 105 deletions
-52
View File
@@ -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");
});
});
+36
View File
@@ -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<Response> => {
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(),
+40
View File
@@ -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");
});
});