Fix: Provider-Cooldown-Fallback fuehrt die Kette jetzt wirklich mit dem Ersatz-Provider an (Routing-Slice)
Wenn ein Provider in den Manager-Cooldown laeuft (>=20 Fehler in Folge) und auto-Fallback aktiv ist, berechnete der Manager zwar einen Ersatz-Provider (findFallbackProviderNotInCooldown), warf ihn dann aber weg: der unrestrictLink- Aufruf bekam keinen Hint, also baute debrid.ts die Provider-Kette frisch aus providerOrder und fuehrte erneut mit dem ausgebremsten Provider 1 an — ein meist in den 60s-Timeout laufender, verschwendeter Versuch, bevor ueberhaupt der gesunde Provider drankam. Jetzt wird der Ersatz-Provider als bevorzugter Lead durchgereicht. Die Kette wird UMSORTIERT, nicht beschnitten: der ausgebremste Provider bleibt als letzter Notnagel in der Reihenfolge, sodass kein Link gestrandet werden kann. Sind ALLE Provider im Cooldown, erreicht der Aufruf unrestrictLink ohnehin nicht (der else-Zweig queued einen Retry). Greift damit nur, wenn ein Provider bereits nachweislich degradiert ist — strikt besser, wenn aktiv, und ohne Timeout-, Cancel- oder Budget-Vertrag anzufassen. - debrid.ts: reine leadProviderChainWith(order, preferred) (export) + optionaler 4. Param preferredLeadProvider an unrestrictLink; Reorder direkt nach Aufbau der Reihenfolge. - download-manager.ts: Ersatz-Provider in preferredLeadProvider festhalten und an den unrestrictLink-Aufruf durchreichen. - Tests: Integration (ohne Hint fuehrt realdebrid, mit Hint fuehrt debridlink) + drei reine Helper-Tests fuer die No-Stranding-Invariante. Zwei verbleibende HIGH-Punkte sind bewusst NICHT autonom geaendert, sondern als Produkt-/UI-Entscheidung in tasks/entscheidungen-offen.md vorgelegt (globaler 60s-Failover-Timeout; gespiegelter Mega API/Web-Schalter).
This commit is contained in:
+54
-1
@@ -3,7 +3,7 @@ import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||
import { classifyMegaDebridAccountFailureForTests, clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||
import { classifyMegaDebridAccountFailureForTests, clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, leadProviderChainWith, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@@ -15,6 +15,21 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("leadProviderChainWith", () => {
|
||||
it("leaves the order unchanged when no preferred provider is given", () => {
|
||||
expect(leadProviderChainWith(["realdebrid", "debridlink", "alldebrid"], null)).toEqual(["realdebrid", "debridlink", "alldebrid"]);
|
||||
expect(leadProviderChainWith(["realdebrid", "debridlink"], undefined)).toEqual(["realdebrid", "debridlink"]);
|
||||
});
|
||||
|
||||
it("leads with the preferred provider but keeps every other provider as a later fallback", () => {
|
||||
expect(leadProviderChainWith(["realdebrid", "debridlink", "alldebrid"], "alldebrid")).toEqual(["alldebrid", "realdebrid", "debridlink"]);
|
||||
});
|
||||
|
||||
it("does not drop or strand any provider when the preferred one is not in the order", () => {
|
||||
expect(leadProviderChainWith(["realdebrid", "debridlink"], "alldebrid")).toEqual(["realdebrid", "debridlink"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("debrid service", () => {
|
||||
it("falls back to Mega web when Real-Debrid fails", async () => {
|
||||
const settings = {
|
||||
@@ -134,6 +149,44 @@ describe("debrid service", () => {
|
||||
expect(calledUrls.some((url) => url.includes("api.real-debrid.com/rest/1.0/unrestrict/link"))).toBe(false);
|
||||
});
|
||||
|
||||
it("leads the provider chain with the preferred (non-cooled) provider when one is hinted", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
debridLinkApiKeys: "dl-token",
|
||||
providerOrder: ["realdebrid", "debridlink"] as const,
|
||||
providerPrimary: "realdebrid" as const,
|
||||
providerSecondary: "debridlink" 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("debrid-link.com/api/v2/downloader/add")) {
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
value: { downloadUrl: "https://debrid-link.example/file.bin", name: "file.bin", size: 1234 }
|
||||
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) {
|
||||
return new Response(JSON.stringify({
|
||||
download: "https://rd.example/file.bin",
|
||||
filename: "file.bin",
|
||||
filesize: 1234
|
||||
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
return new Response("not-found", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const service = new DebridService(settings);
|
||||
const control = await service.unrestrictLink("https://hoster.example/lead-control.bin");
|
||||
expect(control.provider).toBe("realdebrid");
|
||||
|
||||
const preferred = await service.unrestrictLink("https://hoster.example/lead-pref.bin", undefined, undefined, "debridlink");
|
||||
expect(preferred.provider).toBe("debridlink");
|
||||
});
|
||||
|
||||
it("uses the next Debrid-Link key when the first key hit its local daily limit", async () => {
|
||||
const keys = parseDebridLinkApiKeys("dl-key-one\ndl-key-two");
|
||||
let usedAuthHeader = "";
|
||||
|
||||
Reference in New Issue
Block a user