Mega-Debrid: Account am Tageslimit bis Neustart parken (Streak-Heuristik) statt endlos neu testen
User-Entscheidung: ein Mega-Debrid-Account am Tageslimit soll bis zum Programm-Neustart uebersprungen werden, nicht alle 20s/2min neu getestet. Ground Truth (Support-Bundle gegrept): der limitierte Account liefert im Web-Pfad NIE eine unterscheidbare Meldung — "Kein Server" = 0 Treffer, "Antwort leer" = 20.861. Tageslimit und transienter Blip sind auf Message-Ebene nicht trennbar (generate() findet ohne processDebrid- Code keinen Code -> return null -> "Antwort leer"). Ein Trigger auf "Kein Server" waere toter Code. Loesung (Verhaltens-Signal statt Wortlaut): - megaDebridEmptyResponseStreaks zaehlt aufeinanderfolgende "Antwort leer"/"Kein Server"- Treffer je Account; ab 3 wird der Account bis Neustart geparkt (until=MAX_SAFE_INTEGER, nur In-Memory -> Neustart loescht). Erfolg/anderer Fehler setzt zurueck. - classifyAccountFailure markiert beide Signale als limitSignal (Symmetrie: ein einzelner evtl. transienter Treffer parkt NICHT, behaelt kurzen Cooldown). - Skip-Branch: "uebersprungen (bis Neustart gesperrt)", traegt nicht zu earliestCooldownUntil bei (kein absurder Retry-Timer); Post-Loop wirft klare Endmeldung wenn alle geparkt. - generate() surfacet "Kein Server" zusaetzlich als Page-Error (falls es doch im HTML steht). - UI: Rotations-Verlauf zeigt "bis Neustart gesperrt". Verifiziert: tsc 9 (Baseline), 655 Tests + 5 neue (inkl. Wiring-E2E der eine echte leere Antwort durch unrestrictWithAccounts->classify->catch->Park treibt), Build gruen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
254fce8736
commit
ffcd0817cf
+120
-25
@@ -313,15 +313,44 @@ function getDebridLinkKeyHostCooldownState(
|
||||
};
|
||||
}
|
||||
|
||||
/** Per-account cooldown cache for Mega-Debrid: accountId → expiry timestamp. */
|
||||
/** Per-account cooldown cache for Mega-Debrid: accountId → expiry timestamp.
|
||||
* untilRestart: ein Tageslimit-Account wird fuer den REST der Laufzeit uebersprungen
|
||||
* (nicht alle 20s/2min neu getestet) und kommt erst nach einem Neustart zurueck — die
|
||||
* Map liegt nur im RAM, ein Neustart loescht sie also automatisch. */
|
||||
type MegaDebridCooldownCategory = "invalid" | "rate_limit" | "quota" | "temporary" | "skip";
|
||||
type MegaDebridCooldownDetail = { until: number; message: string; category: MegaDebridCooldownCategory };
|
||||
type MegaDebridCooldownDetail = { until: number; message: string; category: MegaDebridCooldownCategory; untilRestart?: boolean };
|
||||
const megaDebridAccountCooldowns = new Map<string, MegaDebridCooldownDetail>();
|
||||
const MEGA_DEBRID_ACCOUNT_COOLDOWN_MS = 120_000; // 2 min cooldown per failed account
|
||||
const MEGA_DEBRID_INVALID_ACCOUNT_COOLDOWN_MS = 60 * 60 * 1000;
|
||||
|
||||
/** Zaehlt aufeinanderfolgende "Antwort leer"-Fehlversuche je Account-Key. Ein
|
||||
* tageslimitierter Mega-Debrid-Account liefert im Web-Pfad KEINE unterscheidbare
|
||||
* Meldung ("Kein Server" taucht in echten Logs nie auf — immer nur "Antwort leer"),
|
||||
* ist aber daran erkennbar, dass er PERSISTENT leer antwortet. Nach
|
||||
* MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART aufeinanderfolgenden Leer-Antworten wird der
|
||||
* Account bis Neustart geparkt; ein einzelner transienter Blip (Streak < Schwelle)
|
||||
* behaelt den kurzen 20s-Cooldown. Ein Erfolg oder ein anderer Fehlertyp setzt den
|
||||
* Zaehler zurueck. */
|
||||
const megaDebridEmptyResponseStreaks = new Map<string, number>();
|
||||
export const MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART = 3;
|
||||
|
||||
/** Verbucht eine "Antwort leer"-Antwort fuer den Account-Key und liefert die neue
|
||||
* Streak-Laenge. Ab MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART parkt der Aufrufer den
|
||||
* Account bis Neustart. Exportiert fuer deterministische Tests. */
|
||||
export function recordMegaDebridEmptyResponseStreak(accountId: string): number {
|
||||
const streak = (megaDebridEmptyResponseStreaks.get(accountId) || 0) + 1;
|
||||
megaDebridEmptyResponseStreaks.set(accountId, streak);
|
||||
return streak;
|
||||
}
|
||||
|
||||
/** Setzt die "Antwort leer"-Streak zurueck (bei Erfolg oder einem anderen Fehlertyp). */
|
||||
export function clearMegaDebridEmptyResponseStreak(accountId: string): void {
|
||||
megaDebridEmptyResponseStreaks.delete(accountId);
|
||||
}
|
||||
|
||||
export function resetMegaDebridRuntimeStateForTests(): void {
|
||||
megaDebridAccountCooldowns.clear();
|
||||
megaDebridEmptyResponseStreaks.clear();
|
||||
}
|
||||
|
||||
/** Periodic cleanup of expired Mega-Debrid cooldown entries. */
|
||||
@@ -341,6 +370,11 @@ export function primeMegaDebridRuntimeCooldownForTests(accountId: string, cooldo
|
||||
setMegaDebridAccountCooldownState(accountId, cooldownMs, message, "temporary");
|
||||
}
|
||||
|
||||
/** Parkt einen Account-Key bis Neustart (Tageslimit). Exportiert fuer Tests. */
|
||||
export function primeMegaDebridUntilRestartForTests(accountId: string, message = "Tageslimit (Test) — bis Neustart gesperrt"): void {
|
||||
setMegaDebridAccountCooldownState(accountId, 0, message, "quota", true);
|
||||
}
|
||||
|
||||
function clearMegaDebridAccountCooldownState(accountId: string): void {
|
||||
megaDebridAccountCooldowns.delete(accountId);
|
||||
}
|
||||
@@ -349,8 +383,21 @@ function setMegaDebridAccountCooldownState(
|
||||
accountId: string,
|
||||
cooldownMs: number,
|
||||
message: string,
|
||||
category: MegaDebridCooldownCategory
|
||||
category: MegaDebridCooldownCategory,
|
||||
untilRestart = false
|
||||
): void {
|
||||
if (untilRestart) {
|
||||
// Bis-Neustart-Sperre: never expires in-process (Number.MAX_SAFE_INTEGER liegt
|
||||
// ausserhalb des gueltigen Date-Bereichs → Anzeige wird via untilRestart-Flag
|
||||
// gesondert behandelt, nicht ueber new Date(until)).
|
||||
megaDebridAccountCooldowns.set(accountId, {
|
||||
until: Number.MAX_SAFE_INTEGER,
|
||||
message,
|
||||
category,
|
||||
untilRestart: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
|
||||
clearMegaDebridAccountCooldownState(accountId);
|
||||
return;
|
||||
@@ -365,7 +412,7 @@ function setMegaDebridAccountCooldownState(
|
||||
export function getMegaDebridAccountCooldownState(
|
||||
accountId: string,
|
||||
now = Date.now()
|
||||
): { until: number; remainingMs: number; message: string; category: MegaDebridCooldownCategory } | null {
|
||||
): { until: number; remainingMs: number; message: string; category: MegaDebridCooldownCategory; untilRestart: boolean } | null {
|
||||
const detail = megaDebridAccountCooldowns.get(accountId);
|
||||
if (!detail) {
|
||||
return null;
|
||||
@@ -378,7 +425,8 @@ export function getMegaDebridAccountCooldownState(
|
||||
until: detail.until,
|
||||
remainingMs: detail.until - now,
|
||||
message: detail.message,
|
||||
category: detail.category
|
||||
category: detail.category,
|
||||
untilRestart: detail.untilRestart === true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1933,6 +1981,7 @@ class MegaDebridClient {
|
||||
let usableAccountSeen = false;
|
||||
const cooldownFailures: string[] = [];
|
||||
let earliestCooldownUntil = 0;
|
||||
let parkedUntilRestartSeen = false;
|
||||
const totalAccounts = accounts.length;
|
||||
const providerName = `Mega-Debrid ${mode === "api" ? "API" : "Web"}`;
|
||||
const linkShort = String(link || "").slice(0, 80);
|
||||
@@ -1959,15 +2008,25 @@ class MegaDebridClient {
|
||||
const cooldownKey = `${account.id}:${mode}`;
|
||||
const accountCooldownState = getMegaDebridAccountCooldownState(cooldownKey);
|
||||
if (accountCooldownState) {
|
||||
const untilStr = new Date(accountCooldownState.until).toLocaleTimeString();
|
||||
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (Cooldown bis ${untilStr}), pruefe naechsten Account`);
|
||||
const untilStr = accountCooldownState.untilRestart
|
||||
? "Neustart"
|
||||
: new Date(accountCooldownState.until).toLocaleTimeString();
|
||||
const reasonText = accountCooldownState.untilRestart
|
||||
? "Tageslimit erreicht — bis Neustart gesperrt"
|
||||
: `Cooldown bis ${untilStr}`;
|
||||
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (${reasonText}), pruefe naechsten Account`);
|
||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_COOLDOWN", {
|
||||
reason: accountCooldownState.message,
|
||||
category: accountCooldownState.category,
|
||||
until: untilStr
|
||||
});
|
||||
cooldownFailures.push(`Mega-Debrid${accountLabel}: ${accountCooldownState.message}`);
|
||||
if (!earliestCooldownUntil || accountCooldownState.until < earliestCooldownUntil) {
|
||||
// Eine Bis-Neustart-Sperre traegt NICHT zu earliestCooldownUntil bei: es gibt
|
||||
// keinen sinnvollen endlichen Retry-Zeitpunkt (der Account kommt erst nach
|
||||
// Neustart zurueck). Sonst wuerde MAX_SAFE_INTEGER einen absurden Retry-Timer setzen.
|
||||
if (accountCooldownState.untilRestart) {
|
||||
parkedUntilRestartSeen = true;
|
||||
} else if (!earliestCooldownUntil || accountCooldownState.until < earliestCooldownUntil) {
|
||||
earliestCooldownUntil = accountCooldownState.until;
|
||||
}
|
||||
continue;
|
||||
@@ -1985,6 +2044,7 @@ class MegaDebridClient {
|
||||
const client = new MegaDebridClient(account.login, account.password, mode, allowApiFallback, megaWebUnrestrict);
|
||||
const result = await client.unrestrictLink(link, signal);
|
||||
clearMegaDebridAccountCooldownState(cooldownKey);
|
||||
clearMegaDebridEmptyResponseStreak(cooldownKey);
|
||||
const elapsedMs = Date.now() - testStartedAt;
|
||||
logger.info(`Mega-Debrid${accountLabel}: Unrestrict OK nach ${elapsedMs}ms -> ${result.fileName || "?"}`);
|
||||
logAccountRotation("INFO", providerName, rotationLabel, "OK", {
|
||||
@@ -2002,7 +2062,27 @@ class MegaDebridClient {
|
||||
const failure = MegaDebridClient.classifyAccountFailure(error);
|
||||
const elapsedMs = Date.now() - testStartedAt;
|
||||
failures.push(`Mega-Debrid${accountLabel}: ${failure.message}`);
|
||||
if (failure.cooldownMs > 0) {
|
||||
|
||||
// "Antwort leer"-Streak fuehren: ein tageslimitierter Account antwortet
|
||||
// PERSISTENT leer (siehe Kommentar an megaDebridEmptyResponseStreaks). Erreicht
|
||||
// der Account die Schwelle, wird er bis Neustart geparkt — statt alle 20s neu
|
||||
// getestet zu werden. failure.untilRestart deckt zusaetzlich den Fall ab, dass
|
||||
// generate() die "Kein Server"-Meldung doch mal direkt liefert.
|
||||
let parkUntilRestart = false;
|
||||
let parkMessage = failure.message;
|
||||
if (failure.limitSignal) {
|
||||
const streak = recordMegaDebridEmptyResponseStreak(cooldownKey);
|
||||
if (streak >= MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART) {
|
||||
parkUntilRestart = true;
|
||||
parkMessage = `Tageslimit erreicht (${streak}x kein Server/leere Antwort) — bis Neustart gesperrt`;
|
||||
}
|
||||
} else {
|
||||
clearMegaDebridEmptyResponseStreak(cooldownKey);
|
||||
}
|
||||
|
||||
if (parkUntilRestart) {
|
||||
setMegaDebridAccountCooldownState(cooldownKey, 0, parkMessage, "quota", true);
|
||||
} else if (failure.cooldownMs > 0) {
|
||||
setMegaDebridAccountCooldownState(cooldownKey, failure.cooldownMs, failure.message, failure.category);
|
||||
} else {
|
||||
clearMegaDebridAccountCooldownState(cooldownKey);
|
||||
@@ -2016,9 +2096,11 @@ class MegaDebridClient {
|
||||
});
|
||||
throw new Error(`Mega-Debrid${accountLabel}: ${failure.message}`);
|
||||
}
|
||||
const cooldownInfo = failure.cooldownMs > 0
|
||||
? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s`
|
||||
: "";
|
||||
const cooldownInfo = parkUntilRestart
|
||||
? ", bis Neustart gesperrt"
|
||||
: failure.cooldownMs > 0
|
||||
? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s`
|
||||
: "";
|
||||
// Find the next account that will be tried (for clearer log)
|
||||
let nextLabel = "ENDE";
|
||||
for (let nextIdx = idx + 1; nextIdx < accounts.length; nextIdx += 1) {
|
||||
@@ -2028,12 +2110,12 @@ class MegaDebridClient {
|
||||
break;
|
||||
}
|
||||
}
|
||||
logger.warn(`Mega-Debrid${accountLabel}: ${failure.message}${cooldownInfo}, pruefe naechsten Account (${nextLabel})`);
|
||||
logger.warn(`Mega-Debrid${accountLabel}: ${parkUntilRestart ? parkMessage : failure.message}${cooldownInfo}, pruefe naechsten Account (${nextLabel})`);
|
||||
logAccountRotation("WARN", providerName, rotationLabel, "FAILED", {
|
||||
reason: parkUntilRestart ? parkMessage : failure.message,
|
||||
elapsedMs,
|
||||
reason: failure.message,
|
||||
category: failure.category,
|
||||
cooldownSec: failure.cooldownMs > 0 ? Math.ceil(failure.cooldownMs / 1000) : 0,
|
||||
category: parkUntilRestart ? "quota" : failure.category,
|
||||
cooldownSec: parkUntilRestart || failure.cooldownMs <= 0 ? 0 : Math.ceil(failure.cooldownMs / 1000),
|
||||
next: nextLabel,
|
||||
link: linkShort
|
||||
});
|
||||
@@ -2045,6 +2127,12 @@ class MegaDebridClient {
|
||||
const retryMs = Math.max(1000, earliestCooldownUntil - Date.now() + 1000);
|
||||
throw new Error(`mega_debrid_cooldown:${retryMs}:${cooldownFailures.join(" | ")}`);
|
||||
}
|
||||
if (parkedUntilRestartSeen) {
|
||||
// Alle (verbliebenen) Accounts haben ihr Tageslimit erreicht und sind bis
|
||||
// Neustart gesperrt. KEIN mega_debrid_cooldown:<ms> — es gibt keinen sinnvollen
|
||||
// Retry-Zeitpunkt; ein endlicher Timer wuerde nur erneut leer pollen.
|
||||
throw new Error(`Mega-Debrid: Alle Accounts am Tageslimit (bis Neustart gesperrt)${cooldownFailures.length > 0 ? ` | ${cooldownFailures.join(" | ")}` : ""}`);
|
||||
}
|
||||
throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar");
|
||||
}
|
||||
throw new Error(failures.join(" | ") || "Mega-Debrid: Kein aktiver Account verfuegbar");
|
||||
@@ -2056,7 +2144,7 @@ class MegaDebridClient {
|
||||
*/
|
||||
private static classifyAccountFailure(
|
||||
error: unknown
|
||||
): { fatal: boolean; cooldownMs: number; message: string; category: MegaDebridCooldownCategory } {
|
||||
): { fatal: boolean; cooldownMs: number; message: string; category: MegaDebridCooldownCategory; limitSignal?: boolean } {
|
||||
const errorText = compactErrorText(error).replace(/^Error:\s*/i, "");
|
||||
|
||||
// Abort — don't retry other accounts
|
||||
@@ -2089,14 +2177,18 @@ 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.
|
||||
// "Kein Server für diesen Hoster verfügbar" = Account-Tageslimit erschöpft (oder der
|
||||
// Hoster ist kurz nicht bedient — laut Kommentar an MEGA_DEBRID_NO_SERVER_RE moeglich).
|
||||
// Wie "Antwort leer" ein Limit-Signal: feedet die Streak (limitSignal). Erst nach
|
||||
// mehreren Treffern wird der Account bis Neustart geparkt — ein einzelner (evtl.
|
||||
// transienter) Treffer erzwingt KEINEN Neustart, behaelt aber den 2-Min-Cooldown.
|
||||
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"
|
||||
category: "quota",
|
||||
limitSignal: true
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2110,16 +2202,19 @@ class MegaDebridClient {
|
||||
};
|
||||
}
|
||||
|
||||
// Mega-Web "Antwort leer" / empty body — server frequently returns transient
|
||||
// empty responses that recover within seconds. A 2-minute cooldown for this
|
||||
// is way too long because the account is fundamentally healthy. Use a short
|
||||
// 20s cooldown so the next download attempt can use this account again.
|
||||
// Mega-Web "Antwort leer" / empty body — kann zweierlei sein: (a) ein transienter
|
||||
// Blip (erholt sich in Sekunden → kurzer 20s-Cooldown reicht) ODER (b) ein
|
||||
// tageslimitierter Account, der PERSISTENT leer antwortet. Da beide Faelle auf
|
||||
// Message-Ebene identisch aussehen (in echten Logs taucht "Kein Server" nie auf,
|
||||
// immer nur "Antwort leer"), markieren wir emptyResponse=true; der Aufrufer zaehlt
|
||||
// die Streak und parkt den Account erst nach mehreren Leer-Antworten bis Neustart.
|
||||
if (/antwort\s+leer|empty\s+response|leere\s+antwort/i.test(errorText)) {
|
||||
return {
|
||||
fatal: false,
|
||||
cooldownMs: 20_000,
|
||||
message: errorText || "Mega-Web transient empty response",
|
||||
category: "temporary"
|
||||
category: "temporary",
|
||||
limitSignal: true
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user