Compare commits
4 Commits
6ae9f5d725
...
39c587c8b7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39c587c8b7 | ||
|
|
fa2c7eb6be | ||
|
|
85c8d6bdf9 | ||
|
|
f2ae1c19e2 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.219",
|
"version": "1.7.220",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -1885,6 +1885,9 @@ class MegaDebridClient {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
if (signal?.aborted) {
|
if (signal?.aborted) {
|
||||||
|
if (/queue.?timeout/i.test(lastError)) {
|
||||||
|
throw new Error(lastError.replace(/^Error:\s*/i, ""));
|
||||||
|
}
|
||||||
throw new Error("aborted:debrid");
|
throw new Error("aborted:debrid");
|
||||||
}
|
}
|
||||||
if (web?.directUrl) {
|
if (web?.directUrl) {
|
||||||
|
|||||||
@ -9436,24 +9436,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const megaResetPark = parseMegaDebridResetPark(errorText);
|
const megaRawError = error instanceof Error ? String(error.message || "") : String(error || "");
|
||||||
if (megaResetPark && active.unrestrictRetries < maxUnrestrictRetries) {
|
const megaCooldownRetry = parseMegaDebridCooldownRetry(megaRawError);
|
||||||
active.unrestrictRetries += 1;
|
|
||||||
item.retries += 1;
|
|
||||||
logger.warn(`Mega-Debrid bis Tagesreset gesperrt: item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${megaResetPark.delayMs}ms, link=${item.url.slice(0, 80)}`);
|
|
||||||
this.queueRetry(
|
|
||||||
item,
|
|
||||||
active,
|
|
||||||
megaResetPark.delayMs,
|
|
||||||
`Mega-Debrid bis Tagesreset gesperrt, Pause ${Math.ceil(megaResetPark.delayMs / 1000)}s`
|
|
||||||
);
|
|
||||||
item.lastError = megaResetPark.detail || errorText;
|
|
||||||
this.persistSoon();
|
|
||||||
this.emitState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const megaCooldownRetry = parseMegaDebridCooldownRetry(errorText);
|
|
||||||
if (megaCooldownRetry && active.unrestrictRetries < maxUnrestrictRetries) {
|
if (megaCooldownRetry && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
active.unrestrictRetries += 1;
|
active.unrestrictRetries += 1;
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
@ -9470,6 +9454,23 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const megaResetPark = parseMegaDebridResetPark(megaRawError);
|
||||||
|
if (megaResetPark && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
|
active.unrestrictRetries += 1;
|
||||||
|
item.retries += 1;
|
||||||
|
logger.warn(`Mega-Debrid bis Tagesreset gesperrt: item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${megaResetPark.delayMs}ms, link=${item.url.slice(0, 80)}`);
|
||||||
|
this.queueRetry(
|
||||||
|
item,
|
||||||
|
active,
|
||||||
|
megaResetPark.delayMs,
|
||||||
|
`Mega-Debrid bis Tagesreset gesperrt, Pause ${Math.ceil(megaResetPark.delayMs / 1000)}s`
|
||||||
|
);
|
||||||
|
item.lastError = megaResetPark.detail || errorText;
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isMegaDebridTransientResolveFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
if (isMegaDebridTransientResolveFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
active.unrestrictRetries += 1;
|
active.unrestrictRetries += 1;
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
@ -10607,7 +10608,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
rewindTarget
|
rewindTarget
|
||||||
});
|
});
|
||||||
} catch (rewindError) {
|
} catch (rewindError) {
|
||||||
logAttemptEvent("WARN", "Resume-Schutz: finales Rueckspulen fehlgeschlagen", {
|
try {
|
||||||
|
fs.rmSync(effectiveTargetPath, { force: true });
|
||||||
|
item.downloadedBytes = 0;
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
|
logAttemptEvent("WARN", "Resume-Schutz: finales Rueckspulen fehlgeschlagen, Teil-Datei verworfen", {
|
||||||
attempt,
|
attempt,
|
||||||
written,
|
written,
|
||||||
rewindTarget,
|
rewindTarget,
|
||||||
|
|||||||
@ -177,12 +177,12 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal, abortErrorFactory: () => Error = abortError): Promise<T> {
|
||||||
if (!signal) {
|
if (!signal) {
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
if (signal.aborted) {
|
if (signal.aborted) {
|
||||||
throw abortError();
|
throw abortErrorFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
@ -194,7 +194,7 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
|
|||||||
}
|
}
|
||||||
settled = true;
|
settled = true;
|
||||||
signal.removeEventListener("abort", onAbort);
|
signal.removeEventListener("abort", onAbort);
|
||||||
reject(abortError());
|
reject(abortErrorFactory());
|
||||||
};
|
};
|
||||||
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true });
|
signal.addEventListener("abort", onAbort, { once: true });
|
||||||
@ -284,6 +284,7 @@ export class MegaWebFallback {
|
|||||||
private async runExclusive<T>(job: () => Promise<T>, key: string, signal?: AbortSignal): Promise<T> {
|
private async runExclusive<T>(job: () => Promise<T>, key: string, signal?: AbortSignal): Promise<T> {
|
||||||
const queuedAt = Date.now();
|
const queuedAt = Date.now();
|
||||||
const QUEUE_WAIT_TIMEOUT_MS = 90000;
|
const QUEUE_WAIT_TIMEOUT_MS = 90000;
|
||||||
|
let workStarted = false;
|
||||||
const guardedJob = async (): Promise<T> => {
|
const guardedJob = async (): Promise<T> => {
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
const waited = Date.now() - queuedAt;
|
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` });
|
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)`);
|
throw new Error(`Mega-Web Queue-Timeout (${Math.floor(waited / 1000)}s gewartet)`);
|
||||||
}
|
}
|
||||||
|
workStarted = true;
|
||||||
const workStartedAt = Date.now();
|
const workStartedAt = Date.now();
|
||||||
try {
|
try {
|
||||||
const result = await job();
|
const result = await job();
|
||||||
@ -304,7 +306,11 @@ export class MegaWebFallback {
|
|||||||
const prev = this.queues.get(key) ?? Promise.resolve();
|
const prev = this.queues.get(key) ?? Promise.resolve();
|
||||||
const run = prev.then(guardedJob, guardedJob);
|
const run = prev.then(guardedJob, guardedJob);
|
||||||
this.queues.set(key, run.then(() => undefined, () => undefined));
|
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<string> {
|
private async login(login: string, password: string, signal?: AbortSignal): Promise<string> {
|
||||||
|
|||||||
@ -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 {
|
function readSettingsFile(filePath: string): AppSettings | null {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
|
||||||
const merged = normalizeSettings({
|
const merged = normalizeSettings({
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
...parsed
|
...migrateLegacyMegaEnableFlags(parsed)
|
||||||
});
|
});
|
||||||
return sanitizeCredentialPersistence(merged);
|
return sanitizeCredentialPersistence(merged);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,8 +1,125 @@
|
|||||||
# Autonomer Audit-Loop — Download/Fehler/Rotation (Goal 2026-06-17, 8h)
|
# Autonomer Audit-Loop — Download/Fehler/Rotation (Goal 2026-06-17, 8h)
|
||||||
|
|
||||||
|
## GOAL-ANPASSUNG (Nutzer, nach Runde 8 + Synthese-Start): noch Runde 9 + 10, dann Goal BEENDEN.
|
||||||
|
Plan: Synthese-Pass (wrexz7mdf, Capstone R1-8) auswerten → Runde 9 (Provider-spezifische
|
||||||
|
Unrestrict-/Rotations-Pfade: Mega-Web-Fallback Session/Single-Flight, AllDebrid Host-Cooldown/
|
||||||
|
Rapidgator-Backoff, DebridLink-Key-Rotation, Passwort-Cache-Race) → Runde 10 (Concurrency/Locking +
|
||||||
|
Settings/Persistenz-Integritaet: Hybrid-Race, targetPath-Claim/Release-Races, Settings-Migration,
|
||||||
|
Backup/Restore, account-check) → Abschlussbericht + Ende. Fixes je rot-bewiesen, buendeln zu v1.7.220 falls HIGH/MED.
|
||||||
|
|
||||||
Disziplin: erst BELEGEN (Code-Zitat + konkretes Szenario), dann adversarisch verifizieren,
|
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.
|
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)
|
## Runde 1 (laeuft)
|
||||||
- Discover+Verify-Workflow ueber 7 Subsysteme (scheduler-slots, unrestrict-retry, mega-rotation,
|
- Discover+Verify-Workflow ueber 7 Subsysteme (scheduler-slots, unrestrict-retry, mega-rotation,
|
||||||
classify-cooldown, mega-web-token, provider-chain-timeout, account-availability).
|
classify-cooldown, mega-web-token, provider-chain-timeout, account-availability).
|
||||||
@ -168,6 +285,12 @@ importiert+konstruiert). Risiko Startup-Regression empirisch ausgeschlossen.
|
|||||||
- Advisor NICHT auf Kadenz pollen — nur bei echter Ship/Fix-Entscheidung mit neuer Info.
|
- Advisor NICHT auf Kadenz pollen — nur bei echter Ship/Fix-Entscheidung mit neuer Info.
|
||||||
- Die 2 HIGH Produkt/UI-Entscheidungen bleiben beim Nutzer (nicht autonom shippen).
|
- Die 2 HIGH Produkt/UI-Entscheidungen bleiben beim Nutzer (nicht autonom shippen).
|
||||||
|
|
||||||
|
## Release-Status: v1.7.219 RELEASED (Gitea 6ae9f5d + GitHub-Mirror df3670a)
|
||||||
|
Roll-up Runde 7+8: be15419 MED VP-1 dt.-Tonspur + eacd0c9 HIGH RANGE-1 Silent-Corruption + REWIND-TRUNCATE.
|
||||||
|
HIGH rechtfertigt Release. Advisor explizit cleared (vor Implementierung konsultiert, Check A verifiziert).
|
||||||
|
Mirror kuratiert ohne CLAUDE.md/tasks/ (Leak-Check sauber). NAECHSTER SCHRITT (Advisor): Synthese/Regressions-
|
||||||
|
Pass ueber kumulativen Diff aller Session-Fixes (Interaktion), KEINE weitere Discovery-Runde.
|
||||||
|
|
||||||
## Release-Status: v1.7.218 RELEASED (Gitea bbb9355 + GitHub-Mirror 94d143b)
|
## Release-Status: v1.7.218 RELEASED (Gitea bbb9355 + GitHub-Mirror 94d143b)
|
||||||
Roll-up Runde 4+5+6: 986fbab MED Provider-Cooldown-Routing + 03c908b No-Stranding-Test +
|
Roll-up Runde 4+5+6: 986fbab MED Provider-Cooldown-Routing + 03c908b No-Stranding-Test +
|
||||||
f1e35f5 LOW Mega-Tagesreset-Park + d2a1b83 HIGH Shelve-Loop-RetryLimit. HIGH rechtfertigt das
|
f1e35f5 LOW Mega-Tagesreset-Park + d2a1b83 HIGH Shelve-Loop-RetryLimit. HIGH rechtfertigt das
|
||||||
@ -179,6 +302,28 @@ Multi-Agent-adversarisch, Routing-Fix frueher advisor-gesegnet, Praezedenz 215/2
|
|||||||
Runde-5/6-Charakterisierungen (PP-SEM-1 benign, DISK-1 deferred, deferred-LOW-Cluster #10/#11/#13
|
Runde-5/6-Charakterisierungen (PP-SEM-1 benign, DISK-1 deferred, deferred-LOW-Cluster #10/#11/#13
|
||||||
benign) NICHT released — dokumentiert.
|
benign) NICHT released — dokumentiert.
|
||||||
|
|
||||||
|
## SYNTHESE/Regressions-Pass (Fix-Interaktion ueber kumulativen Diff v1.7.212..HEAD) — Workflow wrexz7mdf
|
||||||
|
3 Reviewer (Catch-Cascade / Streaming / Provider-Chain) + adversarisch verifizieren. 3 confirmed (1 MED, 2 LOW),
|
||||||
|
2 refuted. Bestaetigt: Rest komponiert sauber; Risiko war (wie Advisor sagte) Fix-Interaktion, nicht neue Bugs.
|
||||||
|
- **CONFIRMED MED (GEFIXT) C1 reset_park maskiert kurzen Cooldown:** Bei BEIDEN Mega-Modi aktiv aggregiert die
|
||||||
|
Provider-Kette beide Token (`mega_debrid_reset_park:LONG` von API-Park + `mega_debrid_cooldown:30000` von
|
||||||
|
Web-Cooldown). Manager prueft reset_park VOR cooldown → Item ~24h geparkt obwohl Web in ~30s erholt. Fix:
|
||||||
|
Cooldown-Zweig VOR reset_park (kuerzerer Delay gewinnt). Rot-bewiesen (Single-Pass-processItem-Test: Aggregat
|
||||||
|
beider Token → retryAfter < 60s, fullStatus "Cooldown" nicht "Tagesreset"; ohne Reorder ~24h).
|
||||||
|
- **CONFIRMED LOW (GEFIXT) C2 Token-Truncation:** reset_park-Token konnte von compactErrorText (220-Zeichen-Cap)
|
||||||
|
abgeschnitten werden, wenn Mega nicht Lead + vorheriger Provider verbose → Park still uebersprungen. Fix: Mega-
|
||||||
|
Token aus der UNGEKUERZTEN error.message parsen (megaRawError). (Im selben Edit wie C1.)
|
||||||
|
- **CONFIRMED LOW (GEHAERTET + Doku korrigiert) RANGE1-TRUNCATEFAIL-FINALIZE:** Mein 219-Commit OVERCLAIMte —
|
||||||
|
bei FEHLGESCHLAGENEM finalem Rewind-truncate finalisiert tryFinalizeItemFromDisk(9238) die Garbage-Datei
|
||||||
|
(size==totalBytes) VOR dem Re-Entry. NET-NEUTRAL vs pre-audit (v1.7.212 hatte denselben Finalize), KEINE
|
||||||
|
Regression. Haertung: bei truncate-Fehler jetzt rmSync(Teil-Datei)+downloadedBytes=0 → Finalize lehnt ab →
|
||||||
|
sauberer Re-Download (best-effort; faellt der rm auch, bleibt es net-neutral). Macht den 219-Claim wahr.
|
||||||
|
- **Refuted (1/3 je):** reset_park von shelve-guard/unrestrictRetries-Exhaustion unter finite retryLimit
|
||||||
|
verdraengt — beide nicht bestaetigt (compound/nicht erreichbar).
|
||||||
|
- **Capstone-Verdikt:** Die 8-Runden-Fixes komponieren — inner-rewind success-only-reset und final-rewind sind
|
||||||
|
per-attempt mutually exclusive; prealloc-reconcile double-truncated nicht nach erfolgreichem Rewind; Routing
|
||||||
|
+ cooldown + park kohaerent NACH C1/C2-Fix. Diese 3 Synthese-Fixes buendeln zu v1.7.220.
|
||||||
|
|
||||||
## Runde 8 (Byte-Streaming downloadToFile: Range/Resume/Append/Truncation) — Workflow wwjz4srkq
|
## Runde 8 (Byte-Streaming downloadToFile: Range/Resume/Append/Truncation) — Workflow wwjz4srkq
|
||||||
3 Finder + adversarisch verifizieren. 3 confirmed (1 HIGH + 2 MED, alle Silent-Corruption-Familie), 1 refuted.
|
3 Finder + adversarisch verifizieren. 3 confirmed (1 HIGH + 2 MED, alle Silent-Corruption-Familie), 1 refuted.
|
||||||
Advisor VOR Implementierung konsultiert (HIGH-Hot-Path) — Design + Check-A-Branch (totalBytes-null) bestaetigt.
|
Advisor VOR Implementierung konsultiert (HIGH-Hot-Path) — Design + Check-A-Branch (totalBytes-null) bestaetigt.
|
||||||
|
|||||||
@ -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)
|
## 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):**
|
- **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
|
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
|
das genau im LETZTEN Wiederhol-Versuch passierte, blieb dieser Müll in der Datei und der anschließende
|
||||||
|
|||||||
@ -1961,6 +1961,43 @@ describe("debrid service", () => {
|
|||||||
expect(result.directUrl).toBe("https://mega-web.example/ok.rar");
|
expect(result.directUrl).toBe("https://mega-web.example/ok.rar");
|
||||||
}, 20000);
|
}, 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", () => {
|
it("escalates a Mega-Debrid account to 'until restart' after the empty-response streak threshold", () => {
|
||||||
const key = `${getMegaDebridAccountId("user1")}:web`;
|
const key = `${getMegaDebridAccountId("user1")}:web`;
|
||||||
expect(MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART).toBe(3);
|
expect(MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART).toBe(3);
|
||||||
|
|||||||
@ -301,6 +301,57 @@ describe("download manager", () => {
|
|||||||
expect(session.items[itemId].retries).toBeLessThan(20);
|
expect(session.items[itemId].retries).toBeLessThan(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers the short Mega cooldown over the until-reset park when an aggregate error carries both tokens", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-mega-both-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||||
|
initPackageLogs(storagePaths.baseDir);
|
||||||
|
initItemLogs(storagePaths.baseDir);
|
||||||
|
|
||||||
|
const session = emptySession();
|
||||||
|
const packageId = "mega-both-pkg";
|
||||||
|
const itemId = "mega-both-item";
|
||||||
|
const outputDir = path.join(root, "downloads", "MegaBoth");
|
||||||
|
const extractDir = path.join(root, "extract", "MegaBoth");
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
fs.mkdirSync(extractDir, { recursive: true });
|
||||||
|
session.packageOrder = [packageId];
|
||||||
|
session.packages[packageId] = {
|
||||||
|
id: packageId, name: "MegaBoth", outputDir, extractDir,
|
||||||
|
status: "downloading", itemIds: [itemId], cancelled: false, enabled: true,
|
||||||
|
createdAt: Date.now(), updatedAt: Date.now()
|
||||||
|
} as any;
|
||||||
|
session.items[itemId] = {
|
||||||
|
id: itemId, packageId, url: "https://rapidgator.net/file/mega-both.rar", provider: "megadebrid-api",
|
||||||
|
status: "downloading", retries: 0, speedBps: 0, downloadedBytes: 0, totalBytes: null,
|
||||||
|
progressPercent: 0, fileName: "mega-both.rar", targetPath: "", resumable: true,
|
||||||
|
attempts: 0, lastError: "", fullStatus: "", createdAt: Date.now(), updatedAt: Date.now()
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
{ ...defaultSettings(), megaLogin: "u", megaPassword: "p", megaCredentials: "u:p", megaDebridApiEnabled: true, megaDebridWebEnabled: true, outputDir: path.join(root, "downloads"), extractDir: path.join(root, "extract") },
|
||||||
|
session,
|
||||||
|
storagePaths
|
||||||
|
);
|
||||||
|
(manager as any).retryStateByItem.set(itemId, { freshRetryUsed: true, resumeHardResetUsed: true, stallRetries: 0, genericErrorRetries: 0, unrestrictRetries: 0 });
|
||||||
|
(manager as any).debridService.unrestrictLink = async () => {
|
||||||
|
throw new Error("Unrestrict fehlgeschlagen: Mega-Debrid API: mega_debrid_reset_park:86400000:Alle Accounts bis zum Tagesreset gesperrt | Mega-Debrid Web: mega_debrid_cooldown:30000:rate limit");
|
||||||
|
};
|
||||||
|
|
||||||
|
const active = { itemId, packageId, abortController: new AbortController(), abortReason: "none", resumable: true, nonResumableCounted: false, blockedOnDiskWrite: false, blockedOnDiskSince: 0 };
|
||||||
|
(manager as any).activeTasks.set(itemId, active);
|
||||||
|
|
||||||
|
const before = Date.now();
|
||||||
|
await (manager as any).processItem(active);
|
||||||
|
|
||||||
|
const retryAfter = (manager as any).retryAfterByItem.get(itemId) as number;
|
||||||
|
expect(retryAfter).toBeGreaterThan(0);
|
||||||
|
const delayMs = retryAfter - before;
|
||||||
|
expect(delayMs).toBeLessThan(60000);
|
||||||
|
expect(session.items[itemId].fullStatus).toMatch(/Cooldown/i);
|
||||||
|
expect(session.items[itemId].fullStatus).not.toMatch(/Tagesreset/i);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the quick post-process requeue once the final package items are finished", () => {
|
it("keeps the quick post-process requeue once the final package items are finished", () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-postprocess-final-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-postprocess-final-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
@ -314,5 +314,41 @@ describe("mega-web-fallback", () => {
|
|||||||
clearTimeout(timer);
|
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<void>((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('<form id="debridForm"></form>', { status: 200 });
|
||||||
|
if (u.includes("form=debrid")) return new Response(`<div class="acp-box"><h3>Link: https://mega.debrid/l</h3><a href="javascript:processDebrid(1,'code',0)">d</a></div>`, { 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -180,6 +180,67 @@ describe("settings storage", () => {
|
|||||||
expect(webNormalized.hosterRouting.rapidgator).toBe("megadebrid-web");
|
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", () => {
|
it("normalizes provider daily limits and resets stale daily usage", () => {
|
||||||
const [debridLinkKey] = parseDebridLinkApiKeys("dl-key-one");
|
const [debridLinkKey] = parseDebridLinkApiKeys("dl-key-one");
|
||||||
const normalized = normalizeSettings({
|
const normalized = normalizeSettings({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user