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:
parent
21803f316b
commit
73004f3864
@ -1897,7 +1897,7 @@ class MegaDebridClient {
|
|||||||
logger.info(`Mega-Debrid (API) unrestrict OK: ${apiResult.fileName}`);
|
logger.info(`Mega-Debrid (API) unrestrict OK: ${apiResult.fileName}`);
|
||||||
return apiResult;
|
return apiResult;
|
||||||
}
|
}
|
||||||
throw new Error("Mega-Debrid API: Login oder Unrestrict fehlgeschlagen");
|
throw new Error("Mega-Debrid API: Linkgenerierung lieferte kein Ergebnis");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorText = compactErrorText(error);
|
const errorText = compactErrorText(error);
|
||||||
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||||
@ -2162,7 +2162,11 @@ class MegaDebridClient {
|
|||||||
return { fatal: true, cooldownMs: 0, message: errorText, category: "temporary" };
|
return { fatal: true, cooldownMs: 0, message: errorText, category: "temporary" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/login|password|auth|credentials|unauthorized|forbidden/i.test(errorText) || /connectUser/i.test(errorText)) {
|
if (/token.?error|please log.?in/i.test(errorText)) {
|
||||||
|
return { fatal: false, cooldownMs: 15_000, message: errorText, category: "temporary" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/bad.?login|incorrect.?(login|password)|invalid.?(login|password|credentials)|wrong.?password|unauthorized|forbidden|connectUser/i.test(errorText)) {
|
||||||
return {
|
return {
|
||||||
fatal: false,
|
fatal: false,
|
||||||
cooldownMs: MEGA_DEBRID_INVALID_ACCOUNT_COOLDOWN_MS,
|
cooldownMs: MEGA_DEBRID_INVALID_ACCOUNT_COOLDOWN_MS,
|
||||||
|
|||||||
@ -7998,9 +7998,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
||||||
if (provider === "megadebrid-web") {
|
if (provider === "megadebrid-web" || provider === "megadebrid-api") {
|
||||||
|
const mode = provider === "megadebrid-web" ? "web" : "api";
|
||||||
const usableAccounts = getAvailableMegaDebridAccounts(this.settings)
|
const usableAccounts = getAvailableMegaDebridAccounts(this.settings)
|
||||||
.filter((account) => !getMegaDebridAccountCooldownState(`${account.id}:web`))
|
.filter((account) => !getMegaDebridAccountCooldownState(`${account.id}:${mode}`))
|
||||||
.length;
|
.length;
|
||||||
return Math.max(1, usableAccounts);
|
return Math.max(1, usableAccounts);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1339,6 +1339,84 @@ describe("debrid service", () => {
|
|||||||
expect(getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`)).toBeNull();
|
expect(getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does NOT slap a 60-minute 'invalid' cooldown on a working account when the API returns no result (transient)", async () => {
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
token: "",
|
||||||
|
bestToken: "",
|
||||||
|
allDebridToken: "",
|
||||||
|
megaLogin: "user",
|
||||||
|
megaPassword: "pass",
|
||||||
|
megaCredentials: "user:pass",
|
||||||
|
megaDebridApiEnabled: true,
|
||||||
|
megaDebridWebEnabled: false,
|
||||||
|
providerPrimary: "megadebrid-api" as const,
|
||||||
|
providerSecondary: "none" 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("action=connectUser")) {
|
||||||
|
return new Response(JSON.stringify({ response_code: "ok", token: "tok", vip_end: Math.floor(Date.now() / 1000) + 999999 }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
if (url.includes("action=getLink")) {
|
||||||
|
return new Response(JSON.stringify({ response_code: "ok" }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
return new Response("not-found", { status: 404 });
|
||||||
|
}) as typeof fetch;
|
||||||
|
|
||||||
|
const service = new DebridService(settings);
|
||||||
|
const err = await service.unrestrictLink("https://rapidgator.net/file/no-result.rar.html").then(() => null, (e: unknown) => e);
|
||||||
|
expect(err).toBeTruthy();
|
||||||
|
expect(String(err)).not.toMatch(/Login oder Unrestrict/i);
|
||||||
|
|
||||||
|
const cooldown = getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`);
|
||||||
|
if (cooldown) {
|
||||||
|
expect(cooldown.category).not.toBe("invalid");
|
||||||
|
expect(cooldown.remainingMs).toBeLessThan(5 * 60 * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a Mega-Debrid API 'Token error, please log-in' as a short transient cooldown, not invalid", async () => {
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
token: "",
|
||||||
|
bestToken: "",
|
||||||
|
allDebridToken: "",
|
||||||
|
megaLogin: "user",
|
||||||
|
megaPassword: "pass",
|
||||||
|
megaCredentials: "user:pass",
|
||||||
|
megaDebridApiEnabled: true,
|
||||||
|
megaDebridWebEnabled: false,
|
||||||
|
providerPrimary: "megadebrid-api" as const,
|
||||||
|
providerSecondary: "none" 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("action=connectUser")) {
|
||||||
|
return new Response(JSON.stringify({ response_code: "ok", token: "tok", vip_end: Math.floor(Date.now() / 1000) + 999999 }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
if (url.includes("action=getLink")) {
|
||||||
|
return new Response(JSON.stringify({ response_code: "error_token", response_text: "Token error, please log-in" }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
return new Response("not-found", { status: 404 });
|
||||||
|
}) as typeof fetch;
|
||||||
|
|
||||||
|
const service = new DebridService(settings);
|
||||||
|
await service.unrestrictLink("https://rapidgator.net/file/token-collision.rar.html").then(() => null, (e: unknown) => e);
|
||||||
|
|
||||||
|
const cooldown = getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`);
|
||||||
|
if (cooldown) {
|
||||||
|
expect(cooldown.category).not.toBe("invalid");
|
||||||
|
expect(cooldown.remainingMs).toBeLessThan(60 * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("uses Mega Web only when it is configured as a separate fallback provider", async () => {
|
it("uses Mega Web only when it is configured as a separate fallback provider", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
|
|||||||
@ -6721,6 +6721,160 @@ describe("download manager", () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
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 () => {
|
it("shows the same AllDebrid countdown for all immediately free slots", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user