From fa2c7eb6be025397a48ff3534e0109bcdcaefb66 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 17 Jun 2026 14:23:41 +0200 Subject: [PATCH] Fix: Mega-Web Selbst-Cooldown bei belegter Queue + Legacy-Config Mega-Demotion (Audit Runde 9+10) MW-1 (HIGH): Ein gesunder Mega-Web-Account wurde 120s gesperrt, wenn der 60s-Gesamttimeout ablief, waehrend die Umwandlung noch SERIELL in der per-Account-Single-Flight-Queue auf ihren Vorgaenger wartete - also bevor echte Arbeit begann. Die Queue-Wartezeit zaehlte zu elapsedMs (>= 8s), sodass der Abbruch faelschlich wie ein In-Arbeit-Abbruch eines langsamen Accounts gewertet wurde. Genau die gemeldete 'Tool sperrt sich selbst'-Klasse (Web-Variante). Zweiteilig: (1) MegaWebFallback.runExclusive trackt workStarted und meldet einen Abbruch-vor-Arbeitsbeginn als Queue-Timeout statt aborted:mega-web; (2) unrestrictViaWeb bewahrt diese Klassifikation statt sie zu aborted:debrid zu plaetten -> die Rotation trifft die Queue-Timeout-Ausnahme (cooldownMs 0) statt den Abbruch-Cooldown. Ein echter In-Arbeit-Abbruch sperrt weiterhin (langsame Accounts korrekt ueberspringen). Nach dem No-Cooldown-Pfad rotiert der Retry per In-Flight-Tiefe auf einen freien Account. SET-MIG-01 (MED): Eine Config von vor v1.6.90 kannte die getrennten Mega-API/Web-Enable-Flags nicht. readSettingsFile merged {...defaultSettings(), ...parsed}, also fuellten die false-Defaults die fehlenden Flags BEVOR normalizeSettings lief -> der creds-basierte Migrationszweig war tot, Mega blieb auf aus trotz vorhandener Creds und wurde beim ersten Settings-Save still aus der providerOrder demotet. Fix: reine migrateLegacyMegaEnableFlags seedet die Flags nur, wenn BEIDE im RAW-parsed fehlen und Creds vorhanden sind (absent-both als einziger sicherer Trigger; present-false bleibt unberuehrt = bewusst-deaktiviert nicht re-aktivieren). Greift nur bei Legacy-Configs, die seit dem Upgrade noch nicht neu gespeichert wurden. Je rot-bewiesener Test (per Temp-Revert verifiziert, nicht-vakuum). Volle Suite 890 gruen, tsc unveraendert (6 vorbestehende Fehler). Runde-9/10-Doku + die zwei offenen Nutzer-Entscheidungen aktualisiert. --- src/main/debrid.ts | 3 + src/main/mega-web-fallback.ts | 14 ++-- src/main/storage.ts | 14 +++- tasks/audit-loop.md | 110 ++++++++++++++++++++++++++++++++ tasks/entscheidungen-offen.md | 24 +++++++ tests/debrid.test.ts | 37 +++++++++++ tests/mega-web-fallback.test.ts | 36 +++++++++++ tests/storage.test.ts | 61 ++++++++++++++++++ 8 files changed, 294 insertions(+), 5 deletions(-) diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 680aed4..b959c6f 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -1885,6 +1885,9 @@ class MegaDebridClient { return null; }); if (signal?.aborted) { + if (/queue.?timeout/i.test(lastError)) { + throw new Error(lastError.replace(/^Error:\s*/i, "")); + } throw new Error("aborted:debrid"); } if (web?.directUrl) { diff --git a/src/main/mega-web-fallback.ts b/src/main/mega-web-fallback.ts index a9b2683..25b452e 100644 --- a/src/main/mega-web-fallback.ts +++ b/src/main/mega-web-fallback.ts @@ -177,12 +177,12 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise }); } -async function raceWithAbort(promise: Promise, signal?: AbortSignal): Promise { +async function raceWithAbort(promise: Promise, signal?: AbortSignal, abortErrorFactory: () => Error = abortError): Promise { if (!signal) { return promise; } if (signal.aborted) { - throw abortError(); + throw abortErrorFactory(); } return new Promise((resolve, reject) => { @@ -194,7 +194,7 @@ async function raceWithAbort(promise: Promise, signal?: AbortSignal): Prom } settled = true; signal.removeEventListener("abort", onAbort); - reject(abortError()); + reject(abortErrorFactory()); }; signal.addEventListener("abort", onAbort, { once: true }); @@ -284,6 +284,7 @@ export class MegaWebFallback { private async runExclusive(job: () => Promise, key: string, signal?: AbortSignal): Promise { const queuedAt = Date.now(); const QUEUE_WAIT_TIMEOUT_MS = 90000; + let workStarted = false; const guardedJob = async (): Promise => { throwIfAborted(signal); const waited = Date.now() - queuedAt; @@ -291,6 +292,7 @@ export class MegaWebFallback { traceConversionPhase({ phase: "web-queue", provider: "megadebrid-web", queueWaitMs: waited, outcome: "queue-timeout", detail: `${Math.floor(waited / 1000)}s in Web-Queue gewartet` }); throw new Error(`Mega-Web Queue-Timeout (${Math.floor(waited / 1000)}s gewartet)`); } + workStarted = true; const workStartedAt = Date.now(); try { const result = await job(); @@ -304,7 +306,11 @@ export class MegaWebFallback { const prev = this.queues.get(key) ?? Promise.resolve(); const run = prev.then(guardedJob, guardedJob); this.queues.set(key, run.then(() => undefined, () => undefined)); - return raceWithAbort(run, signal); + return raceWithAbort(run, signal, () => + workStarted + ? abortError() + : new Error(`Mega-Web Queue-Timeout (abgebrochen nach ${Math.floor((Date.now() - queuedAt) / 1000)}s Wartezeit, Account war belegt)`) + ); } private async login(login: string, password: string, signal?: AbortSignal): Promise { diff --git a/src/main/storage.ts b/src/main/storage.ts index ae0204f..a1b016c 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -627,12 +627,24 @@ function normalizeAudioStripSummary(raw: unknown): AudioStripSummary | undefined }; } +function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings { + if (parsed.megaDebridApiEnabled !== undefined || parsed.megaDebridWebEnabled !== undefined) { + return parsed; + } + const hasMegaCreds = Boolean(asText(parsed.megaLogin) && asText(parsed.megaPassword)); + if (!hasMegaCreds) { + return parsed; + } + const preferApi = parsed.megaDebridPreferApi !== undefined ? Boolean(parsed.megaDebridPreferApi) : true; + return { ...parsed, megaDebridApiEnabled: preferApi, megaDebridWebEnabled: !preferApi }; +} + function readSettingsFile(filePath: string): AppSettings | null { try { const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings; const merged = normalizeSettings({ ...defaultSettings(), - ...parsed + ...migrateLegacyMegaEnableFlags(parsed) }); return sanitizeCredentialPersistence(merged); } catch (error) { diff --git a/tasks/audit-loop.md b/tasks/audit-loop.md index 91fee79..4e9cdcf 100644 --- a/tasks/audit-loop.md +++ b/tasks/audit-loop.md @@ -10,6 +10,116 @@ Backup/Restore, account-check) → Abschlussbericht + Ende. Fixes je rot-bewiese Disziplin: erst BELEGEN (Code-Zitat + konkretes Szenario), dann adversarisch verifizieren, dann TDD-Fix. Kein Blind-Fix. Tests gruen + tsc=6 nach jeder Runde. Periodisch releasen. +## Runde 9 (Provider-spezifische Unrestrict-/Rotations-Pfade) — Mega-Web-Single-Flight + DebridLink-Key + Concurrency +Fokus: Mega-Web-Fallback Session/Single-Flight-Queue, DebridLink-Key-Rotation, In-Flight-Verteilung. +- **CONFIRMED HIGH (GEFIXT) MW-1 Selbst-Cooldown eines GESUNDEN, nur in der Queue wartenden Web-Accounts:** + Der Caller-Timeout (`DEFAULT_UNRESTRICT_TIMEOUT_MS`=60s) feuert, waehrend die zweite Umwandlung eines + Accounts noch SERIELL in der Mega-Web-Single-Flight-Queue (90s `QUEUE_WAIT_TIMEOUT_MS`) auf den laufenden + Vorgaenger wartet — also bevor ueberhaupt echte Arbeit begann. `raceWithAbort` warf bisher unbedingt + `aborted:mega-web`; `unrestrictViaWeb` (debrid.ts:1887) flachte JEDEN signal-aborted-Fall zu `aborted:debrid` + ab; die Rotation (debrid.ts:2072) wertete das via `/aborted/i && !/timeout/i` → `ranLongEnough` + (elapsedMs schliesst die Queue-Wartezeit ein, also >= 8s) → **120s Account-Cooldown auf einen voellig + gesunden Account**. Genau die vom Nutzer gemeldete „Tool sperrt sich selbst"-Klasse (Web-Variante). + Zweiteiliger Fix: + (1) `MegaWebFallback.runExclusive` (mega-web-fallback.ts) trackt `workStarted` und reicht eine + `abortErrorFactory` an `raceWithAbort`: abgebrochen-bevor-Arbeit-begann → `Mega-Web Queue-Timeout (…)` + (matcht `/queue.?timeout/i`), nur ein echter In-Arbeit-Abbruch bleibt `aborted:mega-web`. + (2) `unrestrictViaWeb` (debrid.ts:1887) bewahrt einen `/queue.?timeout/i`-klassifizierten lastError statt + ihn zu `aborted:debrid` zu plaetten → Rotation trifft die bestehende Queue-Timeout-Ausnahme + (classifyAccountFailure 2243 → cooldownMs 0) statt den Abbruch-Cooldown-Zweig. + Rot-bewiesen NICHT-vakuum (zwei Tests, beide per Temp-Revert rot verifiziert): + - Teil 1 (mega-web-fallback.test.ts): echtes `MegaWebFallback`, Queue mit langsamem Erst-Job belegt, + Zweit-Call WAEHREND in Queue abgebrochen → `rejects.toThrow(/queue.?timeout/i)` (ohne Fix: `aborted:mega-web`). + - Teil 2 (debrid.test.ts): durch `DebridService.unrestrictLink` mit `RD_MEGA_ABORT_MIN_RUN_MS=0` (besiegt die + Vakuum-Falle: der buggy Pfad WUERDE selbst bei Sofort-Abbruch cooldownen), Signal bricht WAEHREND des + Calls ab, megaWeb meldet Queue-Timeout → `getMegaDebridAccountCooldownState` bleibt null (ohne Fix: 120s + „Abbruch/Timeout nach 0s"). tsc=6, volle mega-web+debrid-Suite 103 gruen inkl. der bestehenden + Abbruch-Cooldown-Tests (echter langsamer Account cooled WEITERHIN — keine Regression). + - **Rotations-Verifikation (Advisor-Shippability-Diskriminator):** Nach dem No-Cooldown-Pfad retried der + Manager `unrestrictWithAccounts` frisch. Die Account-Wahl sortiert per `megaDebridInFlight`-TIEFE + (debrid.ts:1981-1986, least-busy zuerst). Invariante: jeder Belegt-Halter der MegaWebFallback-Queue + entspricht einem in-flight `client.unrestrictLink`, das `megaDebridInFlight` inkrementiert hat (2033, + Dekrement nur im finally 2152). Also sieht der Retry des queue-getimeouteten Links den saturierenden + Account bei Tiefe>=1 und rotiert auf einen freien — MW-1 ist NACHWEISLICH besser als das alte 120s-Lockout + (das einen nur-belegten gesunden Account sperrte). Kein Tight-Retry-Loop auf saturierter Queue, solange + irgendein Account frei ist; sind ALLE saturiert, queued der least-busy (unvermeidbar — keine freie Kapazitaet, + aber das alte Cooldown haette es via Faux-Park SCHLIMMER gemacht). +- **DOKUMENTIERT, nicht gefixt (LOW) DL-1 DebridLink-Key-Cooldown bei User-Cancel-Abbruch:** classifyKeyFailure + (debrid.ts:3118-3126) gibt fuer Text mit „aborted" (via isRetryableErrorText 597 + isTransport 3119) + `cooldownMs: 15_000` zurueck — auch bei einem SCHNELLEN User-Cancel (kein Min-Run-Gate wie bei Mega 2072). + Symptom: 15s Key-Cooldown nach Nutzer-Abbruch. Advisor (revidiert eigenen frueheren „clean fix"-Call): + classifyKeyFailure hat KEIN elapsedMs und DebridLink hat keinen Min-Run-Knopf-Aequivalent zu + `getMegaDebridAbortMinRunMs()`. Der treue Fix (Caller-Gate bei 2787 spiegeln) braucht entweder cross-Provider- + Wiederverwendung des Mega-Knopfes oder einen neuen DebridLink-Knopf = neue Oberflaeche im Live-Hot-Path fuer + einen LOW-Bug (Symptom: 15s nach User-Cancel). Klart die eigene „clean + null-Regression"-Schwelle NICHT → + dokumentiert, nicht gefixt. +- **DOKUMENTIERT, nicht gefixt (MED) DL-CONCURRENCY-PILEUP:** Bei gleichzeitigen Umwandlungen, die ALLE + Mega-Accounts saturieren, queued der least-busy-Account weitere Links seriell (per-Account-Single-Flight) → + Wartezeiten stapeln sich. MW-1 entfernt die FRUEHERE versehentliche Backpressure (der falsche 120s-Cooldown + wirkte als grobe Ratenbegrenzung), routet aber korrekt per In-Flight-Tiefe um belegte Accounts herum statt sie + faelschlich zu sperren — strikt besser. Echter Fix (In-Flight-Tiefen-Spread groesser ziehen / globale + Web-Parallelitaetsgrenze) = groessere Aenderung, 4.-Bug-Risiko auf Live-Server → deferred. MW-1 reduziert das + Pileup-in-Park-Risiko bereits (kein Faux-Lockout merely-busy Accounts). + +## Runde 10 (Concurrency/Locking + Settings/Persistenz-Integritaet) — LETZTE RUNDE (Nutzer: „nach der Runde ist Schluss") +4 Finder (hybrid/targetPath-Races, Slot-Accounting, Settings-Migration/Persistenz, Backup-Restore/account-check) +→ adversarisch 3 Lenses → Synthese. Workflow wwyqsdrf9: 2 Finder (slot-accounting, settings-persistence) starben +an Stream-Idle-Timeout (Riesen-Dateien) → 0/7 confirmed war NUR fuer 2 von 4 Dimensionen ehrlich. KEIN stilles +Coverage-Loch akzeptiert → Re-Run wwyqsdrf9b (wfmu4pw2n) mit engerem Scope (Grep-dann-Region statt Ganzdatei). +- **Dim hybrid/targetPath + backup/account-check (wwyqsdrf9): 7 Kandidaten, ALLE refutiert (>=2/3), quell-reverifiziert.** + Kern-Invarianten halten: synchrones `claimTargetPath` (dl-mgr:6257) = nie zwei Items auf einem Pfad → alle drei + Hybrid-„Races" kollabieren zu Sub-Sekunden-Redundanzarbeit, kein Collision/Korruption; jeder Loss-Ausgang von + CRC/Extraction-Fail re-queued (`hybridExtractRequeue.add` 11739). Restore ist fail-closed (encrypted-or-reject, + Binaerheader „MDD1" wirft in JSON.parse vor normalizeSettings). account-check `valid:false` ist reiner + Renderer-Badge (kein src/main liest ihn als Gate; Disable laeuft ueber megaDebridDisabledAccountIds). +- **CONFIRMED MED (DOKUMENTIERT, nicht gefixt) BYTE-DROP-RETRY-1 (wfmu4pw2n):** Integrity-/too-small-/tiny-Retry + doppelzaehlt eine volle Datei in die Byte-Statistik. `dropItemContribution` (6247) loescht den + itemContributedBytes-Eintrag OHNE von session.totalDownloadedBytes abzuziehen (eigener Kommentar: „retry path + subtracts on its own"), aber die EINZIGE Subtraktion (9991-9997) liest genau diesen geloeschten Eintrag → 0 → + Subtraktion tot → Re-Download addiert N nochmal → (k+1)*fileSize nach k Integrity-Fails. enableIntegrityCheck + default true → organisch. Verdict isReal (CORRECTNESS+REPRO), aber BLAST-RADIUS = NUR Statistik: kein + Slot/Admission/Semaphore liest diese Counter (grep-belegt), Session-Counter self-healen bei jedem Neustart, + nur persistiertes totalDownloadedAllTime + avg-Speed bleiben dauerhaft inflationiert = kosmetisch. NICHT clean + (dropItemContribution ueber 26 Call-Sites ueberladen; ~10 im Retry-Bereich; ein 3-Site-Patch liefert + partielles/inkonsistentes Accounting = arguably schlimmer; Guard-Test dl-mgr.test.ts:6152 sperrt die + Completion-Removal-Semantik) → DOKUMENTIERT mit Rezept (dropItemContribution splitten in ForCompletion/ForRetry + nach Klassifikation aller Retry-Sites re-download-vs-terminal). Kosmetisch + nicht-clean → klart Live-Schwelle nicht. +- **CONFIRMED MED (GEFIXT) SET-MIG-01 (wfmu4pw2n):** Pre-v1.6.90-Config-Migration tot → Mega-Debrid wird beim + ersten Settings-Panel-Save still aus der Provider-Reihenfolge demotet. readSettingsFile (storage.ts:633) merged + `{...defaultSettings(), ...parsed}`; defaultSettings setzt megaDebridApiEnabled/WebEnabled:false (constants.ts:50-51) + BEVOR normalizeSettings laeuft → der `=== undefined`-Migrationszweig (355-360) ist tot auf dem Disk-Pfad (Git: + tot seit v1.6.90/0003d78). Renderer (App.tsx:447-494) gated Mega-Inklusion am false-Flag → persistDraftSettings + schreibt eine Mega-bereinigte providerOrder zurueck. Fix: reine `migrateLegacyMegaEnableFlags(parsed)` in + readSettingsFile — seedet apiEnabled=preferApi/webEnabled=!preferApi NUR wenn BEIDE Flags im RAW-parsed fehlen + UND Creds da sind (Creds-Erkennung wie normalizeSettings: asText(login)&&asText(password)). Lokalisiert, keine + normalizeSettings-Signatur-Aenderung, getypt (kein Cast). EHRLICHER Scope (Advisor): rettet NUR Legacy-Configs, + die seit dem Upgrade noch NICHT ueber das Settings-Panel neu gespeichert wurden (ein Post-Upgrade-Save schreibt + present-false → Trigger feuert nicht mehr) — schmales historisches Fenster, NICHT „rettet die Live-Mega dieses + Nutzers". HARTE Grenze (Advisor): Trigger bleibt absent-both; present-false NICHT anfassen (= bewusst-deaktiviert, + ununterscheidbar → das geparkte entscheidungen-offen #2-Migrationsrisiko). Rot-bewiesen NICHT-vakuum (Assertion NUR + auf den geladenen Flags — providerOrder demotet backend-seitig nicht = waere vakuum; via Temp-Revert rot bestaetigt: + apiEnabled true→false). Boundary-Test (bewusst-deaktiviert bleibt false + ohne Creds keine Migration) bleibt beim + Revert gruen = unabhaengig. Volle Suite 890 gruen, tsc=6. +- **REFUTED (wfmu4pw2n) SET-PERSIST-02:** Residual-Lost-Update nach R2-Generations-Guard — der einzige verlorene + Payload ist totalRuntimeAllTimeMs (`<=`-Ratchet, naechste Session re-derived) + contrived Sub-ms-Race der die + Prozess-Teardown gewinnen muss → self-healing kosmetisch, nicht erreichbar. +- **Coherence-Verdikt:** Concurrency-Accounting + Settings-Persistenz sind kohaerent. Byte-Counter sind + telemetry-only, voll entkoppelt von Admission/Slot (grep-belegt) → BYTE-DROP kann nicht stranden/freezen/ + ueber-admiten. Persistenz kohaerent bis auf EINE benannte Inkohaerenz: Backend normalizeConfiguredProvider + (storage.ts:144) mapt „megadebrid"→konkret unabhaengig der Flags, Renderer gated auf dem Flag = Wurzel von + SET-MIG-01 (jetzt am Loader gefixt). +- **EHRLICHE Coverage-Luecke (vom Audit selbst aufgedeckt, R10-HYB-3):** Die in project_pending genannte + Passwort-Cache-Race liegt NICHT in download-manager.ts (dort kein passwordCache/resolvePassword), sondern in + src/main/extractor.ts — ausserhalb des Round-10-Scopes. Bewusst NICHT in dieser letzten Runde auditiert (Nutzer: + „nach der Runde ist Schluss"); als das eine identifizierte, un-auditierte Concurrency-Seam fuer eine etwaige + kuenftige Runde dokumentiert statt still fallengelassen. + +## GOAL-ABSCHLUSS +Runde 9 + 10 erledigt (Nutzer-Anpassung). Release v1.7.220 buendelt: 85c8d6b (Synthese C1/C2/RANGE1-Haertung) + +MW-1 (HIGH, Web-Selbstcooldown) + SET-MIG-01 (MED, Legacy-Mega-Demotion). Gitea + GitHub-Mirror MIT .exe (4 Assets). +Dokumentiert-nicht-gefixt: DL-1, DL-CONCURRENCY-PILEUP, BYTE-DROP-RETRY-1, extractor.ts-Passwort-Cache-Seam. +Beim Nutzer (nicht autonom): 60s-Failover-Kappung + gespiegelter Mega-API/Web-Schalter (entscheidungen-offen.md). + ## Runde 1 (laeuft) - Discover+Verify-Workflow ueber 7 Subsysteme (scheduler-slots, unrestrict-retry, mega-rotation, classify-cooldown, mega-web-token, provider-chain-timeout, account-availability). diff --git a/tasks/entscheidungen-offen.md b/tasks/entscheidungen-offen.md index 276a617..78042e8 100644 --- a/tasks/entscheidungen-offen.md +++ b/tasks/entscheidungen-offen.md @@ -63,6 +63,30 @@ ODER zwei unabhängige Pro-Modus-Schalter (mehr Kontrolle, aber UI + Migration n ## Erledigt in dieser Runde (zur Info, kein Handlungsbedarf) +- **Alte Konfiguration: Mega-Debrid fällt nach einem Upgrade nicht mehr still aus der Provider-Reihenfolge:** + Eine Konfigurationsdatei, die noch von einer sehr alten Version (vor v1.6.90) stammt, kannte die getrennten + Mega-Debrid „API aktiv"/„Web aktiv"-Schalter noch nicht. Beim Laden wurden diese fehlenden Schalter still auf + „aus" gesetzt, obwohl Mega-Zugangsdaten vorhanden waren — und sobald man danach das erste Mal die Einstellungen + speicherte, wurde Mega-Debrid dadurch lautlos aus der Provider-Reihenfolge entfernt. Jetzt wird beim Laden einer + solchen alten Datei erkannt, dass die Schalter komplett fehlen, und Mega-Debrid passend zu deiner Bevorzugung + (API oder Web) aktiviert — genau die Migration, die ursprünglich gedacht war, aber durch einen Default-Vorrang + nie ausgelöst hatte. Ehrlicher Umfang: Das betrifft nur alte Dateien, die seit dem Upgrade noch NICHT über die + Einstellungen neu gespeichert wurden — wer seit dem Update schon einmal in den Einstellungen gespeichert hat, hat + die Schalter bereits als „aus" stehen und greift dort weiterhin manuell ein (das ist Absicht: ein bewusst auf + „aus" gestellter Schalter wird NICHT wieder angeschaltet). Rot-bewiesener Test; voller Testlauf grün. + +- **Mega-Web: gesunder Account sperrt sich nicht mehr selbst, nur weil er gerade belegt war (deine „Tool sperrt sich selbst"-Klasse, Web-Variante):** + Wenn mehrere Links gleichzeitig über DENSELBEN Mega-Account umgewandelt wurden, laufen sie absichtlich nacheinander + (eine Warteschlange pro Account, damit nicht doppelt eingeloggt/gehämmert wird). Wartete ein Link in dieser Schlange + noch auf seinen Vorgänger und lief dabei der 60-Sekunden-Gesamttimeout ab, wurde der Abbruch fälschlich wie ein echter + Account-Fehler gewertet → der völlig gesunde Account bekam 120 Sekunden Sperre. Jetzt wird ein Abbruch, der NOCH IN DER + Warteschlange passiert (bevor echte Arbeit begann), als reiner Warteschlangen-Timeout erkannt: KEINE Account-Sperre, + der Link wird einfach erneut versucht und rotiert dann von selbst auf einen freien Account. Ein echter Abbruch MITTEN + in der Arbeit sperrt den Account weiterhin (damit langsame Accounts korrekt übersprungen werden) — das blieb unverändert. + Zwei rot-bewiesene Tests (jeder ohne Fix nachweislich rot; der zweite läuft komplett durch die echte Account-Rotation + und prüft, dass keine Sperre gesetzt wird). Die Rotation auf einen freien Account ist über die Auslastungs-Verteilung + (am-wenigsten-belegter-Account-zuerst) abgesichert — strikt besser als die alte 120s-Pauschalsperre. + - **Stille Datei-Beschädigung beim Resume nach Verbindungsabbruch behoben (für Dateien mit bekannter Größe):** Wenn ein Debrid-Server beim Abbruch einen kleinen Fehler-Müll-Block mitten in den Datenstrom schreibt und das genau im LETZTEN Wiederhol-Versuch passierte, blieb dieser Müll in der Datei und der anschließende diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index edbf86e..ad3559c 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -1961,6 +1961,43 @@ describe("debrid service", () => { expect(result.directUrl).toBe("https://mega-web.example/ok.rar"); }, 20000); + it("setzt KEINEN Account-Cooldown, wenn der Mega-Web-Abbruch nur ein Queue-Timeout war (Account war belegt, nicht ungesund)", async () => { + process.env.RD_MEGA_ABORT_MIN_RUN_MS = "0"; + const settings = { + ...defaultSettings(), + token: "", + bestToken: "", + allDebridToken: "", + megaLogin: "user", + megaPassword: "pass", + megaCredentials: "user:pass", + 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; + + const controller = new AbortController(); + let calls = 0; + const megaWeb = vi.fn(async () => { + calls += 1; + controller.abort("caller-timeout"); + throw new Error("Mega-Web Queue-Timeout (abgebrochen nach 60s Wartezeit, Account war belegt)"); + }); + + const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); + await expect( + service.unrestrictLink("https://rapidgator.net/file/queue-timeout-no-cooldown", controller.signal) + ).rejects.toThrow(); + + const key = `${getMegaDebridAccountId("user")}:web`; + expect(getMegaDebridAccountCooldownState(key)).toBeNull(); + expect(calls).toBeGreaterThanOrEqual(1); + }, 20000); + it("escalates a Mega-Debrid account to 'until restart' after the empty-response streak threshold", () => { const key = `${getMegaDebridAccountId("user1")}:web`; expect(MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART).toBe(3); diff --git a/tests/mega-web-fallback.test.ts b/tests/mega-web-fallback.test.ts index af3ee3b..c13eb11 100644 --- a/tests/mega-web-fallback.test.ts +++ b/tests/mega-web-fallback.test.ts @@ -314,5 +314,41 @@ describe("mega-web-fallback", () => { clearTimeout(timer); } }); + + it("klassifiziert einen Abbruch WAEHREND in der Queue als Queue-Timeout (nicht harter Abbruch), damit der belegte Account nicht bestraft wird", async () => { + let releaseLogin: () => void = () => {}; + const loginGate = new Promise((resolve) => { releaseLogin = resolve; }); + globalThis.fetch = vi.fn(async (url: string | URL | Request) => { + const u = String(url); + if (u.includes("form=login")) { + await loginGate; + const headers = new Headers(); + headers.append("set-cookie", "session=c; path=/"); + return new Response("", { headers, status: 200 }); + } + if (u.includes("page=debrideur")) return new Response('
', { status: 200 }); + if (u.includes("form=debrid")) return new Response(`

Link: https://mega.debrid/l

d
`, { status: 200 }); + if (u.includes("ajax=debrid")) return new Response(JSON.stringify({ link: "https://mega.direct/ok" }), { status: 200 }); + return new Response("Not found", { status: 404 }); + }) as unknown as typeof fetch; + + const fallback = new MegaWebFallback(() => ({ login: "same", password: "pw" })); + const firstCtrl = new AbortController(); + const first = fallback.unrestrict("https://mega.debrid/a", firstCtrl.signal, { login: "same", password: "pw" }); + + await new Promise((r) => setTimeout(r, 25)); + + const secondCtrl = new AbortController(); + const second = fallback.unrestrict("https://mega.debrid/b", secondCtrl.signal, { login: "same", password: "pw" }); + + await new Promise((r) => setTimeout(r, 25)); + secondCtrl.abort("caller-timeout"); + + await expect(second).rejects.toThrow(/queue.?timeout/i); + + releaseLogin(); + await first.catch(() => null); + await new Promise((r) => setTimeout(r, 20)); + }, 10000); }); }); diff --git a/tests/storage.test.ts b/tests/storage.test.ts index 797f8b1..671ad06 100644 --- a/tests/storage.test.ts +++ b/tests/storage.test.ts @@ -180,6 +180,67 @@ describe("settings storage", () => { expect(webNormalized.hosterRouting.rapidgator).toBe("megadebrid-web"); }); + it("migriert eine pre-v1.6.90-Config (Mega-Creds, beide Enable-Flags fehlen) zu aktiviertem Mega-Debrid statt es still auf false zu setzen", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-")); + tempDirs.push(dir); + const paths = createStoragePaths(dir); + const legacyApi = { + megaLogin: "mega-user", + megaPassword: "mega-pass", + megaDebridPreferApi: true, + providerPrimary: "realdebrid", + providerSecondary: "megadebrid" + }; + fs.writeFileSync(paths.configFile, JSON.stringify(legacyApi), "utf8"); + const loadedApi = loadSettings(paths); + expect(loadedApi.megaDebridApiEnabled).toBe(true); + expect(loadedApi.megaDebridWebEnabled).toBe(false); + + const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-")); + tempDirs.push(dir2); + const paths2 = createStoragePaths(dir2); + const legacyWeb = { + megaLogin: "mega-user", + megaPassword: "mega-pass", + megaDebridPreferApi: false, + providerPrimary: "realdebrid", + providerSecondary: "megadebrid" + }; + fs.writeFileSync(paths2.configFile, JSON.stringify(legacyWeb), "utf8"); + const loadedWeb = loadSettings(paths2); + expect(loadedWeb.megaDebridApiEnabled).toBe(false); + expect(loadedWeb.megaDebridWebEnabled).toBe(true); + }); + + it("re-aktiviert KEINE bewusst deaktivierten Mega-Flags und migriert nicht ohne Mega-Creds", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-")); + tempDirs.push(dir); + const paths = createStoragePaths(dir); + const deliberatelyDisabled = { + megaLogin: "mega-user", + megaPassword: "mega-pass", + megaDebridPreferApi: true, + megaDebridApiEnabled: false, + megaDebridWebEnabled: false + }; + fs.writeFileSync(paths.configFile, JSON.stringify(deliberatelyDisabled), "utf8"); + const loaded = loadSettings(paths); + expect(loaded.megaDebridApiEnabled).toBe(false); + expect(loaded.megaDebridWebEnabled).toBe(false); + + const dir2 = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-")); + tempDirs.push(dir2); + const paths2 = createStoragePaths(dir2); + const noCreds = { + megaDebridPreferApi: true, + providerPrimary: "realdebrid" + }; + fs.writeFileSync(paths2.configFile, JSON.stringify(noCreds), "utf8"); + const loadedNoCreds = loadSettings(paths2); + expect(loadedNoCreds.megaDebridApiEnabled).toBe(false); + expect(loadedNoCreds.megaDebridWebEnabled).toBe(false); + }); + it("normalizes provider daily limits and resets stale daily usage", () => { const [debridLinkKey] = parseDebridLinkApiKeys("dl-key-one"); const normalized = normalizeSettings({