Compare commits
2 Commits
d594c5082b
...
471e40b87f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
471e40b87f | ||
|
|
26df55f7ba |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.225",
|
"version": "1.7.226",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -288,6 +288,7 @@ type MegaDebridCooldownCategory = "invalid" | "rate_limit" | "quota" | "temporar
|
|||||||
type MegaDebridCooldownDetail = { until: number; message: string; category: MegaDebridCooldownCategory; untilRestart?: boolean };
|
type MegaDebridCooldownDetail = { until: number; message: string; category: MegaDebridCooldownCategory; untilRestart?: boolean };
|
||||||
const megaDebridAccountCooldowns = new Map<string, MegaDebridCooldownDetail>();
|
const megaDebridAccountCooldowns = new Map<string, MegaDebridCooldownDetail>();
|
||||||
const MEGA_DEBRID_ACCOUNT_COOLDOWN_MS = 120_000;
|
const MEGA_DEBRID_ACCOUNT_COOLDOWN_MS = 120_000;
|
||||||
|
const MEGA_DEBRID_SLOW_LINK_RETRY_MS = 120_000;
|
||||||
const MEGA_DEBRID_INVALID_ACCOUNT_COOLDOWN_MS = 60 * 60 * 1000;
|
const MEGA_DEBRID_INVALID_ACCOUNT_COOLDOWN_MS = 60 * 60 * 1000;
|
||||||
|
|
||||||
// A Mega-Web account abort (the shared unrestrict timeout firing while this
|
// A Mega-Web account abort (the shared unrestrict timeout firing while this
|
||||||
@ -2068,15 +2069,33 @@ class MegaDebridClient {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const elapsedMs = Date.now() - testStartedAt;
|
const elapsedMs = Date.now() - testStartedAt;
|
||||||
const abortText = compactErrorText(error).replace(/^Error:\s*/i, "");
|
const abortText = compactErrorText(error).replace(/^Error:\s*/i, "");
|
||||||
// Timeout/abort on THIS account (the shared unrestrict signal fired). Cool
|
// Timeout/abort on THIS account (the shared unrestrict timeout fired). The
|
||||||
// the account down — if it actually ran, not a quick user-cancel — so the
|
// account-wide cooldown exists ONLY to make the retry rotate to another
|
||||||
// download-manager's retry rotates to the NEXT account instead of hammering
|
// account — so it is set only when another usable account actually exists.
|
||||||
// this one. The shared signal is now aborted, so we stop this pass; the
|
// With no rotation target (single account / all others busy), cooling the
|
||||||
// retry runs the rotation fresh with this account skipped. A genuine cancel
|
// sole account would freeze EVERY queued item while the account is healthy;
|
||||||
// is not retried by the caller, so the cooldown is harmless there.
|
// a >60s timeout is a slow-LINK signal, not an unhealthy-account signal, so
|
||||||
|
// we park just this link (mega_debrid_slow_link) and leave the account free
|
||||||
|
// for other items. A quick user-cancel (below the min run) parks nothing.
|
||||||
if (/aborted/i.test(abortText) && !/timeout/i.test(abortText)) {
|
if (/aborted/i.test(abortText) && !/timeout/i.test(abortText)) {
|
||||||
const ranLongEnough = elapsedMs >= getMegaDebridAbortMinRunMs();
|
const ranLongEnough = elapsedMs >= getMegaDebridAbortMinRunMs();
|
||||||
if (ranLongEnough) {
|
const otherUsableAccounts = orderedEntries.reduce((count, candidate) => {
|
||||||
|
if (candidate.account.id === account.id) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
if (isMegaDebridAccountDisabled(settings, candidate.account.id)) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
if (isMegaDebridAccountDailyLimitReached(settings, candidate.account.id)) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
if (getMegaDebridAccountCooldownState(`${candidate.account.id}:${mode}`)) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
return count + 1;
|
||||||
|
}, 0);
|
||||||
|
const rotateToAnotherAccount = ranLongEnough && otherUsableAccounts > 0;
|
||||||
|
if (rotateToAnotherAccount) {
|
||||||
setMegaDebridAccountCooldownState(cooldownKey, MEGA_DEBRID_ACCOUNT_COOLDOWN_MS, `Abbruch/Timeout nach ${Math.ceil(elapsedMs / 1000)}s`, "temporary");
|
setMegaDebridAccountCooldownState(cooldownKey, MEGA_DEBRID_ACCOUNT_COOLDOWN_MS, `Abbruch/Timeout nach ${Math.ceil(elapsedMs / 1000)}s`, "temporary");
|
||||||
}
|
}
|
||||||
traceConversionPhase({
|
traceConversionPhase({
|
||||||
@ -2085,15 +2104,18 @@ class MegaDebridClient {
|
|||||||
account: rotationLabel,
|
account: rotationLabel,
|
||||||
workMs: elapsedMs,
|
workMs: elapsedMs,
|
||||||
outcome: "aborted",
|
outcome: "aborted",
|
||||||
detail: `${abortText}${ranLongEnough ? ` cd=${Math.ceil(MEGA_DEBRID_ACCOUNT_COOLDOWN_MS / 1000)}s` : ""}`
|
detail: `${abortText}${rotateToAnotherAccount ? ` cd=${Math.ceil(MEGA_DEBRID_ACCOUNT_COOLDOWN_MS / 1000)}s` : ranLongEnough ? ` slowlink=${Math.ceil(MEGA_DEBRID_SLOW_LINK_RETRY_MS / 1000)}s` : ""}`
|
||||||
});
|
});
|
||||||
failures.push(`Mega-Debrid${accountLabel}: ${abortText}`);
|
failures.push(`Mega-Debrid${accountLabel}: ${abortText}`);
|
||||||
logAccountRotation("WARN", providerName, rotationLabel, "TIMEOUT_COOLDOWN", {
|
logAccountRotation("WARN", providerName, rotationLabel, "TIMEOUT_COOLDOWN", {
|
||||||
elapsedMs,
|
elapsedMs,
|
||||||
reason: abortText,
|
reason: abortText,
|
||||||
cooldownSec: ranLongEnough ? Math.ceil(MEGA_DEBRID_ACCOUNT_COOLDOWN_MS / 1000) : 0,
|
cooldownSec: rotateToAnotherAccount ? Math.ceil(MEGA_DEBRID_ACCOUNT_COOLDOWN_MS / 1000) : 0,
|
||||||
next: "naechster Account beim Retry"
|
next: rotateToAnotherAccount ? "naechster Account beim Retry" : "Einzel-Retry (Account bleibt fuer andere Items frei)"
|
||||||
});
|
});
|
||||||
|
if (ranLongEnough && !rotateToAnotherAccount) {
|
||||||
|
throw new Error(`mega_debrid_slow_link:${MEGA_DEBRID_SLOW_LINK_RETRY_MS}:Mega-Debrid${accountLabel}: ${abortText}`);
|
||||||
|
}
|
||||||
throw new Error(`Mega-Debrid${accountLabel}: ${abortText}`);
|
throw new Error(`Mega-Debrid${accountLabel}: ${abortText}`);
|
||||||
}
|
}
|
||||||
const failure = MegaDebridClient.classifyAccountFailure(error);
|
const failure = MegaDebridClient.classifyAccountFailure(error);
|
||||||
|
|||||||
@ -660,6 +660,20 @@ export function parseMegaDebridCooldownRetry(errorText: string): { delayMs: numb
|
|||||||
return { delayMs, detail: text.replace(/mega_debrid_cooldown:\d+:/i, "").trim() };
|
return { delayMs, detail: text.replace(/mega_debrid_cooldown:\d+:/i, "").trim() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseMegaDebridSlowLinkRetry(errorText: string): { delayMs: number; detail: string } | null {
|
||||||
|
const text = String(errorText || "");
|
||||||
|
const match = text.match(/mega_debrid_slow_link:(\d+)/i);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw = Number(match[1]);
|
||||||
|
if (!Number.isFinite(raw) || raw <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const delayMs = Math.max(1000, Math.min(15 * 60 * 1000, raw));
|
||||||
|
return { delayMs, detail: text.replace(/mega_debrid_slow_link:\d+:/i, "").trim() };
|
||||||
|
}
|
||||||
|
|
||||||
export function parseMegaDebridResetPark(errorText: string): { delayMs: number; detail: string } | null {
|
export function parseMegaDebridResetPark(errorText: string): { delayMs: number; detail: string } | null {
|
||||||
const match = String(errorText || "").match(/mega_debrid_reset_park:(\d+):(.*)$/is);
|
const match = String(errorText || "").match(/mega_debrid_reset_park:(\d+):(.*)$/is);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
@ -9467,6 +9481,24 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const megaRawError = error instanceof Error ? String(error.message || "") : String(error || "");
|
const megaRawError = error instanceof Error ? String(error.message || "") : String(error || "");
|
||||||
|
const megaSlowLinkRetry = parseMegaDebridSlowLinkRetry(megaRawError);
|
||||||
|
if (megaSlowLinkRetry && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
|
active.unrestrictRetries += 1;
|
||||||
|
item.retries += 1;
|
||||||
|
item.provider = null;
|
||||||
|
logger.warn(`Mega-Debrid Link langsam (Timeout): item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${megaSlowLinkRetry.delayMs}ms, link=${item.url.slice(0, 80)}`);
|
||||||
|
this.queueRetry(
|
||||||
|
item,
|
||||||
|
active,
|
||||||
|
megaSlowLinkRetry.delayMs,
|
||||||
|
`Mega-Debrid: Link zu langsam, Einzel-Retry in ${Math.ceil(megaSlowLinkRetry.delayMs / 1000)}s`
|
||||||
|
);
|
||||||
|
item.lastError = megaSlowLinkRetry.detail || errorText;
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const megaCooldownRetry = parseMegaDebridCooldownRetry(megaRawError);
|
const megaCooldownRetry = parseMegaDebridCooldownRetry(megaRawError);
|
||||||
if (megaCooldownRetry && active.unrestrictRetries < maxUnrestrictRetries) {
|
if (megaCooldownRetry && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
active.unrestrictRetries += 1;
|
active.unrestrictRetries += 1;
|
||||||
|
|||||||
@ -2071,6 +2071,55 @@ describe("debrid service", () => {
|
|||||||
expect(calls).toBeGreaterThanOrEqual(1);
|
expect(calls).toBeGreaterThanOrEqual(1);
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
|
it("single Mega-Debrid account: a long Web abort parks only the slow link and does NOT freeze the sole account", async () => {
|
||||||
|
process.env.RD_MEGA_ABORT_MIN_RUN_MS = "0";
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
token: "",
|
||||||
|
bestToken: "",
|
||||||
|
allDebridToken: "",
|
||||||
|
megaLogin: "user",
|
||||||
|
megaPassword: "pass",
|
||||||
|
megaCredentials: "user:pass",
|
||||||
|
megaDebridPreferApi: false,
|
||||||
|
providerOrder: [] as const,
|
||||||
|
providerPrimary: "megadebrid" as const,
|
||||||
|
providerSecondary: "none" as const,
|
||||||
|
providerTertiary: "none" as const,
|
||||||
|
autoProviderFallback: false
|
||||||
|
};
|
||||||
|
globalThis.fetch = (async () => new Response("error", { status: 500 })) as typeof fetch;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
let calls = 0;
|
||||||
|
const megaWeb = vi.fn((): Promise<{ fileName: string; directUrl: string; fileSize: number | null; retriesUsed: number }> => {
|
||||||
|
calls += 1;
|
||||||
|
if (calls === 1) {
|
||||||
|
controller.abort("simulated-60s-timeout");
|
||||||
|
return Promise.reject(new Error("aborted"));
|
||||||
|
}
|
||||||
|
return Promise.resolve({
|
||||||
|
fileName: "healthy.rar",
|
||||||
|
directUrl: "https://www11.unrestrict.link/download/file/ok/healthy.rar",
|
||||||
|
fileSize: null,
|
||||||
|
retriesUsed: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||||
|
|
||||||
|
const err = await service.unrestrictLink("https://rapidgator.net/file/slow-link.rar.html", controller.signal).then(() => null, (e: unknown) => e);
|
||||||
|
expect(err).toBeTruthy();
|
||||||
|
expect(String(err)).toMatch(/mega_debrid_slow_link:\d+:/i);
|
||||||
|
|
||||||
|
const key = `${getMegaDebridAccountId("user")}:web`;
|
||||||
|
expect(getMegaDebridAccountCooldownState(key)).toBeNull();
|
||||||
|
|
||||||
|
const second = await service.unrestrictLink("https://rapidgator.net/file/healthy.rar.html");
|
||||||
|
expect(second.provider).toBe("megadebrid");
|
||||||
|
expect(calls).toBeGreaterThanOrEqual(2);
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
it("escalates a Mega-Debrid account to 'until restart' after the empty-response streak threshold", () => {
|
it("escalates a Mega-Debrid account to 'until restart' after the empty-response streak threshold", () => {
|
||||||
const key = `${getMegaDebridAccountId("user1")}:web`;
|
const key = `${getMegaDebridAccountId("user1")}:web`;
|
||||||
expect(MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART).toBe(3);
|
expect(MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART).toBe(3);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { transientResolveRetryDelayMs, parseMegaDebridCooldownRetry, parseMegaDebridResetPark } from "../src/main/download-manager";
|
import { transientResolveRetryDelayMs, parseMegaDebridCooldownRetry, parseMegaDebridResetPark, parseMegaDebridSlowLinkRetry } from "../src/main/download-manager";
|
||||||
|
|
||||||
describe("transientResolveRetryDelayMs (fast, bounded retry for transient resolve failures)", () => {
|
describe("transientResolveRetryDelayMs (fast, bounded retry for transient resolve failures)", () => {
|
||||||
it("starts fast (<= 3s) instead of the 5s..120s exponential", () => {
|
it("starts fast (<= 3s) instead of the 5s..120s exponential", () => {
|
||||||
@ -63,6 +63,31 @@ describe("parseMegaDebridCooldownRetry (honor the encoded account-cooldown delay
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("parseMegaDebridSlowLinkRetry (park only the slow link, never the account)", () => {
|
||||||
|
it("parses the encoded delay from a slow-link error", () => {
|
||||||
|
const r = parseMegaDebridSlowLinkRetry("mega_debrid_slow_link:120000:Mega-Debrid (Account 1/1, Su******e3): aborted");
|
||||||
|
expect(r).not.toBeNull();
|
||||||
|
expect(r!.delayMs).toBe(120000);
|
||||||
|
expect(r!.detail).toContain("Mega-Debrid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses it when embedded in the aggregated provider-chain error", () => {
|
||||||
|
const aggregated = "Provider-Kette: Mega-Debrid Web fehlgeschlagen (Error: mega_debrid_slow_link:90000:Mega-Debrid (Account 1/1): aborted)";
|
||||||
|
expect(parseMegaDebridSlowLinkRetry(aggregated)!.delayMs).toBe(90000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps to [1s, 15min]", () => {
|
||||||
|
expect(parseMegaDebridSlowLinkRetry("mega_debrid_slow_link:1:x")!.delayMs).toBe(1000);
|
||||||
|
expect(parseMegaDebridSlowLinkRetry("mega_debrid_slow_link:99999999:x")!.delayMs).toBe(15 * 60 * 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not collide with the account-cooldown or reset-park tokens", () => {
|
||||||
|
expect(parseMegaDebridSlowLinkRetry("mega_debrid_cooldown:20330:x")).toBeNull();
|
||||||
|
expect(parseMegaDebridSlowLinkRetry("mega_debrid_reset_park:43200000:x")).toBeNull();
|
||||||
|
expect(parseMegaDebridCooldownRetry("mega_debrid_slow_link:120000:x")).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("parseMegaDebridResetPark (park the item until the Tagesreset, not a ~2min generic retry)", () => {
|
describe("parseMegaDebridResetPark (park the item until the Tagesreset, not a ~2min generic retry)", () => {
|
||||||
it("parses the encoded until-reset delay from the park token", () => {
|
it("parses the encoded until-reset delay from the park token", () => {
|
||||||
const r = parseMegaDebridResetPark("mega_debrid_reset_park:43200000:Mega-Debrid: Alle Accounts am Tageslimit (bis zum Tagesreset gesperrt)");
|
const r = parseMegaDebridResetPark("mega_debrid_reset_park:43200000:Mega-Debrid: Alle Accounts am Tageslimit (bis zum Tagesreset gesperrt)");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user