Compare commits
No commits in common. "8e1159565b944ab44fd28f00ecb98c953be5cf48" and "3a8be961b0174ff181f382970e9ef7809f53ec6c" have entirely different histories.
8e1159565b
...
3a8be961b0
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.143",
|
"version": "1.7.142",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -22,15 +22,7 @@ const ONEFICHIER_API_BASE = "https://api.1fichier.com/v1";
|
|||||||
const ONEFICHIER_URL_RE = /^https?:\/\/(?:www\.)?(?:1fichier\.com|alterupload\.com|cjoint\.net|desfichiers\.com|dfichiers\.com|megadl\.fr|mesfichiers\.org|piecejointe\.net|pjointe\.com|tenvoi\.com|dl4free\.com)\/\?([a-z0-9]{5,20})$/i;
|
const ONEFICHIER_URL_RE = /^https?:\/\/(?:www\.)?(?:1fichier\.com|alterupload\.com|cjoint\.net|desfichiers\.com|dfichiers\.com|megadl\.fr|mesfichiers\.org|piecejointe\.net|pjointe\.com|tenvoi\.com|dl4free\.com)\/\?([a-z0-9]{5,20})$/i;
|
||||||
|
|
||||||
const DEBRID_LINK_API_BASE = "https://debrid-link.com/api/v2";
|
const DEBRID_LINK_API_BASE = "https://debrid-link.com/api/v2";
|
||||||
/** Truly key-wide quota errors: the whole key is exhausted regardless of host. */
|
const DEBRID_LINK_QUOTA_ERRORS = new Set(["maxLink", "maxLinkHost", "maxData", "maxDataHost"]);
|
||||||
const DEBRID_LINK_KEY_QUOTA_ERRORS = new Set(["maxLink", "maxData"]);
|
|
||||||
/** Per-(key, host) quota errors: only this host is exhausted for this key — the
|
|
||||||
* key remains usable for other hosters. */
|
|
||||||
const DEBRID_LINK_HOST_QUOTA_ERRORS = new Set(["maxLinkHost", "maxDataHost"]);
|
|
||||||
/** Backward-compat union — includes BOTH key-wide and per-host quota codes.
|
|
||||||
* Use this only for "is it a quota error of any kind?" checks; for behavior
|
|
||||||
* branches use the more specific sets above. */
|
|
||||||
const DEBRID_LINK_QUOTA_ERRORS = new Set([...DEBRID_LINK_KEY_QUOTA_ERRORS, ...DEBRID_LINK_HOST_QUOTA_ERRORS]);
|
|
||||||
const DEBRID_LINK_INVALID_TOKEN_ERRORS = new Set(["badToken", "hidedToken", "expired_token"]);
|
const DEBRID_LINK_INVALID_TOKEN_ERRORS = new Set(["badToken", "hidedToken", "expired_token"]);
|
||||||
const DEBRID_LINK_RATE_LIMIT_ERRORS = new Set(["floodDetected"]);
|
const DEBRID_LINK_RATE_LIMIT_ERRORS = new Set(["floodDetected"]);
|
||||||
const DEBRID_LINK_RETRYABLE_ERRORS = new Set(["internalError", "server_error"]);
|
const DEBRID_LINK_RETRYABLE_ERRORS = new Set(["internalError", "server_error"]);
|
||||||
@ -66,8 +58,6 @@ export function resetDebridLinkRuntimeStateForTests(): void {
|
|||||||
debridLinkKeyCooldowns.clear();
|
debridLinkKeyCooldowns.clear();
|
||||||
debridLinkKeyCooldownDetails.clear();
|
debridLinkKeyCooldownDetails.clear();
|
||||||
debridLinkKeyRuntimeStatuses.clear();
|
debridLinkKeyRuntimeStatuses.clear();
|
||||||
debridLinkKeyHostCooldowns.clear();
|
|
||||||
debridLinkKeyHostCooldownDetails.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Drop all Debrid-Link cooldown/runtime entries for key IDs that are no
|
/** Drop all Debrid-Link cooldown/runtime entries for key IDs that are no
|
||||||
@ -85,17 +75,6 @@ export function pruneDebridLinkRuntimeStateForKeys(activeKeyIds: Set<string>): v
|
|||||||
debridLinkKeyRuntimeStatuses.delete(keyId);
|
debridLinkKeyRuntimeStatuses.delete(keyId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Per-(key, host) cooldown keys have format `${keyId}|${hoster}` — drop any
|
|
||||||
// whose keyId is no longer in the active set so removed keys don't keep
|
|
||||||
// memory state around if they're re-added later.
|
|
||||||
for (const stateKey of debridLinkKeyHostCooldowns.keys()) {
|
|
||||||
const sepIdx = stateKey.indexOf("|");
|
|
||||||
const keyId = sepIdx >= 0 ? stateKey.slice(0, sepIdx) : stateKey;
|
|
||||||
if (!activeKeyIds.has(keyId)) {
|
|
||||||
debridLinkKeyHostCooldowns.delete(stateKey);
|
|
||||||
debridLinkKeyHostCooldownDetails.delete(stateKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Periodic cleanup of expired Debrid-Link cooldown/runtime entries.
|
/** Periodic cleanup of expired Debrid-Link cooldown/runtime entries.
|
||||||
@ -117,13 +96,6 @@ export function pruneExpiredDebridLinkRuntimeState(now = Date.now()): number {
|
|||||||
removed += 1;
|
removed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const [stateKey, until] of debridLinkKeyHostCooldowns) {
|
|
||||||
if (until + grace < now) {
|
|
||||||
debridLinkKeyHostCooldowns.delete(stateKey);
|
|
||||||
debridLinkKeyHostCooldownDetails.delete(stateKey);
|
|
||||||
removed += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,92 +192,6 @@ function getDebridLinkKeyCooldownState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Per-(key, host) cooldown cache. When a key hits maxLinkHost / maxDataHost
|
|
||||||
* for a specific host, only that combination should be blocked — the key
|
|
||||||
* itself stays usable for other hosters. Map key format: `${keyId}|${hoster}`. */
|
|
||||||
const debridLinkKeyHostCooldowns = new Map<string, number>();
|
|
||||||
const debridLinkKeyHostCooldownDetails = new Map<string, DebridLinkCooldownDetail>();
|
|
||||||
|
|
||||||
function makeDebridLinkKeyHostCooldownKey(keyId: string, hoster: string): string {
|
|
||||||
return `${keyId}|${hoster.toLowerCase()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearDebridLinkKeyHostCooldownState(keyId: string, hoster: string): void {
|
|
||||||
const stateKey = makeDebridLinkKeyHostCooldownKey(keyId, hoster);
|
|
||||||
debridLinkKeyHostCooldowns.delete(stateKey);
|
|
||||||
debridLinkKeyHostCooldownDetails.delete(stateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDebridLinkKeyHostCooldownState(
|
|
||||||
keyId: string,
|
|
||||||
hoster: string,
|
|
||||||
cooldownMs: number,
|
|
||||||
message: string,
|
|
||||||
category: DebridLinkCooldownCategory
|
|
||||||
): void {
|
|
||||||
if (!hoster) {
|
|
||||||
// Fall back to key-wide cooldown when we can't determine the hoster — better
|
|
||||||
// a slightly broader block than letting the key thrash on the same failure.
|
|
||||||
setDebridLinkKeyCooldownState(keyId, cooldownMs, message, category);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
|
|
||||||
clearDebridLinkKeyHostCooldownState(keyId, hoster);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const stateKey = makeDebridLinkKeyHostCooldownKey(keyId, hoster);
|
|
||||||
// Same max-wins semantics as setDebridLinkKeyCooldownState — parallel items
|
|
||||||
// hitting maxDataHost on the same (key, host) shouldn't shorten an existing
|
|
||||||
// longer cooldown. Strong categories (quota / rate_limit / invalid) win over
|
|
||||||
// generic temporary regardless of duration.
|
|
||||||
const newUntil = Date.now() + Math.max(1000, Math.floor(cooldownMs));
|
|
||||||
const existingUntil = Number(debridLinkKeyHostCooldowns.get(stateKey) || 0);
|
|
||||||
const existingDetail = debridLinkKeyHostCooldownDetails.get(stateKey);
|
|
||||||
const newIsStrongCategory = category === "rate_limit" || category === "quota" || category === "invalid";
|
|
||||||
const existingIsStrongCategory = existingDetail
|
|
||||||
? (existingDetail.category === "rate_limit" || existingDetail.category === "quota" || existingDetail.category === "invalid")
|
|
||||||
: false;
|
|
||||||
if (existingUntil > Date.now()) {
|
|
||||||
if (existingUntil >= newUntil && (!newIsStrongCategory || existingIsStrongCategory)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (existingIsStrongCategory && !newIsStrongCategory) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debridLinkKeyHostCooldowns.set(stateKey, newUntil);
|
|
||||||
debridLinkKeyHostCooldownDetails.set(stateKey, { message, category });
|
|
||||||
// Intentionally NOT updating setDebridLinkKeyRuntimeStatus here — the key
|
|
||||||
// is still healthy for other hosters, only this (key, host) is blocked.
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDebridLinkKeyHostCooldownState(
|
|
||||||
keyId: string,
|
|
||||||
hoster: string,
|
|
||||||
now = Date.now()
|
|
||||||
): { until: number; remainingMs: number; message: string; category: DebridLinkCooldownCategory } | null {
|
|
||||||
if (!hoster) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const stateKey = makeDebridLinkKeyHostCooldownKey(keyId, hoster);
|
|
||||||
const until = Number(debridLinkKeyHostCooldowns.get(stateKey) || 0);
|
|
||||||
if (!until) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (until <= now) {
|
|
||||||
debridLinkKeyHostCooldowns.delete(stateKey);
|
|
||||||
debridLinkKeyHostCooldownDetails.delete(stateKey);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const detail = debridLinkKeyHostCooldownDetails.get(stateKey);
|
|
||||||
return {
|
|
||||||
until,
|
|
||||||
remainingMs: until - now,
|
|
||||||
message: detail?.message || "Debrid-Link Key fuer Host im Cooldown",
|
|
||||||
category: detail?.category || "quota"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Per-account cooldown cache for Mega-Debrid: accountId → expiry timestamp. */
|
/** Per-account cooldown cache for Mega-Debrid: accountId → expiry timestamp. */
|
||||||
type MegaDebridCooldownCategory = "invalid" | "rate_limit" | "quota" | "temporary" | "skip";
|
type MegaDebridCooldownCategory = "invalid" | "rate_limit" | "quota" | "temporary" | "skip";
|
||||||
type MegaDebridCooldownDetail = { until: number; message: string; category: MegaDebridCooldownCategory };
|
type MegaDebridCooldownDetail = { until: number; message: string; category: MegaDebridCooldownCategory };
|
||||||
@ -2571,7 +2457,6 @@ class DebridLinkClient {
|
|||||||
const totalKeys = this.apiKeys.length;
|
const totalKeys = this.apiKeys.length;
|
||||||
const providerName = "Debrid-Link";
|
const providerName = "Debrid-Link";
|
||||||
const linkShort = String(link || "").slice(0, 80);
|
const linkShort = String(link || "").slice(0, 80);
|
||||||
const linkHoster = extractHosterFromUrl(link);
|
|
||||||
|
|
||||||
// Always start from first key — use first available, skip disabled/limited/cooldown.
|
// Always start from first key — use first available, skip disabled/limited/cooldown.
|
||||||
// This ensures all parallel items use the same key until it's actually exhausted.
|
// This ensures all parallel items use the same key until it's actually exhausted.
|
||||||
@ -2606,25 +2491,6 @@ class DebridLinkClient {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Per-(key, host) cooldown — set when a previous attempt for THIS host
|
|
||||||
// returned maxLinkHost / maxDataHost. The key itself is healthy for other
|
|
||||||
// hosters, so we only skip it for this specific link.
|
|
||||||
const hostCooldownState = linkHoster ? getDebridLinkKeyHostCooldownState(apiKey.id, linkHoster) : null;
|
|
||||||
if (hostCooldownState) {
|
|
||||||
const untilStr = new Date(hostCooldownState.until).toLocaleTimeString();
|
|
||||||
logger.info(`Debrid-Link${keyLabel}: uebersprungen (Host-Cooldown ${linkHoster} bis ${untilStr}), pruefe naechsten Key`);
|
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_HOST_COOLDOWN", {
|
|
||||||
reason: hostCooldownState.message,
|
|
||||||
category: hostCooldownState.category,
|
|
||||||
host: linkHoster,
|
|
||||||
until: untilStr
|
|
||||||
});
|
|
||||||
cooldownFailures.push(`Debrid-Link${keyLabel}: ${hostCooldownState.message}`);
|
|
||||||
if (!earliestCooldownUntil || hostCooldownState.until < earliestCooldownUntil) {
|
|
||||||
earliestCooldownUntil = hostCooldownState.until;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CLEAR per-key TEST log line BEFORE the network call, so the user
|
// CLEAR per-key TEST log line BEFORE the network call, so the user
|
||||||
// can always see exactly which key is currently being tested for
|
// can always see exactly which key is currently being tested for
|
||||||
@ -2661,21 +2527,7 @@ class DebridLinkClient {
|
|||||||
});
|
});
|
||||||
failures.push(`Debrid-Link${keyLabel}: ${failure.message}`);
|
failures.push(`Debrid-Link${keyLabel}: ${failure.message}`);
|
||||||
if (failure.cooldownMs > 0) {
|
if (failure.cooldownMs > 0) {
|
||||||
if (failure.hostOnly) {
|
setDebridLinkKeyCooldownState(apiKey.id, failure.cooldownMs, failure.message, failure.category || "temporary");
|
||||||
// Per-(key, host) quota — block only this combination, not the
|
|
||||||
// whole key. The key remains "ready" for other hosters. If the
|
|
||||||
// hoster couldn't be parsed from the URL, the helper falls back
|
|
||||||
// to a key-wide cooldown (better safe than thrashing).
|
|
||||||
setDebridLinkKeyHostCooldownState(
|
|
||||||
apiKey.id,
|
|
||||||
failure.hoster || "",
|
|
||||||
failure.cooldownMs,
|
|
||||||
failure.message,
|
|
||||||
failure.category || "quota"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setDebridLinkKeyCooldownState(apiKey.id, failure.cooldownMs, failure.message, failure.category || "temporary");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
clearDebridLinkKeyCooldownState(apiKey.id);
|
clearDebridLinkKeyCooldownState(apiKey.id);
|
||||||
setDebridLinkKeyRuntimeStatus(apiKey.id, failure.category === "invalid" ? "invalid" : "error", failure.message);
|
setDebridLinkKeyRuntimeStatus(apiKey.id, failure.category === "invalid" ? "invalid" : "error", failure.message);
|
||||||
@ -2898,7 +2750,7 @@ class DebridLinkClient {
|
|||||||
apiKey: ReturnType<typeof parseDebridLinkApiKeys>[number],
|
apiKey: ReturnType<typeof parseDebridLinkApiKeys>[number],
|
||||||
link: string,
|
link: string,
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
): Promise<{ fatal: boolean; cooldownMs: number; message: string; category?: DebridLinkCooldownCategory; providerWide?: boolean; hostOnly?: boolean; hoster?: string }> {
|
): Promise<{ fatal: boolean; cooldownMs: number; message: string; category?: DebridLinkCooldownCategory; providerWide?: boolean }> {
|
||||||
const errorText = compactErrorText(error).replace(/^Error:\s*/i, "");
|
const errorText = compactErrorText(error).replace(/^Error:\s*/i, "");
|
||||||
if (error instanceof DebridLinkApiError) {
|
if (error instanceof DebridLinkApiError) {
|
||||||
const code = String(error.code || "").trim() || `HTTP ${error.status}`;
|
const code = String(error.code || "").trim() || `HTTP ${error.status}`;
|
||||||
@ -2920,29 +2772,13 @@ class DebridLinkClient {
|
|||||||
category: "rate_limit"
|
category: "rate_limit"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (DEBRID_LINK_HOST_QUOTA_ERRORS.has(code)) {
|
if (DEBRID_LINK_QUOTA_ERRORS.has(code)) {
|
||||||
// Per-(key, host) quota — only this host is exhausted for this key.
|
|
||||||
// The key remains usable for other hosters, so we mark the failure
|
|
||||||
// hostOnly and let the rotation loop apply a per-(key, host) cooldown.
|
|
||||||
const cooldownMs = await this.fetchQuotaCooldownMs(apiKey, signal);
|
const cooldownMs = await this.fetchQuotaCooldownMs(apiKey, signal);
|
||||||
const hosterRaw = extractHosterFromUrl(link);
|
const hoster = extractHosterFromUrl(link) || "host";
|
||||||
const hosterLabel = hosterRaw || "host";
|
|
||||||
return {
|
return {
|
||||||
fatal: false,
|
fatal: false,
|
||||||
cooldownMs,
|
cooldownMs,
|
||||||
message: `Quota erreicht fuer ${hosterLabel} (${code}: ${description})`,
|
message: `Quota erreicht fuer ${hoster} (${code}: ${description})`,
|
||||||
category: "quota",
|
|
||||||
hostOnly: true,
|
|
||||||
hoster: hosterRaw
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (DEBRID_LINK_KEY_QUOTA_ERRORS.has(code)) {
|
|
||||||
// Key-wide quota — whole key is exhausted, blocks all hosters.
|
|
||||||
const cooldownMs = await this.fetchQuotaCooldownMs(apiKey, signal);
|
|
||||||
return {
|
|
||||||
fatal: false,
|
|
||||||
cooldownMs,
|
|
||||||
message: `Quota erreicht (${code}: ${description})`,
|
|
||||||
category: "quota"
|
category: "quota"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -388,90 +388,6 @@ describe("debrid service", () => {
|
|||||||
expect(result.directUrl).toBe("https://debrid-link.example/quota-ok.bin");
|
expect(result.directUrl).toBe("https://debrid-link.example/quota-ok.bin");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("scopes Debrid-Link maxDataHost cooldown to the (key, host) pair so the key stays usable for other hosters", 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
|
|
||||||
};
|
|
||||||
|
|
||||||
const unrestrictAuthHeaders: string[] = [];
|
|
||||||
|
|
||||||
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("debrid-link.com/api/v2/downloader/limits")) {
|
|
||||||
return new Response(JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
value: { nextResetSeconds: { value: 900 } }
|
|
||||||
}), {
|
|
||||||
status: 200,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only count calls to /downloader/add (the unrestrict endpoint)
|
|
||||||
if (url.includes("/downloader/add")) {
|
|
||||||
unrestrictAuthHeaders.push(authHeader);
|
|
||||||
// Read the body to know which link is being unrestricted
|
|
||||||
const bodyText = init?.body ? String(init.body) : "";
|
|
||||||
const isRapidgator = /rapidgator/i.test(bodyText);
|
|
||||||
// Only key-one + rapidgator returns maxDataHost. All other (key, host)
|
|
||||||
// combinations succeed.
|
|
||||||
if (authHeader === "Bearer dl-key-one" && isRapidgator) {
|
|
||||||
return new Response(JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
error: "maxDataHost",
|
|
||||||
error_description: "host quota reached"
|
|
||||||
}), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
||||||
}
|
|
||||||
return new Response(JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
value: {
|
|
||||||
downloadUrl: `https://debrid-link.example/${authHeader.slice(-3)}-${isRapidgator ? "rg" : "ot"}.bin`,
|
|
||||||
name: "ok.bin",
|
|
||||||
size: 1024
|
|
||||||
}
|
|
||||||
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response("not-found", { status: 404 });
|
|
||||||
}) as typeof fetch;
|
|
||||||
|
|
||||||
const service = new DebridService(settings);
|
|
||||||
|
|
||||||
// 1) First rapidgator: key-one hits maxDataHost → key-two succeeds.
|
|
||||||
const r1 = await service.unrestrictLink("https://rapidgator.net/file/first");
|
|
||||||
expect(r1.providerLabel).toContain("Key 2");
|
|
||||||
|
|
||||||
// 2) Second rapidgator request: key-one MUST be skipped (host cooldown
|
|
||||||
// on (key1, rapidgator)), only key-two should be tried.
|
|
||||||
unrestrictAuthHeaders.length = 0;
|
|
||||||
const r2 = await service.unrestrictLink("https://rapidgator.net/file/second");
|
|
||||||
expect(unrestrictAuthHeaders).toEqual(["Bearer dl-key-two"]);
|
|
||||||
expect(r2.providerLabel).toContain("Key 2");
|
|
||||||
|
|
||||||
// 3) Different host: key-one must NOT be skipped — its host-cooldown is
|
|
||||||
// only for rapidgator, not for uploaded.net.
|
|
||||||
unrestrictAuthHeaders.length = 0;
|
|
||||||
const r3 = await service.unrestrictLink("https://uploaded.net/file/third");
|
|
||||||
expect(unrestrictAuthHeaders).toEqual(["Bearer dl-key-one"]);
|
|
||||||
expect(r3.providerLabel).toContain("Key 1");
|
|
||||||
});
|
|
||||||
|
|
||||||
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(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user