Compare commits
No commits in common. "471e40b87fab6083326feed9feb27b7a24dad948" and "d594c5082b6c5aed39baaa73d6ea70cde61060e4" have entirely different histories.
471e40b87f
...
d594c5082b
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.226",
|
"version": "1.7.225",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -288,7 +288,6 @@ 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
|
||||||
@ -2069,33 +2068,15 @@ 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 timeout fired). The
|
// Timeout/abort on THIS account (the shared unrestrict signal fired). Cool
|
||||||
// account-wide cooldown exists ONLY to make the retry rotate to another
|
// the account down — if it actually ran, not a quick user-cancel — so the
|
||||||
// account — so it is set only when another usable account actually exists.
|
// download-manager's retry rotates to the NEXT account instead of hammering
|
||||||
// With no rotation target (single account / all others busy), cooling the
|
// this one. The shared signal is now aborted, so we stop this pass; the
|
||||||
// sole account would freeze EVERY queued item while the account is healthy;
|
// retry runs the rotation fresh with this account skipped. A genuine cancel
|
||||||
// a >60s timeout is a slow-LINK signal, not an unhealthy-account signal, so
|
// is not retried by the caller, so the cooldown is harmless there.
|
||||||
// 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();
|
||||||
const otherUsableAccounts = orderedEntries.reduce((count, candidate) => {
|
if (ranLongEnough) {
|
||||||
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({
|
||||||
@ -2104,18 +2085,15 @@ class MegaDebridClient {
|
|||||||
account: rotationLabel,
|
account: rotationLabel,
|
||||||
workMs: elapsedMs,
|
workMs: elapsedMs,
|
||||||
outcome: "aborted",
|
outcome: "aborted",
|
||||||
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` : ""}`
|
detail: `${abortText}${ranLongEnough ? ` cd=${Math.ceil(MEGA_DEBRID_ACCOUNT_COOLDOWN_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: rotateToAnotherAccount ? Math.ceil(MEGA_DEBRID_ACCOUNT_COOLDOWN_MS / 1000) : 0,
|
cooldownSec: ranLongEnough ? Math.ceil(MEGA_DEBRID_ACCOUNT_COOLDOWN_MS / 1000) : 0,
|
||||||
next: rotateToAnotherAccount ? "naechster Account beim Retry" : "Einzel-Retry (Account bleibt fuer andere Items frei)"
|
next: "naechster Account beim Retry"
|
||||||
});
|
});
|
||||||
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,20 +660,6 @@ 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) {
|
||||||
@ -9481,24 +9467,6 @@ 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,55 +2071,6 @@ 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, parseMegaDebridSlowLinkRetry } from "../src/main/download-manager";
|
import { transientResolveRetryDelayMs, parseMegaDebridCooldownRetry, parseMegaDebridResetPark } 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,31 +63,6 @@ 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