diff --git a/src/main/debrid.ts b/src/main/debrid.ts index dd22fe5..19f370c 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -6,6 +6,7 @@ import { APP_VERSION, REQUEST_RETRIES } from "./constants"; import { logger } from "./logger"; import { logAccountRotation } from "./account-rotation-log"; import { RealDebridClient, UnrestrictedLink } from "./realdebrid"; +import { MEGA_DEBRID_NO_SERVER_RE } from "./mega-web-fallback"; import { isMegaFileUrl, resolveMegaFilename } from "./mega-public-api"; import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils"; @@ -1866,8 +1867,11 @@ class MegaDebridClient { if (!lastError) { lastError = "Mega-Web Antwort leer"; } - // Don't retry permanent hoster errors (dead link, file removed, etc.) - if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(lastError)) { + // Don't retry permanent hoster errors (dead link, file removed, etc.) — and + // don't hammer a "Kein Server für diesen Hoster" (account hoster quota) message: + // immediate retries are futile (the limit persists) and waste the shared + // rotation budget, so break and let the rotation move to the next account. + if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(lastError) || MEGA_DEBRID_NO_SERVER_RE.test(lastError)) { break; } if (attempt < REQUEST_RETRIES) { @@ -2085,6 +2089,17 @@ class MegaDebridClient { }; } + // "Kein Server für diesen Hoster verfügbar" = Account-Tageslimit für diesen + // Hoster erschöpft (oder Hoster kurz nicht bedient). Quota-Cooldown, nächster Account. + if (MEGA_DEBRID_NO_SERVER_RE.test(errorText)) { + return { + fatal: false, + cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS, + message: "Kein Server fuer diesen Hoster (Tageslimit/Hoster nicht verfuegbar)", + category: "quota" + }; + } + // Rate limit if (/rate.?limit|too.?many|429/i.test(errorText)) { return { diff --git a/src/main/mega-web-fallback.ts b/src/main/mega-web-fallback.ts index da0d350..5d5273d 100644 --- a/src/main/mega-web-fallback.ts +++ b/src/main/mega-web-fallback.ts @@ -16,6 +16,15 @@ const DEBRID_URL = "https://www.mega-debrid.eu/index.php?form=debrid"; const DEBRID_AJAX_URL = "https://www.mega-debrid.eu/index.php?ajax=debrid&json"; const DEBRID_REFERER = "https://www.mega-debrid.eu/index.php?page=debrideur&lang=de"; +/** + * Mega-Debrid-Antwort "Kein Server für diesen Hoster verfügbar". Kommt zurück, wenn + * das Tageslimit DIESES Accounts für den Hoster erschöpft ist (oder der Hoster kurz + * nicht bedient wird). KEIN Session-/Leer-Fall — der Account soll schnell scheitern, + * damit die Multi-Account-Rotation sofort zum nächsten (nicht limitierten) Account + * wechselt, statt re-Login + Retry-Sturm das geteilte Unrestrict-Budget zu fressen. + */ +export const MEGA_DEBRID_NO_SERVER_RE = /kein server f(?:ü|u)r diesen hoster|no server (?:is )?available for this host|aucun serveur disponible/i; + function normalizeLink(link: string): string { return link.trim().toLowerCase(); } @@ -395,6 +404,15 @@ export class MegaWebFallback { await sleepWithSignal(1200, signal); continue; } + const serverMsg = (parsed.text || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); + // "Kein Server für diesen Hoster verfügbar" = Account-Tageslimit erschöpft. + // Surface die Meldung statt null zurückzugeben — sonst re-loggt unrestrict() + // ein + pollt erneut (Retry-Sturm), was bei einem limitierten Account zwecklos + // ist und das geteilte Rotations-Budget verbrennt. So scheitert der Account + // schnell und die Rotation nutzt den nächsten Account. + if (serverMsg && MEGA_DEBRID_NO_SERVER_RE.test(serverMsg)) { + throw new Error(`Mega-Web: ${serverMsg}`); + } return null; } diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 0638378..d49599d 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -1482,6 +1482,46 @@ describe("debrid service", () => { expect(megaWeb).toHaveBeenCalledTimes(1); }, 20000); + it("fails fast on Mega-Debrid hoster quota ('Kein Server') and rotates to the next account", async () => { + // User-Report: Account 1 am Tageslimit liefert "Kein Server für diesen Hoster + // verfügbar". Frueher lief das durch die volle Retry-Maschine (re-Login + 3x) und + // fraß das geteilte Rotations-Budget -> der funktionierende Account 2 lief in den + // Timeout (aborted:debrid -> fatal). Jetzt: schnell scheitern (1 Versuch) + rotieren. + const settings = { + ...defaultSettings(), + token: "", + bestToken: "", + allDebridToken: "", + megaLogin: "user1", + megaPassword: "pass1", + megaCredentials: "user1:pass1\nuser2:pass2", + 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; + + let calls = 0; + const megaWeb = vi.fn(async () => { + calls += 1; + if (calls === 1) { + throw new Error("Mega-Web: Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal."); + } + return { fileName: "acc2.rar", directUrl: "https://mega-web.example/acc2.rar", fileSize: null, retriesUsed: 0 }; + }); + + const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); + const result = await service.unrestrictLink("https://rapidgator.net/file/quota-rotate-test"); + + expect((result as { sourceAccountId?: string }).sourceAccountId).toBe(getMegaDebridAccountId("user2")); + expect(result.directUrl).toBe("https://mega-web.example/acc2.rar"); + // Fail-fast: acc1 darf NICHT 3x (REQUEST_RETRIES) probiert werden -> genau 1x acc1, dann acc2. + expect(calls).toBe(2); + }, 20000); + it("respects provider selection and does not append hidden providers", async () => { const settings = { ...defaultSettings(), diff --git a/tests/mega-web-fallback.test.ts b/tests/mega-web-fallback.test.ts index 53df5e2..405a1a1 100644 --- a/tests/mega-web-fallback.test.ts +++ b/tests/mega-web-fallback.test.ts @@ -60,6 +60,35 @@ describe("mega-web-fallback", () => { expect(fetchCallCount).toBe(4); }); + it("fails fast on 'Kein Server für diesen Hoster' (account hoster quota) instead of re-login + re-poll", async () => { + let ajaxCalls = 0; + globalThis.fetch = vi.fn(async (url: string | URL | Request) => { + const urlStr = String(url); + if (urlStr.includes("form=login")) { + const headers = new Headers(); + headers.append("set-cookie", "session=goodcookie; path=/"); + return new Response("", { headers, status: 200 }); + } + if (urlStr.includes("page=debrideur")) { + return new Response('
', { status: 200 }); + } + if (urlStr.includes("form=debrid")) { + return new Response(`

Link: https://mega.debrid/l1

d
`, { status: 200 }); + } + if (urlStr.includes("ajax=debrid")) { + ajaxCalls += 1; + return new Response(JSON.stringify({ link: "", text: "Erreur : Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal." }), { status: 200 }); + } + return new Response("Not found", { status: 404 }); + }) as unknown as typeof fetch; + + const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" })); + // Muss schnell mit der ECHTEN Meldung scheitern — NICHT null zurückgeben (was + // re-Login + erneutes Pollen auslösen würde und das Rotations-Budget frisst). + await expect(fallback.unrestrict("https://mega.debrid/l1")).rejects.toThrow(/kein server für diesen hoster/i); + expect(ajaxCalls).toBe(1); + }); + it("throws if login fails to set cookie", async () => { globalThis.fetch = vi.fn(async (url: string | URL | Request) => { const urlStr = String(url);