Debrid-Link skip-Errors: Key bleibt "ready" statt "error"

fileNotAvailable, disabledServerHost, notFreeHost, serverNotAllowed,
freeServerOverload, maintenanceHost, noServerHost sind LINK- oder
HOST-level Fehler, nicht Key-level. Der Key antwortet ganz normal und
sagt nur "diesen Link kann ich aktuell nicht verarbeiten".

Vorher wurde trotzdem der Runtime-Status auf "error" gesetzt — sah in
der UI aus als waere der Key kaputt und hat die Rotations-Heuristiken
irritiert.

Fix: bei failure.category === "skip" den Runtime-Status in Ruhe lassen.
Der Key bleibt "ready" (bzw. was er vorher war). Invalid bleibt
"invalid", alle anderen fehlerhaften Antworten bleiben "error".

Test: Key 1 gibt fileNotAvailable zurueck → Key 2 erfolgreich. Key 1
darf danach NICHT "error" sein (per neuem Test-Helper
getDebridLinkKeyRuntimeStateForTests).
This commit is contained in:
Sucukdeluxe 2026-04-20 16:52:18 +02:00
parent 8e1159565b
commit 5ecb636d95
2 changed files with 72 additions and 2 deletions

View File

@ -131,6 +131,11 @@ export function primeDebridLinkRuntimeCooldownForTests(keyId: string, cooldownMs
setDebridLinkKeyCooldownState(keyId, cooldownMs, message, "temporary"); setDebridLinkKeyCooldownState(keyId, cooldownMs, message, "temporary");
} }
export function getDebridLinkKeyRuntimeStateForTests(keyId: string): DebridLinkRuntimeState | null {
const status = debridLinkKeyRuntimeStatuses.get(keyId);
return status ? status.state : null;
}
function clearDebridLinkKeyCooldownState(keyId: string): void { function clearDebridLinkKeyCooldownState(keyId: string): void {
debridLinkKeyCooldowns.delete(keyId); debridLinkKeyCooldowns.delete(keyId);
debridLinkKeyCooldownDetails.delete(keyId); debridLinkKeyCooldownDetails.delete(keyId);
@ -2678,7 +2683,15 @@ class DebridLinkClient {
} }
} else { } else {
clearDebridLinkKeyCooldownState(apiKey.id); clearDebridLinkKeyCooldownState(apiKey.id);
setDebridLinkKeyRuntimeStatus(apiKey.id, failure.category === "invalid" ? "invalid" : "error", failure.message); if (failure.category === "invalid") {
setDebridLinkKeyRuntimeStatus(apiKey.id, "invalid", failure.message);
} else if (failure.category !== "skip") {
// "skip" means the LINK or HOST is unavailable (fileNotAvailable,
// disabledServerHost, notFreeHost, freeServerOverload, ...), NOT
// that the key is broken. The key responded normally — leave its
// runtime status alone so the UI doesn't flag it as errored.
setDebridLinkKeyRuntimeStatus(apiKey.id, "error", failure.message);
}
} }
if (failure.fatal) { if (failure.fatal) {
logAccountRotation("ERROR", providerName, rotationLabel, "FATAL", { logAccountRotation("ERROR", providerName, rotationLabel, "FATAL", {

View File

@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants"; import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits"; import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, normalizeResolvedFilename, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid"; import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, normalizeResolvedFilename, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
@ -472,6 +472,63 @@ describe("debrid service", () => {
expect(r3.providerLabel).toContain("Key 1"); expect(r3.providerLabel).toContain("Key 1");
}); });
it("does not mark Debrid-Link key as errored when the API returns fileNotAvailable (link-level, not key-level)", async () => {
const settings = {
...defaultSettings(),
debridLinkApiKeys: "dl-key-one\ndl-key-two",
providerOrder: ["debridlink"] as const,
providerPrimary: "debridlink" as const,
providerSecondary: "none" as const,
providerTertiary: "none" as const,
autoProviderFallback: true
};
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
const headers = init?.headers;
let authHeader = "";
if (headers instanceof Headers) {
authHeader = headers.get("Authorization") || "";
} else if (Array.isArray(headers)) {
authHeader = headers.find(([key]) => key.toLowerCase() === "authorization")?.[1] || "";
} else {
authHeader = String((headers as Record<string, unknown> | undefined)?.Authorization || "");
}
if (!url.includes("/downloader/add")) {
return new Response("not-found", { status: 404 });
}
if (authHeader === "Bearer dl-key-one") {
return new Response(JSON.stringify({
success: false,
error: "fileNotAvailable",
error_description: "link is currently not available"
}), { status: 403, headers: { "Content-Type": "application/json" } });
}
return new Response(JSON.stringify({
success: true,
value: {
downloadUrl: "https://debrid-link.example/ok.bin",
name: "ok.bin",
size: 1024
}
}), { status: 200, headers: { "Content-Type": "application/json" } });
}) as typeof fetch;
const key1Id = parseDebridLinkApiKeys("dl-key-one")[0].id;
const key2Id = parseDebridLinkApiKeys("dl-key-two")[0].id;
const service = new DebridService(settings);
const result = await service.unrestrictLink("https://rapidgator.net/file/example");
expect(result.providerLabel).toContain("Key 2");
// Key-one responded normally — just that the link was unavailable on the
// hoster side. Key-one is NOT broken and must not be flagged as "error".
expect(getDebridLinkKeyRuntimeStateForTests(key1Id)).not.toBe("error");
// Key-two served the link successfully, so it's "ready".
expect(getDebridLinkKeyRuntimeStateForTests(key2Id)).toBe("ready");
});
it("treats bad Debrid-Link file passwords as fatal and does not rotate keys", async () => { it("treats bad Debrid-Link file passwords as fatal and does not rotate keys", async () => {
const settings = { const settings = {
...defaultSettings(), ...defaultSettings(),