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:
parent
254fce8736
commit
ffcd0817cf
@ -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,7 +2096,9 @@ class MegaDebridClient {
|
||||
});
|
||||
throw new Error(`Mega-Debrid${accountLabel}: ${failure.message}`);
|
||||
}
|
||||
const cooldownInfo = failure.cooldownMs > 0
|
||||
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)
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -375,6 +375,15 @@ export class MegaWebFallback {
|
||||
throw new Error(`Mega-Web: Link permanent ungültig (${permanentError})`);
|
||||
}
|
||||
|
||||
// Tageslimit dieses Accounts: die DEBRID-Seite enthaelt dann KEINEN processDebrid-
|
||||
// Code (parseCodes leer → wir wuerden gleich null/"Antwort leer" liefern). Steht die
|
||||
// "Kein Server für diesen Hoster"-Meldung als Page-Error im HTML, surface sie hier,
|
||||
// damit die Rotation den Account als tageslimitiert erkennt statt als Leer-Blip.
|
||||
const noServerError = pageErrors.find((err) => MEGA_DEBRID_NO_SERVER_RE.test(err));
|
||||
if (noServerError) {
|
||||
throw new Error(`Mega-Web: ${noServerError}`);
|
||||
}
|
||||
|
||||
const code = pickCode(parseCodes(html), link);
|
||||
if (!code) {
|
||||
return null;
|
||||
|
||||
@ -1143,16 +1143,21 @@ function formatCheckedAgo(checkedAt: number): string {
|
||||
return `vor ${days} Tag${days === 1 ? "" : "en"}`;
|
||||
}
|
||||
|
||||
function rotationEventText(ev: { event: string; cooldownSec?: number; next?: string }): string {
|
||||
function rotationEventText(ev: { event: string; cooldownSec?: number; next?: string; reason?: string }): string {
|
||||
const untilRestart = /bis Neustart gesperrt/i.test(ev.reason || "");
|
||||
switch (ev.event) {
|
||||
case "OK": return "erfolgreich";
|
||||
case "FAILED": {
|
||||
if (untilRestart) {
|
||||
const nx = ev.next && ev.next !== "ENDE" ? ` → ${ev.next}` : "";
|
||||
return `Tageslimit erreicht, bis Neustart gesperrt${nx}`;
|
||||
}
|
||||
const cd = ev.cooldownSec ? `, Cooldown ${ev.cooldownSec}s` : "";
|
||||
const nx = ev.next && ev.next !== "ENDE" ? ` → ${ev.next}` : "";
|
||||
return `fehlgeschlagen${cd}${nx}`;
|
||||
}
|
||||
case "FATAL": return "abgebrochen (fataler Fehler)";
|
||||
case "SKIP_COOLDOWN": return "übersprungen (Cooldown aktiv)";
|
||||
case "SKIP_COOLDOWN": return untilRestart ? "übersprungen (bis Neustart gesperrt)" : "übersprungen (Cooldown aktiv)";
|
||||
case "SKIP_DISABLED": return "übersprungen (deaktiviert)";
|
||||
case "SKIP_DAILY_LIMIT": return "übersprungen (Tageslimit erreicht)";
|
||||
case "SKIP_HOST_COOLDOWN": return "übersprungen (Host-Cooldown)";
|
||||
|
||||
@ -112,3 +112,26 @@ Ein Test, der eine Hilfsfunktion mit dem richtigen Flag direkt aufruft, beweist
|
||||
das Flag funktioniert — NICHT, dass der Produktionspfad das Flag setzt. Für echte
|
||||
Absicherung einen End-to-End-Test durch den realen Einstiegspunkt fahren und per
|
||||
Negativ-Gate (Flag temporär entfernen → Test muss fallen) verifizieren.
|
||||
|
||||
## 2026-05-31 — Log-Symptom ≠ User-Wortlaut: greppen, bevor man auf eine Meldung triggert
|
||||
|
||||
**Muster:** User meldete Mega-Debrid-Tageslimit als „Kein Server für diesen Hoster". Ich
|
||||
wollte den Fix an genau diese Meldung (`MEGA_DEBRID_NO_SERVER_RE`) hängen. Der Advisor
|
||||
stoppte: der Screenshot zeigte als Cooldown-Grund **„Antwort leer"**, nicht „Kein Server".
|
||||
|
||||
**Beweis (Support-Bundle gegrept):** „Kein Server"/„Erreur"/„aucun serveur" = **0** Treffer
|
||||
im ganzen Bundle, „Antwort leer" = **20.861** Treffer. Der limitierte Account liefert im
|
||||
Web-Pfad NIE eine unterscheidbare Meldung — `generate()` findet ohne `processDebrid`-Code
|
||||
keinen Code → `return null` → der Aufrufer macht daraus „Antwort leer". Ein Trigger auf
|
||||
„Kein Server" wäre toter Code gewesen (= die v1.7.172-Falle, zum 2. Mal fast getreten).
|
||||
|
||||
**Regel:** Bevor man einen Fix an einen bestimmten Meldungstext hängt, in den ECHTEN Logs
|
||||
greppen, ob dieser Text dort überhaupt vorkommt (`count`-Mode, alt-Text vs. Ist-Text). Sind
|
||||
zwei Fälle auf Message-Ebene nicht unterscheidbar (Tageslimit vs. transienter Blip → beide
|
||||
„Antwort leer"), nicht raten — über ein **Verhaltens-Signal** klassifizieren: hier eine
|
||||
Streak (3× hintereinander leer → geparkt), nicht der einmalige Wortlaut.
|
||||
|
||||
**Wiring-Test nicht vergessen** (eigene Lesson): die Helfer-Unit-Tests beweisen nur den
|
||||
Zähler. Ein E2E-Test muss eine ECHTE leere Antwort durch den realen Einstiegspunkt
|
||||
(`unrestrictWithAccounts` → `classifyAccountFailure` → catch → Park) treiben, sonst bleibt
|
||||
unbewiesen, dass der Produktionspfad das Signal überhaupt setzt.
|
||||
|
||||
@ -3,7 +3,7 @@ import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||
import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, normalizeResolvedFilename, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||
import { clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@ -1566,6 +1566,152 @@ describe("debrid service", () => {
|
||||
expect(result.directUrl).toBe("https://mega-web.example/ok.rar");
|
||||
}, 20000);
|
||||
|
||||
it("escalates a Mega-Debrid account to 'until restart' after the empty-response streak threshold", () => {
|
||||
// User-Entscheidung: ein tageslimitierter Account soll NICHT alle 20s neu getestet
|
||||
// werden, sondern bis Programm-Neustart geparkt. Da Tageslimit und transienter
|
||||
// Leer-Blip auf Message-Ebene identisch sind ("Antwort leer", nie "Kein Server" in
|
||||
// echten Logs), zaehlt eine Streak: erst ab der Schwelle wird geparkt.
|
||||
const key = `${getMegaDebridAccountId("user1")}:web`;
|
||||
expect(MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART).toBe(3);
|
||||
expect(recordMegaDebridEmptyResponseStreak(key)).toBe(1); // 1. Blip -> NICHT parken
|
||||
expect(recordMegaDebridEmptyResponseStreak(key)).toBe(2); // 2. -> NICHT parken
|
||||
expect(recordMegaDebridEmptyResponseStreak(key)).toBe(3); // 3. -> Schwelle erreicht -> parken
|
||||
// Ein Erfolg/anderer Fehlertyp setzt die Streak zurueck (Account wieder frisch).
|
||||
clearMegaDebridEmptyResponseStreak(key);
|
||||
expect(recordMegaDebridEmptyResponseStreak(key)).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps an 'until restart' park active forever (never expires until process restart)", () => {
|
||||
// Anders als ein zeitbasierter Cooldown darf die Bis-Neustart-Sperre NIE ablaufen
|
||||
// (nur ein Neustart loescht die In-Memory-Map). Sonst wuerde der limitierte Account
|
||||
// doch wieder getestet werden.
|
||||
const key = `${getMegaDebridAccountId("user1")}:api`;
|
||||
primeMegaDebridUntilRestartForTests(key);
|
||||
const now = getMegaDebridAccountCooldownState(key);
|
||||
expect(now?.untilRestart).toBe(true);
|
||||
// Selbst 100 Tage in der Zukunft ist die Sperre noch aktiv.
|
||||
const farFuture = Date.now() + 100 * 24 * 60 * 60 * 1000;
|
||||
expect(getMegaDebridAccountCooldownState(key, farFuture)?.untilRestart).toBe(true);
|
||||
});
|
||||
|
||||
it("skips a Mega-Debrid account parked until restart and rotates to the next, without re-testing it", async () => {
|
||||
// Beweis der Skip-Logik: ein bis Neustart geparkter Account wird NICHT mehr per
|
||||
// Netzwerk getestet (kein megaWeb-Call fuer ihn), die Rotation nutzt den naechsten.
|
||||
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;
|
||||
|
||||
// user1 ist bereits bis Neustart geparkt (Tageslimit). Beide Mode-Keys parken, damit
|
||||
// der Test unabhaengig vom intern gewaehlten mode ("api"/"web") ist.
|
||||
const user1 = getMegaDebridAccountId("user1");
|
||||
primeMegaDebridUntilRestartForTests(`${user1}:api`);
|
||||
primeMegaDebridUntilRestartForTests(`${user1}:web`);
|
||||
|
||||
const loginsSeen: Array<string | undefined> = [];
|
||||
const megaWeb = vi.fn(async (_link: string, _signal: AbortSignal | undefined, account?: { login: string; password: string }) => {
|
||||
loginsSeen.push(account?.login);
|
||||
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/parked-skip-test");
|
||||
|
||||
// user1 wurde NICHT angefasst (geparkt), nur user2 wurde getestet und loest auf.
|
||||
expect(loginsSeen).not.toContain("user1");
|
||||
expect(loginsSeen).toContain("user2");
|
||||
expect((result as { sourceAccountId?: string }).sourceAccountId).toBe(getMegaDebridAccountId("user2"));
|
||||
}, 20000);
|
||||
|
||||
it("fails terminally (no retry timer) when ALL Mega-Debrid accounts are parked until restart", async () => {
|
||||
// Sind alle Accounts am Tageslimit (bis Neustart gesperrt), gibt es keinen
|
||||
// sinnvollen endlichen Retry-Zeitpunkt: die Rotation muss klar und endgueltig
|
||||
// scheitern statt einen absurden (MAX_SAFE_INTEGER) Retry-Timer zu werfen — und
|
||||
// KEINEN Account erneut per Netzwerk pollen.
|
||||
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;
|
||||
|
||||
for (const login of ["user1", "user2"]) {
|
||||
const id = getMegaDebridAccountId(login);
|
||||
primeMegaDebridUntilRestartForTests(`${id}:api`);
|
||||
primeMegaDebridUntilRestartForTests(`${id}:web`);
|
||||
}
|
||||
|
||||
const megaWeb = vi.fn(async () => ({ fileName: "x.rar", directUrl: "https://mega-web.example/x.rar", fileSize: null, retriesUsed: 0 }));
|
||||
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||
|
||||
await expect(service.unrestrictLink("https://rapidgator.net/file/all-parked-test")).rejects.toThrow(/bis Neustart gesperrt/i);
|
||||
// Kein Account wurde erneut getestet.
|
||||
expect(megaWeb).not.toHaveBeenCalled();
|
||||
}, 20000);
|
||||
|
||||
it("drives a real empty response through the full rotation into an until-restart park (wiring test)", async () => {
|
||||
// Lesson "Wiring-Lock vs. Mechanism-Test": die Helfer-Unit-Tests beweisen nur, dass der
|
||||
// Streak-Zaehler funktioniert — NICHT, dass der Produktionspfad ihn fuettert. Dieser Test
|
||||
// faehrt eine ECHTE leere Antwort durch unrestrictWithAccounts -> classifyAccountFailure
|
||||
// (limitSignal) -> catch -> recordStreak -> Park. Kaeme limitSignal nicht an, wuerde der
|
||||
// catch-else die Streak loeschen und KEIN until-restart setzen -> Assertion faellt.
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
megaLogin: "user1",
|
||||
megaPassword: "pass1",
|
||||
megaCredentials: "user1:pass1", // genau EIN Account
|
||||
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;
|
||||
|
||||
// Provider "megadebrid" + preferApi:false -> resolveMegaDebridProvider -> "megadebrid-web"
|
||||
// -> mode "web" -> Key-Suffix ":web" (das ist genau der Web-Pfad aus dem User-Screenshot).
|
||||
const key = `${getMegaDebridAccountId("user1")}:web`;
|
||||
// Streak schon EINS unter der Schwelle (2 vorherige leere Antworten) — noch NICHT geparkt.
|
||||
recordMegaDebridEmptyResponseStreak(key);
|
||||
recordMegaDebridEmptyResponseStreak(key);
|
||||
expect(getMegaDebridAccountCooldownState(key)?.untilRestart ?? false).toBe(false);
|
||||
|
||||
// megaWeb liefert null -> der echte Web-Pfad macht daraus "Mega-Web Antwort leer".
|
||||
const megaWeb = vi.fn(async () => null);
|
||||
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||
await service.unrestrictLink("https://rapidgator.net/file/wiring").catch(() => undefined);
|
||||
|
||||
// Der echte Fehlversuch tippte die Streak auf die Schwelle -> Park bis Neustart.
|
||||
expect(megaWeb).toHaveBeenCalled(); // Account wurde wirklich getestet (nicht vorab geparkt)
|
||||
expect(getMegaDebridAccountCooldownState(key)?.untilRestart).toBe(true);
|
||||
}, 20000);
|
||||
|
||||
it("respects provider selection and does not append hidden providers", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
|
||||
@ -89,6 +89,39 @@ describe("mega-web-fallback", () => {
|
||||
expect(ajaxCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("surfaces 'Kein Server für diesen Hoster' from the debrid PAGE (daily limit, no debrid code) instead of empty", async () => {
|
||||
// Tageslimit dieses Accounts: die DEBRID-Seite enthält KEINEN processDebrid-Code,
|
||||
// sondern die Limit-Meldung als Page-Error. Früher -> kein Code -> null -> "Antwort
|
||||
// leer" (auf Message-Ebene nicht als Tageslimit erkennbar). Jetzt muss die Meldung
|
||||
// als Fehler hochkommen, damit die Rotation den Account als limitiert behandelt.
|
||||
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")) {
|
||||
// Keine processDebrid(...)-Codes — nur die Tageslimit-Meldung als Page-Error.
|
||||
return new Response('<div class="error">Erreur : Kein Server für diesen Hoster verfügbar. Bitte versuchen Sie es später noch einmal.</div>', { status: 200 });
|
||||
}
|
||||
if (urlStr.includes("ajax=debrid")) {
|
||||
ajaxCalls += 1;
|
||||
return new Response(JSON.stringify({ link: "https://should.not/happen" }), { status: 200 });
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" }));
|
||||
await expect(fallback.unrestrict("https://mega.debrid/l1")).rejects.toThrow(/kein server für diesen hoster/i);
|
||||
// Ohne Code wird gar nicht erst gepollt — die Meldung kommt direkt von der Seite.
|
||||
expect(ajaxCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("logs in with the per-account credentials passed to unrestrict, not the default", async () => {
|
||||
const loginsUsed: string[] = [];
|
||||
globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: { body?: unknown }) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user