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:
parent
bc50e4285d
commit
986fbab04f
@ -1321,6 +1321,13 @@ function toProviderOrder(primary: DebridProvider, secondary: DebridFallbackProvi
|
|||||||
return uniqueProviderOrder(order);
|
return uniqueProviderOrder(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function leadProviderChainWith(order: readonly DebridProvider[], preferred: DebridProvider | null | undefined): DebridProvider[] {
|
||||||
|
if (!preferred || !order.includes(preferred)) {
|
||||||
|
return [...order];
|
||||||
|
}
|
||||||
|
return [preferred, ...order.filter((provider) => provider !== preferred)];
|
||||||
|
}
|
||||||
|
|
||||||
function isRapidgatorLink(link: string): boolean {
|
function isRapidgatorLink(link: string): boolean {
|
||||||
try {
|
try {
|
||||||
const hostname = new URL(link).hostname.toLowerCase();
|
const hostname = new URL(link).hostname.toLowerCase();
|
||||||
@ -3700,7 +3707,7 @@ export class DebridService {
|
|||||||
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
|
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise<ProviderUnrestrictedLink> {
|
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings, preferredLeadProvider?: DebridProvider | null): Promise<ProviderUnrestrictedLink> {
|
||||||
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
|
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
|
||||||
|
|
||||||
const routing = settings.hosterRouting || {};
|
const routing = settings.hosterRouting || {};
|
||||||
@ -3777,9 +3784,10 @@ export class DebridService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const order: DebridProvider[] = (settings.providerOrder && settings.providerOrder.length > 0)
|
const baseOrder: DebridProvider[] = (settings.providerOrder && settings.providerOrder.length > 0)
|
||||||
? uniqueProviderOrder(settings.providerOrder)
|
? uniqueProviderOrder(settings.providerOrder)
|
||||||
: toProviderOrder(settings.providerPrimary, settings.providerSecondary, settings.providerTertiary);
|
: toProviderOrder(settings.providerPrimary, settings.providerSecondary, settings.providerTertiary);
|
||||||
|
const order = leadProviderChainWith(baseOrder, preferredLeadProvider);
|
||||||
|
|
||||||
const primary = order[0];
|
const primary = order[0];
|
||||||
if (!settings.autoProviderFallback) {
|
if (!settings.autoProviderFallback) {
|
||||||
|
|||||||
@ -8771,6 +8771,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
const cooldownProvider = this.getProviderFailureKeyForItem(item);
|
const cooldownProvider = this.getProviderFailureKeyForItem(item);
|
||||||
const cooldownMs = this.getProviderCooldownRemaining(cooldownProvider);
|
const cooldownMs = this.getProviderCooldownRemaining(cooldownProvider);
|
||||||
|
let preferredLeadProvider: DebridProvider | null = null;
|
||||||
if (cooldownMs > 0) {
|
if (cooldownMs > 0) {
|
||||||
if (this.settings.autoProviderFallback) {
|
if (this.settings.autoProviderFallback) {
|
||||||
const fallback = this.findFallbackProviderNotInCooldown(item);
|
const fallback = this.findFallbackProviderNotInCooldown(item);
|
||||||
@ -8782,6 +8783,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
fallback
|
fallback
|
||||||
});
|
});
|
||||||
item.provider = null;
|
item.provider = null;
|
||||||
|
preferredLeadProvider = fallback;
|
||||||
} else {
|
} else {
|
||||||
this.logPackageForItem(item, "WARN", "Provider-Cooldown blockiert Unrestrict", {
|
this.logPackageForItem(item, "WARN", "Provider-Cooldown blockiert Unrestrict", {
|
||||||
provider: cooldownProvider,
|
provider: cooldownProvider,
|
||||||
@ -8825,7 +8827,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
traceConversionNote("slots", this.describeSlotOccupancy());
|
traceConversionNote("slots", this.describeSlotOccupancy());
|
||||||
traceConversionNote("retry", Number(active.unrestrictRetries || 0));
|
traceConversionNote("retry", Number(active.unrestrictRetries || 0));
|
||||||
try {
|
try {
|
||||||
return await this.debridService.unrestrictLink(item.url, unrestrictedSignal);
|
return await this.debridService.unrestrictLink(item.url, unrestrictedSignal, undefined, preferredLeadProvider);
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
if (!active.abortController.signal.aborted && unrestrictTimeoutSignal.aborted) {
|
if (!active.abortController.signal.aborted && unrestrictTimeoutSignal.aborted) {
|
||||||
traceConversionPhase({
|
traceConversionPhase({
|
||||||
|
|||||||
@ -106,6 +106,30 @@ unabhaengigen Code-Verifikation abgedeckt (60s-Timeout kappt Failover, debrid.ts
|
|||||||
- #R3-7 LOW (3/3) Update-Integritaet hash-only, kein Authenticode — ehrliche Grenze, faellt sicher aus.
|
- #R3-7 LOW (3/3) Update-Integritaet hash-only, kein Authenticode — ehrliche Grenze, faellt sicher aus.
|
||||||
- REFUTIERT (0/3): all-accounts-parked wirft plain error ohne cooldown-retry-Token.
|
- REFUTIERT (0/3): all-accounts-parked wirft plain error ohne cooldown-retry-Token.
|
||||||
|
|
||||||
|
## Runde 4 (Failover-Routing-Slice + Entscheidungs-Doku)
|
||||||
|
Follow-on aus R3-Failover-Fund. Advisor-Disziplin: nur die SICHERE Scheibe autonom, der
|
||||||
|
Produkt-Tradeoff geht an den Nutzer (tasks/entscheidungen-offen.md).
|
||||||
|
|
||||||
|
### Autonom gefixt (TDD, rot-bewiesen)
|
||||||
|
- [x] MED Failover-Routing: Manager berechnete bei Provider-Cooldown (>=20 Fehler in Folge,
|
||||||
|
auto-Fallback an) einen Ersatz-Provider (`findFallbackProviderNotInCooldown`), WARF ihn aber
|
||||||
|
weg — `unrestrictLink(item.url, signal)` ohne Hint → debrid.ts baut `order` neu aus
|
||||||
|
providerOrder und fuehrt WIEDER mit dem ausgebremsten Provider1 an (wahrsch. 60s-Timeout
|
||||||
|
verschwendet). Fix: reine `leadProviderChainWith(order, preferred)` (debrid.ts, export) +
|
||||||
|
4. optionaler Param `preferredLeadProvider` an `unrestrictLink`; Manager reicht den Ersatz
|
||||||
|
durch (dl-mgr 8772/8828). REORDER nicht SKIP → ausgebremster Provider bleibt als letzter
|
||||||
|
Notnagel in der Kette, kein Stranding. All-cooled-Fall erreicht `unrestrictLink` gar nicht
|
||||||
|
(else-Zweig queueRetry'd). Tests: integration (control=realdebrid, preferred=debridlink) +
|
||||||
|
3 reine Helper-Tests (null→unchanged, in-order→leads+keeps-all, not-in-order→unchanged).
|
||||||
|
Suite 875 gruen, tsc=6. Commit folgt; HALTEN fuer Roll-up-Release (kein HIGH/dringend).
|
||||||
|
|
||||||
|
### An den Nutzer vorgelegt (NICHT autonom) → tasks/entscheidungen-offen.md
|
||||||
|
- 60s-Failover-Kappung (HIGH): A) pro-Provider-Timeout (Failover immer, aber bis 3×60s
|
||||||
|
Worst-Case) vs B) globales Budget mit Failover-Reserve (langsamer Provider1 frueher
|
||||||
|
abgeschnitten). Produkt-Tradeoff = Nutzerwahl. Post-214 weitgehend latent.
|
||||||
|
- Gespiegelter Mega API/Web-Schalter (HIGH, #R3-2): gemeinsamer Schalter vs unabhaengige
|
||||||
|
pro-Modus-Flags (Migration + UI-Redesign noetig, Nutzer UI-sensibel).
|
||||||
|
|
||||||
## Runde 2 (Download-Ausfuehrung: stream/resume/disk/integrity/extract/persist) — Workflow whspc8ddv
|
## Runde 2 (Download-Ausfuehrung: stream/resume/disk/integrity/extract/persist) — Workflow whspc8ddv
|
||||||
14 confirmed / 7 refuted (>=2/3 adversarisch). Alle HIGH/MED unten unabhaengig am echten Code
|
14 confirmed / 7 refuted (>=2/3 adversarisch). Alle HIGH/MED unten unabhaengig am echten Code
|
||||||
verifiziert (Zeilen zitiert) bevor gefixt. Jeder Fix mit rot-bewiesenem Test, tsc bleibt 6.
|
verifiziert (Zeilen zitiert) bevor gefixt. Jeder Fix mit rot-bewiesenem Test, tsc bleibt 6.
|
||||||
|
|||||||
70
tasks/entscheidungen-offen.md
Normal file
70
tasks/entscheidungen-offen.md
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
# Offene Entscheidungen für dich (Audit-Loop 2026-06-17)
|
||||||
|
|
||||||
|
Diese zwei Punkte habe ich BEWUSST nicht autonom „gefixt", weil jede Lösung
|
||||||
|
einen Produkt-/Geschmacks-Kompromiss enthält, den du entscheiden solltest, nicht ich.
|
||||||
|
Beide sind verifiziert (Code-Zitat + Szenario), nur die Richtung ist deine Wahl.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Failover-Kappung durch globalen 60-Sekunden-Timeout (HIGH)
|
||||||
|
|
||||||
|
**Was passiert (belegt):**
|
||||||
|
`download-manager.ts` baut EINEN Timeout fürs gesamte Unrestrict:
|
||||||
|
```
|
||||||
|
const unrestrictTimeoutSignal = AbortSignal.timeout(getUnrestrictTimeoutMs()); // 60s
|
||||||
|
const unrestrictedSignal = AbortSignal.any([active.abortController.signal, unrestrictTimeoutSignal]);
|
||||||
|
```
|
||||||
|
Dieses EINE Signal geht an die komplette Provider-Kette in `debrid.ts`. Die 60s sind
|
||||||
|
also ein Budget für ALLE Provider zusammen, nicht pro Provider. Wenn Provider 1
|
||||||
|
(z.B. Mega-Web mit Account-Queue) das Budget verbraucht, dann sieht Provider 2 ein
|
||||||
|
bereits abgelaufenes Signal → `debrid.ts` wertet den Abbruch als „kein Failover" und
|
||||||
|
wirft, ohne Provider 2 echt zu versuchen.
|
||||||
|
|
||||||
|
**Aktuell weitgehend latent:** Seit v1.7.214 wird die API zuerst probiert (schneller Pfad),
|
||||||
|
Mega-Web ist nur noch Fallback. Das Szenario „langsamer Provider 1 hungert Provider 2 aus"
|
||||||
|
trifft in der Produktion derzeit selten. Deshalb dokumentiert statt dringend gefixt.
|
||||||
|
|
||||||
|
**Deine Entscheidung — zwei Richtungen (gleiche Spannung, andere Seite):**
|
||||||
|
|
||||||
|
- **A) Pro-Provider-Timeout:** Jeder Provider bekommt sein eigenes frisches 60s-Budget.
|
||||||
|
Failover bekommt IMMER einen echten Versuch.
|
||||||
|
*Kosten:* Worst-Case-Wartezeit pro Item steigt (3 Provider × 60s = bis zu 180s, bevor ein
|
||||||
|
Item aufgibt). Du akzeptierst langsameres Worst-Case-pro-Item für vollständigeres Failover.
|
||||||
|
|
||||||
|
- **B) Globales Budget behalten, aber Slice für Failover reservieren:** Provider 1 wird auf
|
||||||
|
z.B. 35s gedeckelt, damit garantiert Zeit für Provider 2 bleibt.
|
||||||
|
*Kosten:* Ein legitim langsamer Provider 1 (Mega-Web-Account-Queue bis ~90s) wird früher
|
||||||
|
abgeschnitten → mehr Failover, auch wenn Provider 1 noch erfolgreich gewesen wäre.
|
||||||
|
|
||||||
|
Kernfrage, die nur du beantworten kannst: **Wie lange darf ein Item bei einem langsamen
|
||||||
|
aber funktionierenden Provider hängen, bevor wir ihn zugunsten des nächsten aufgeben?**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Gespiegelter Mega API/Web-Schalter (HIGH, in Runde 3 als 2/3 bestätigt)
|
||||||
|
|
||||||
|
**Was passiert (belegt):** Die Mega-Debrid API- und Web-Account-Zeilen teilen sich EINE
|
||||||
|
login-only Enable-Flag (`hasMegaDebridCredentials` gilt für beide; die Pro-Modus-Auswahl
|
||||||
|
läuft über `isMegaDebridModeEnabled(settings, "api"|"web")`). Dein Report: „API ausschalten
|
||||||
|
schaltet Web an" — weil der Schalter sich spiegelt.
|
||||||
|
|
||||||
|
**Warum ich es NICHT autonom geändert habe:**
|
||||||
|
- Daten-Modell-Fix (echte unabhängige Flags) braucht eine Settings-Migration, die
|
||||||
|
deaktivierte Accounts versehentlich re-aktivieren könnte.
|
||||||
|
- Die saubere UI-Variante (zwei echte unabhängige Schalter) ist ein Layout-Redesign — und
|
||||||
|
du bist UI-Geschmack-sensibel; das will ich nicht ungefragt umbauen.
|
||||||
|
|
||||||
|
**Deine Entscheidung:** Ein gemeinsamer Schalter (API+Web zusammen an/aus, klar beschriftet)
|
||||||
|
ODER zwei unabhängige Pro-Modus-Schalter (mehr Kontrolle, aber UI + Migration nötig)?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Erledigt in dieser Runde (zur Info, kein Handlungsbedarf)
|
||||||
|
|
||||||
|
- **MED Failover-Routing:** Wenn ein Provider in den Manager-Cooldown läuft (≥20 Fehler in
|
||||||
|
Folge) und auto-Fallback an ist, hat der Manager bisher zwar einen Ersatz-Provider berechnet,
|
||||||
|
ihn aber WEGGEWORFEN — die Kette führte trotzdem wieder mit dem ausgebremsten Provider an.
|
||||||
|
Jetzt wird der Ersatz-Provider als „Lead" durchgereicht und die Kette führt mit ihm an, OHNE
|
||||||
|
einen Provider zu verlieren (der ausgebremste bleibt als letzter Notnagel in der Kette).
|
||||||
|
Greift nur, wenn der Provider bereits nachweislich degradiert ist → strikt-besser-wenn-aktiv,
|
||||||
|
kein Timeout/Cancel-Vertrag berührt. Rot-bewiesener Test. (Hält für Roll-up-Release bereit.)
|
||||||
@ -3,7 +3,7 @@ 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 { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
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;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
@ -15,6 +15,21 @@ afterEach(() => {
|
|||||||
vi.restoreAllMocks();
|
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", () => {
|
describe("debrid service", () => {
|
||||||
it("falls back to Mega web when Real-Debrid fails", async () => {
|
it("falls back to Mega web when Real-Debrid fails", async () => {
|
||||||
const settings = {
|
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);
|
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 () => {
|
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");
|
const keys = parseDebridLinkApiKeys("dl-key-one\ndl-key-two");
|
||||||
let usedAuthHeader = "";
|
let usedAuthHeader = "";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user