Fix: Selbst-Lahmlegung der Mega-Debrid-API beheben (1-Token-pro-Account + falscher 60-min-Cooldown)

Live aus conversion.log belegt: bei API-first + maxParallel hammerten viele
parallele Umwandlungen DENSELBEN Account → Mega-Debrids 1-Token-pro-Account-
Regel → "Token error, please log-in" (Token gegenseitig invalidiert). Ein
einzelner generischer Null-getLink wurde dann als "Login oder Unrestrict
fehlgeschlagen" geworfen → classifyAccountFailure matchte das Wort "Login" →
category=invalid → 60-MINUTEN-Cooldown auf einen NACHWEISLICH funktionierenden
Account (39x OK davor, klappt auch auf der Webseite). Folge: alle Accounts
cooled, alle Slots haengen an Fehl-Umwandlungen (conv12/dl0), nichts laedt.

Fix A (download-manager): getSerializedValidatingLimit gilt jetzt auch fuer
megadebrid-api (nicht nur -web) = Anzahl nutzbarer Accounts ohne :api-Cooldown.
Damit laeuft hoechstens EINE API-Umwandlung pro Account gleichzeitig (Rotation
verteilt 1/Account) — respektiert die 1-Token-Regel, verhindert die Token-
Kollision und gibt Download-Slots frei.

Fix B (debrid): (1) generische Wurf-Meldung "Login oder Unrestrict
fehlgeschlagen" -> "Linkgenerierung lieferte kein Ergebnis" (kein "Login"-
Trigger mehr). (2) classifyAccountFailure: "token error"/"please log-in" ->
kurzer temporary-Cooldown (15s); invalid-Branch nur noch bei ECHTEN
Credential-Fehlern (bad login/incorrect password/invalid credentials/
unauthorized/forbidden/connectUser) statt losem login|auth. Ein transienter
Fehler sperrt einen guten Account nicht mehr 60 min, sondern hoechstens Sekunden.

Tests: API-Serialisierung 2-Acct->2-parallel + 1-Acct->1-at-a-time; kein
60-min-invalid bei Null-Ergebnis; token-error<60s. 844/844 gruen, tsc=6.
This commit is contained in:
Sucukdeluxe
2026-06-17 03:30:04 +02:00
parent 21803f316b
commit 73004f3864
4 changed files with 241 additions and 4 deletions
+154
View File
@@ -6721,6 +6721,160 @@ describe("download manager", () => {
await new Promise((resolve) => setTimeout(resolve, 150));
});
it("serializes Mega-Debrid API conversions to one per account (no single-token hammering)", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
let getLinkCalls = 0;
const pendingRejectors = new Set<(error: Error) => void>();
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url;
if (url.includes("action=connectUser")) {
return new Response(JSON.stringify({ response_code: "ok", token: `tok-${getLinkCalls}-${url.length}` }), { status: 200, headers: { "Content-Type": "application/json" } });
}
if (url.includes("action=getLink")) {
getLinkCalls += 1;
const signal = init?.signal;
return await new Promise<Response>((_resolve, reject) => {
const rejector = (error: Error): void => {
signal?.removeEventListener("abort", onAbort);
pendingRejectors.delete(rejector);
reject(error);
};
const onAbort = (): void => { rejector(new Error("aborted:test-api")); };
if (signal?.aborted) { onAbort(); return; }
signal?.addEventListener("abort", onAbort, { once: true });
pendingRejectors.add(rejector);
});
}
return originalFetch(input, init);
}) as typeof fetch;
const manager = new DownloadManager(
{
...defaultSettings(),
megaCredentials: "mega-user-a:pass-a\nmega-user-b:pass-b",
megaDebridApiEnabled: true,
megaDebridWebEnabled: false,
megaDebridPreferApi: true,
providerOrder: [],
providerPrimary: "megadebrid",
providerSecondary: "none",
providerTertiary: "none",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: false,
autoReconnect: false,
enableIntegrityCheck: false,
maxParallel: 6
},
emptySession(),
createStoragePaths(path.join(root, "state")),
{}
);
manager.addPackages([{
name: "mega-api-serialized",
links: [
"https://rapidgator.net/file/api-1.part1.rar.html",
"https://rapidgator.net/file/api-2.part2.rar.html",
"https://rapidgator.net/file/api-3.part3.rar.html"
]
}]);
await manager.start();
await waitFor(() => getLinkCalls === 2, 10000);
await new Promise((resolve) => setTimeout(resolve, 250));
const items = Object.values(manager.getSnapshot().session.items);
expect(items.filter((item) => item.status === "validating")).toHaveLength(2);
expect(items.filter((item) => item.status === "queued")).toHaveLength(1);
expect(getLinkCalls).toBe(2);
manager.stop();
for (const reject of Array.from(pendingRejectors)) {
reject(new Error("aborted:test-api"));
}
await new Promise((resolve) => setTimeout(resolve, 150));
});
it("limits Mega-Debrid API conversions to one at a time with a single account", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
let getLinkCalls = 0;
const pendingRejectors = new Set<(error: Error) => void>();
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url;
if (url.includes("action=connectUser")) {
return new Response(JSON.stringify({ response_code: "ok", token: "tok-single" }), { status: 200, headers: { "Content-Type": "application/json" } });
}
if (url.includes("action=getLink")) {
getLinkCalls += 1;
const signal = init?.signal;
return await new Promise<Response>((_resolve, reject) => {
const rejector = (error: Error): void => {
signal?.removeEventListener("abort", onAbort);
pendingRejectors.delete(rejector);
reject(error);
};
const onAbort = (): void => { rejector(new Error("aborted:test-api")); };
if (signal?.aborted) { onAbort(); return; }
signal?.addEventListener("abort", onAbort, { once: true });
pendingRejectors.add(rejector);
});
}
return originalFetch(input, init);
}) as typeof fetch;
const manager = new DownloadManager(
{
...defaultSettings(),
megaCredentials: "mega-user:mega-pass",
megaDebridApiEnabled: true,
megaDebridWebEnabled: false,
megaDebridPreferApi: true,
providerOrder: [],
providerPrimary: "megadebrid",
providerSecondary: "none",
providerTertiary: "none",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: false,
autoReconnect: false,
enableIntegrityCheck: false,
maxParallel: 6
},
emptySession(),
createStoragePaths(path.join(root, "state")),
{}
);
manager.addPackages([{
name: "mega-api-single",
links: [
"https://rapidgator.net/file/single-1.part1.rar.html",
"https://rapidgator.net/file/single-2.part2.rar.html",
"https://rapidgator.net/file/single-3.part3.rar.html"
]
}]);
await manager.start();
await waitFor(() => getLinkCalls === 1, 10000);
await new Promise((resolve) => setTimeout(resolve, 250));
const items = Object.values(manager.getSnapshot().session.items);
expect(items.filter((item) => item.status === "validating")).toHaveLength(1);
expect(items.filter((item) => item.status === "queued")).toHaveLength(2);
expect(getLinkCalls).toBe(1);
manager.stop();
for (const reject of Array.from(pendingRejectors)) {
reject(new Error("aborted:test-api"));
}
await new Promise((resolve) => setTimeout(resolve, 150));
});
it("shows the same AllDebrid countdown for all immediately free slots", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);