diff --git a/src/main/debrid.ts b/src/main/debrid.ts
index a609551..d31a6e2 100644
--- a/src/main/debrid.ts
+++ b/src/main/debrid.ts
@@ -2,6 +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 { APP_VERSION, REQUEST_RETRIES } from "./constants";
import { logger } from "./logger";
import { logAccountRotation } from "./account-rotation-log";
@@ -1888,6 +1889,9 @@ 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;
}
@@ -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" };
}
@@ -3676,6 +3680,9 @@ 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}`);
}
@@ -3796,6 +3803,10 @@ 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 6c88be9..8a48bd2 100644
--- a/src/main/download-manager.ts
+++ b/src/main/download-manager.ts
@@ -22,6 +22,7 @@ import {
StartConflictResolutionResult,
UiSnapshot, DebridAccountStatus } from "../shared/types";
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
+import { isDeadLinkErrorText, germanDeadLinkReason } from "../shared/dead-link";
import {
addDebridLinkApiKeyDailyUsageBytes,
addDebridLinkApiKeyTotalUsageBytes,
@@ -609,14 +610,7 @@ export function getAuthoritativeRealDebridTotal(
function isPermanentLinkError(errorText: string): boolean {
const text = String(errorText || "").toLowerCase();
return text.includes("permanent ungültig")
- || /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");
+ || isDeadLinkErrorText(text);
}
function isUnrestrictFailure(errorText: string): boolean {
@@ -9226,7 +9220,9 @@ export class DownloadManager extends EventEmitter {
item.status = "failed";
this.recordRunOutcome(item.id, "failed");
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.updatedAt = nowMs();
this.retryStateByItem.delete(item.id);
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index b03b232..d56fb7e 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -5411,6 +5411,12 @@ export function App(): ReactElement {
{availableAccountOptions.length} weitere Typen verfügbar
+ {configuredAccountServices.has("megadebrid-api") && configuredAccountServices.has("megadebrid-web") && (
+
+ Mega-Debrid API + Web sind derselbe Account, 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.
+
+ )}
+
{accountRows.length === 0 && (
Noch keine Accounts hinterlegt
diff --git a/src/renderer/styles.css b/src/renderer/styles.css
index 1739eab..0ef92dd 100644
--- a/src/renderer/styles.css
+++ b/src/renderer/styles.css
@@ -2088,6 +2088,20 @@ body,
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 {
display: grid;
gap: 8px;
diff --git a/src/shared/dead-link.ts b/src/shared/dead-link.ts
new file mode 100644
index 0000000..3f08dca
--- /dev/null
+++ b/src/shared/dead-link.ts
@@ -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";
+}
diff --git a/tests/dead-link.test.ts b/tests/dead-link.test.ts
new file mode 100644
index 0000000..a3a93e1
--- /dev/null
+++ b/tests/dead-link.test.ts
@@ -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");
+ });
+});