Fix: Mega-Debrid "Kein Server fuer diesen Hoster" (Tageslimit) -> schnell scheitern + rotieren
User-Report: Account 1 am Tageslimit liefert "Kein Server fuer diesen Hoster verfuegbar". Bisher lief das durch die volle Web-Retry-Maschine (generate->null -> re-Login -> 3x REQUEST_RETRIES) und fraß ~40s des GETEILTEN 60s-Unrestrict-Budgets -> der funktionierende naechste Account (FabelDavid) lief in den Timeout (aborted:debrid -> als fatal klassifiziert, "abgebrochen (fataler Fehler)" im Rotations-Verlauf), obwohl er gehen wuerde. Fix (3 Teile, gemeinsame MEGA_DEBRID_NO_SERVER_RE): 1. mega-web-fallback generate(): die "Kein Server"-Meldung wird surfacet (throw) statt null zurueckzugeben -> kein re-Login + erneutes Pollen. 2. unrestrictViaWeb: bricht bei der Meldung ab (kein 3x-REQUEST_RETRIES) -> sofortige Retries sind zwecklos (Limit bleibt) und verbrennen das geteilte Rotations-Budget. 3. classifyAccountFailure: erkennt die Meldung -> quota-Cooldown (2 min) -> naechster Account, mit echter Meldung im Log statt generischem "Antwort leer". So scheitert der limitierte Account schnell (1 Versuch) und der naechste Account bekommt das volle Budget zum Aufloesen. Tests: mega-web-fallback (throw + ajaxCalls=1) + debrid-Rotation (acc1 Limit -> acc2, calls=2). 645 Tests gruen, tsc 9, Build sauber. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1e5cd3012b
commit
9d8351c017
@ -6,6 +6,7 @@ import { APP_VERSION, REQUEST_RETRIES } from "./constants";
|
|||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { logAccountRotation } from "./account-rotation-log";
|
import { logAccountRotation } from "./account-rotation-log";
|
||||||
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
|
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
|
||||||
|
import { MEGA_DEBRID_NO_SERVER_RE } from "./mega-web-fallback";
|
||||||
import { isMegaFileUrl, resolveMegaFilename } from "./mega-public-api";
|
import { isMegaFileUrl, resolveMegaFilename } from "./mega-public-api";
|
||||||
import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils";
|
import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils";
|
||||||
|
|
||||||
@ -1866,8 +1867,11 @@ class MegaDebridClient {
|
|||||||
if (!lastError) {
|
if (!lastError) {
|
||||||
lastError = "Mega-Web Antwort leer";
|
lastError = "Mega-Web Antwort leer";
|
||||||
}
|
}
|
||||||
// Don't retry permanent hoster errors (dead link, file removed, etc.)
|
// Don't retry permanent hoster errors (dead link, file removed, etc.) — and
|
||||||
if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(lastError)) {
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
if (attempt < REQUEST_RETRIES) {
|
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
|
// Rate limit
|
||||||
if (/rate.?limit|too.?many|429/i.test(errorText)) {
|
if (/rate.?limit|too.?many|429/i.test(errorText)) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -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_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";
|
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 {
|
function normalizeLink(link: string): string {
|
||||||
return link.trim().toLowerCase();
|
return link.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
@ -395,6 +404,15 @@ export class MegaWebFallback {
|
|||||||
await sleepWithSignal(1200, signal);
|
await sleepWithSignal(1200, signal);
|
||||||
continue;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1482,6 +1482,46 @@ describe("debrid service", () => {
|
|||||||
expect(megaWeb).toHaveBeenCalledTimes(1);
|
expect(megaWeb).toHaveBeenCalledTimes(1);
|
||||||
}, 20000);
|
}, 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 () => {
|
it("respects provider selection and does not append hidden providers", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
|
|||||||
@ -60,6 +60,35 @@ describe("mega-web-fallback", () => {
|
|||||||
expect(fetchCallCount).toBe(4);
|
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('<form id="debridForm"></form>', { status: 200 });
|
||||||
|
}
|
||||||
|
if (urlStr.includes("form=debrid")) {
|
||||||
|
return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l1</h3><a href="javascript:processDebrid(1,'code1',0)">d</a></div>`, { 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 () => {
|
it("throws if login fails to set cookie", async () => {
|
||||||
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
||||||
const urlStr = String(url);
|
const urlStr = String(url);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user