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:
parent
ec1bdcdfe2
commit
db59b59053
@ -2,7 +2,7 @@ import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
|||||||
import { parseMegaDebridAccounts, type MegaDebridAccountEntry } from "../shared/mega-debrid-accounts";
|
import { parseMegaDebridAccounts, type MegaDebridAccountEntry } from "../shared/mega-debrid-accounts";
|
||||||
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostLimitInfo, DebridProvider } from "../shared/types";
|
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostLimitInfo, DebridProvider } from "../shared/types";
|
||||||
import { isDebridLinkApiKeyDailyLimitReached, isMegaDebridAccountDisabled, isMegaDebridAccountDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
|
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 { APP_VERSION, REQUEST_RETRIES } from "./constants";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { logAccountRotation } from "./account-rotation-log";
|
import { logAccountRotation } from "./account-rotation-log";
|
||||||
@ -1889,9 +1889,6 @@ class MegaDebridClient {
|
|||||||
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
if (isDeadLinkErrorText(errorText)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (!this.allowApiFallback) {
|
if (!this.allowApiFallback) {
|
||||||
throw error;
|
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" };
|
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))) {
|
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
if (isDeadLinkErrorText(errorText)) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (!settings.autoProviderFallback) {
|
if (!settings.autoProviderFallback) {
|
||||||
throw new Error(`Hoster-Zuordnung fehlgeschlagen (${hosterKey} → ${PROVIDER_LABELS[routedProvider]}): ${errorText}`);
|
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))) {
|
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||||
throw error;
|
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));
|
const nextProvider = order.slice(order.indexOf(provider) + 1).find((candidate) => this.isProviderSelectableFor(settings, candidate));
|
||||||
if (nextProvider) {
|
if (nextProvider) {
|
||||||
logger.warn(`Provider-Kette: ${PROVIDER_LABELS[provider]} fehlgeschlagen (${errorText}), Fallback auf ${PROVIDER_LABELS[nextProvider]}`);
|
logger.warn(`Provider-Kette: ${PROVIDER_LABELS[provider]} fehlgeschlagen (${errorText}), Fallback auf ${PROVIDER_LABELS[nextProvider]}`);
|
||||||
|
|||||||
@ -22,7 +22,6 @@ import {
|
|||||||
StartConflictResolutionResult,
|
StartConflictResolutionResult,
|
||||||
UiSnapshot, DebridAccountStatus } from "../shared/types";
|
UiSnapshot, DebridAccountStatus } from "../shared/types";
|
||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
import { isDeadLinkErrorText, germanDeadLinkReason } from "../shared/dead-link";
|
|
||||||
import {
|
import {
|
||||||
addDebridLinkApiKeyDailyUsageBytes,
|
addDebridLinkApiKeyDailyUsageBytes,
|
||||||
addDebridLinkApiKeyTotalUsageBytes,
|
addDebridLinkApiKeyTotalUsageBytes,
|
||||||
@ -610,7 +609,14 @@ export function getAuthoritativeRealDebridTotal(
|
|||||||
function isPermanentLinkError(errorText: string): boolean {
|
function isPermanentLinkError(errorText: string): boolean {
|
||||||
const text = String(errorText || "").toLowerCase();
|
const text = String(errorText || "").toLowerCase();
|
||||||
return text.includes("permanent ungültig")
|
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 {
|
function isUnrestrictFailure(errorText: string): boolean {
|
||||||
@ -9220,9 +9226,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
item.status = "failed";
|
item.status = "failed";
|
||||||
this.recordRunOutcome(item.id, "failed");
|
this.recordRunOutcome(item.id, "failed");
|
||||||
item.lastError = errorText;
|
item.lastError = errorText;
|
||||||
item.fullStatus = isDeadLinkErrorText(errorText)
|
item.fullStatus = `Link ungültig: ${errorText}`;
|
||||||
? `Link tot – ${germanDeadLinkReason(errorText)}`
|
|
||||||
: `Link ungültig: ${errorText}`;
|
|
||||||
item.speedBps = 0;
|
item.speedBps = 0;
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
this.retryStateByItem.delete(item.id);
|
this.retryStateByItem.delete(item.id);
|
||||||
|
|||||||
@ -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";
|
|
||||||
}
|
|
||||||
16
src/shared/mega-debrid-errors.ts
Normal file
16
src/shared/mega-debrid-errors.ts
Normal file
@ -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";
|
||||||
|
}
|
||||||
@ -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");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1303,6 +1303,42 @@ describe("debrid service", () => {
|
|||||||
expect(megaWeb).toHaveBeenCalledTimes(0);
|
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 () => {
|
it("uses Mega Web only when it is configured as a separate fallback provider", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
|
|||||||
40
tests/mega-debrid-errors.test.ts
Normal file
40
tests/mega-debrid-errors.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user