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
+6 -12
View File
@@ -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]}`);
+9 -5
View File
@@ -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);
-36
View File
@@ -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
View 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";
}