Fix: Tote Mega-Debrid-Links (frz. "Fichier supprimé") vergiften nicht mehr die ganze Queue
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.
This commit is contained in:
parent
e0f8b446e3
commit
938b84392d
@ -2,6 +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 { 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";
|
||||||
@ -1888,6 +1889,9 @@ 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;
|
||||||
}
|
}
|
||||||
@ -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" };
|
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))) {
|
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}`);
|
||||||
}
|
}
|
||||||
@ -3796,6 +3803,10 @@ 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,6 +22,7 @@ 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,
|
||||||
@ -609,14 +610,7 @@ 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")
|
||||||
|| /file.?not.?found/.test(text)
|
|| isDeadLinkErrorText(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 {
|
||||||
@ -9226,7 +9220,9 @@ 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 = `Link ungültig: ${errorText}`;
|
item.fullStatus = isDeadLinkErrorText(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);
|
||||||
|
|||||||
@ -5411,6 +5411,12 @@ export function App(): ReactElement {
|
|||||||
<span className="account-inline-stat">{availableAccountOptions.length} weitere Typen verfügbar</span>
|
<span className="account-inline-stat">{availableAccountOptions.length} weitere Typen verfügbar</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{configuredAccountServices.has("megadebrid-api") && configuredAccountServices.has("megadebrid-web") && (
|
||||||
|
<div className="account-mode-note">
|
||||||
|
<strong>Mega-Debrid API + Web sind derselbe Account</strong>, 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.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{accountRows.length === 0 && (
|
{accountRows.length === 0 && (
|
||||||
<div className="account-empty-state">
|
<div className="account-empty-state">
|
||||||
<strong>Noch keine Accounts hinterlegt</strong>
|
<strong>Noch keine Accounts hinterlegt</strong>
|
||||||
|
|||||||
@ -2088,6 +2088,20 @@ body,
|
|||||||
line-height: 1.5;
|
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 {
|
.account-dl-key-limit-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
36
src/shared/dead-link.ts
Normal file
36
src/shared/dead-link.ts
Normal file
@ -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";
|
||||||
|
}
|
||||||
52
tests/dead-link.test.ts
Normal file
52
tests/dead-link.test.ts
Normal file
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user