Compare commits

..

No commits in common. "6ae9f5d7252be42f571f781b7a26c0711819e270" and "bbb935589e020d946436c9d95c3f125b2af2a990" have entirely different histories.

7 changed files with 9 additions and 243 deletions

View File

@ -1,6 +1,6 @@
{
"name": "real-debrid-downloader",
"version": "1.7.219",
"version": "1.7.218",
"description": "Desktop downloader",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",

View File

@ -9633,7 +9633,6 @@ export class DownloadManager extends EventEmitter {
await fs.promises.truncate(effectiveTargetPath, resumeStart);
existingBytes = resumeStart;
item.downloadedBytes = Math.min(item.downloadedBytes, existingBytes);
resumeRewindBytesNextAttempt = 0;
logAttemptEvent("WARN", "Resume-Schutz aktiv: Teil-Datei vor Retry zurueckgespult", {
attempt,
previousBytes,
@ -9647,6 +9646,8 @@ export class DownloadManager extends EventEmitter {
rewindBytes,
error: compactErrorText(rewindError)
});
} finally {
resumeRewindBytesNextAttempt = 0;
}
} else if (resumeRewindBytesNextAttempt > 0) {
resumeRewindBytesNextAttempt = 0;
@ -10592,29 +10593,6 @@ export class DownloadManager extends EventEmitter {
await sleep(retryDelayWithJitter(attempt, 250));
continue;
}
if (
item.totalBytes != null && item.totalBytes > 0
&& written > existingBytes
&& shouldRewindResumeTail(normalizedLastError)
) {
const rewindTarget = Math.max(0, written - RESUME_REWIND_BYTES);
item.downloadedBytes = Math.min(item.downloadedBytes, rewindTarget);
try {
await fs.promises.truncate(effectiveTargetPath, rewindTarget);
logAttemptEvent("WARN", "Resume-Schutz: letzter Versuch vor Linkerneuerung zurueckgespult", {
attempt,
written,
rewindTarget
});
} catch (rewindError) {
logAttemptEvent("WARN", "Resume-Schutz: finales Rueckspulen fehlgeschlagen", {
attempt,
written,
rewindTarget,
error: compactErrorText(rewindError)
});
}
}
if (maxAttemptsBySetting > maxAttempts) {
const exhaustedError = existingBytes > 0 && normalizedLastError.startsWith("download_underflow:")
? `resume_download_underflow:${normalizedLastError.slice("download_underflow:".length)}`

View File

@ -104,9 +104,6 @@ function isGermanStream(stream: ProbedAudioStream): boolean {
// Free-text title fallback (used when the language tag is missing). Full words
// only — the 2-3 letter codes ger/deu are too ambiguous in a title and would
// pick the wrong track to keep (which then deletes the real German one).
if (lang) {
return false;
}
const title = (stream.title || "").toLowerCase();
return /\b(german|deutsch)\b/.test(title);
}

View File

@ -154,82 +154,12 @@ Produkt-Tradeoff geht an den Nutzer (tasks/entscheidungen-offen.md).
Der breitere Fix (Selektierbarkeit cooldown-aware machen) bewusst NICHT gemacht (groesserer
Blast-Radius auf Live-Server).
## Boot-Verifikation (Advisor-Punkt: Suite gruen != App bootet)
Smoke-Test des gebauten 1.7.218-Binaries (release\win-unpacked, throwaway userData, leere Session):
laeuft >10s stabil, spawnt die normalen 4 Electron-Prozesse (main+GPU+renderer+utility), schreibt
userData/runtime → BOOTET. Boot-Pfad (main.ts-Boot, app-controller-Init, Window/IPC-Registrierung)
von diesem Audit NICHT angefasst; alle editierten Methoden sind Download-Zeit (von der 884er-Suite
importiert+konstruiert). Risiko Startup-Regression empirisch ausgeschlossen.
## Advisor-Leitlinie (Stand Runde 8)
- Nach Runde 8 KEINE weitere Discovery-Runde, sondern ein SYNTHESE/Regressions-Pass ueber den
KUMULATIVEN Diff (Interaktion der 6 gestagten/releasten Fixes) — Confirmed-Yield faellt (R5:0/R6:1/R7:1),
Risiko ist jetzt Fix-Interaktion, nicht unentdeckte Bugs.
- 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).
## 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 +
f1e35f5 LOW Mega-Tagesreset-Park + d2a1b83 HIGH Shelve-Loop-RetryLimit. HIGH rechtfertigt das
Release. Gitea (Live-Update-Quelle): .../releases/tag/v1.7.218. GitHub-Mirror (kuratierter
Single-Commit ohne CLAUDE.md/tasks/, Leak-Check sauber): Sucukdeluxe/multi-debrid-downloader v1.7.218.
Advisor war die GANZE Session ueberlastet → autonom released auf Basis: 3x rot-bewiesene Tests
(je per Temp-Revert verifiziert), volle Suite 882 gruen, tsc=6, unabhaengige Code-Verifikation +
Multi-Agent-adversarisch, Routing-Fix frueher advisor-gesegnet, Praezedenz 215/216/217 autonom.
Runde-5/6-Charakterisierungen (PP-SEM-1 benign, DISK-1 deferred, deferred-LOW-Cluster #10/#11/#13
benign) NICHT released — dokumentiert.
## 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.
Advisor VOR Implementierung konsultiert (HIGH-Hot-Path) — Design + Check-A-Branch (totalBytes-null) bestaetigt.
- **CONFIRMED HIGH (GEFIXT, known-total) RANGE-1 Silent-Mid-File-Corruption:** Auf dem LETZTEN inneren Versuch
wird ein injizierter Garbage-Tail nicht zurueckgespult (`attempt < maxAttempts`-Guard greift nicht),
resumeRewindBytesNextAttempt ist funktions-lokal (ueberlebt downloadToFile-Re-Entry nicht), der Outer-Handler
nimmt fuer terminated-class den generic-retry-Zweig (kein File-Delete), und der Fresh-Link-Resume haengt
echte Bytes NACH dem Garbage an → exakt-laengen-Datei besteht die Length-only-Completion-Pruefung; bei
manifestlosen .mkv/.mp4 nie erkannt. Fix: Rewind-vor-Throw am Exhaustion-Punkt (truncate letzte
RESUME_REWIND_BYTES + downloadedBytes ZUERST setzen → binary-Re-Entry-prealloc-reconcile robust auch bei
truncate-Fehler), GEGATED auf `totalBytes != null && > 0`. Check A verifiziert: known-total → rewound
size < totalBytes=minBytes tryFinalizeItemFromDisk(9238) REJECTET Re-Entry ueberschreibt Garbage.
EHRLICHER Scope (Advisor): GEFIXT fuer known-total Medien (Debrid liefert fast immer fileSize);
**null-total behaelt die separate, vor-bestehende Silent-Corruption** unter dem dokumentierten
Size-only-Validation-Blindspot (durch Rewind NICHT fixbar — kein Laengensignal; nur Hard-Reset wuerde
helfen, groesserer Eingriff). Rot-bewiesen: Cross-Call-Test (final-attempt Garbage → Exhaustion →
queueRetry → 2. downloadToFile → CONTENT-Gleichheit); ohne Fix Length-Assert gruen + Content-Assert rot.
- **CONFIRMED MED (GEFIXT) REWIND-TRUNCATE-FAIL:** Das `finally` setzte resumeRewindBytesNextAttempt=0
UNBEDINGT, auch wenn die Rewind-truncate (9633) warf → transienter win32-EBUSY/AV-Lock-Fehler liess den
Garbage-Tail + cleart das Flag (nie retried). Fix: Reset NUR im Success-Branch → fehlgeschlagenes Rewind
wird naechsten Versuch erneut probiert. Defensive Haertung (Advisor "ship it"); Happy-Path von Test 1113
+ RANGE-1-Test abgedeckt.
- **DOKUMENTIERT, nicht gefixt (MED, schwaechste, Workload-immun) PREALLOC-ZEROS-ACCEPTED:** win32-Prealloc-
Nullen am Ende als komplett akzeptiert fuer NICHT-binaere Typen (1MB-Slack) wenn truncate skip/faellt.
Dominante Medien/Archive sind immun (threshold=0). Narrow Conjunction (non-binary >20MB, Gap<1MB, Crash/
truncate-fail). Fix bekannt (binary-strict footprint im 416-accept + recovery-finalize), aber deferred.
- **Refuted (1/3) TRUNC-1:** fsync auf resume-append fehlt — als Power-Loss-Edge eingestuft, nicht confirmed.
- **Cross-cutting (Advisor: NICHT jetzt anfassen):** Size-only-Completion-Validation ist der gemeinsame
Blindspot; kein billiger Content-Check fuer manifestlose Medien → validateDownloadedFileCompletion NICHT
umbauen (Risiko 4. Bug). Punkt-Fixes sind korrekt; Blindspot bleibt langfristiges Item.
## Runde 7 (Post-Download: Extraction + Video-Processor + Companion/Orchestrierung) — Workflow wcxk08n5i
3 Finder + adversarisch verifizieren. 2 confirmed, 0 refuted. Schliesst den deferred-Extraction-Cluster ab.
- **CONFIRMED MED (GEFIXT) VP-1 Falsche Tonspur:** isGermanStream (video-processor.ts:99-108) wertet die
Titel-Regex /\b(german|deutsch)\b/ fuer JEDEN nicht-deutsch-getaggten Stream — NICHT auf "Sprach-Tag fehlt"
gegated, obwohl der Kommentar (104-106) genau das als Absicht nennt. pickAudioTrack (124, EINZIGER Caller)
nimmt den ERSTEN Treffer → eine eng-Spur mit Titel "...German..." vor der echten ger-Spur GEWINNT → Remux
behaelt Englisch, verwirft die korrekte dt. Spur, ersetzt das Original atomar in-place (500) und strippt
.DL. → irreversibler Datenverlust + falsche Sprache, als Erfolg gemeldet. Single-Trigger (nicht compound).
Betrifft genau die vom Nutzer bestaetigte dt.-Tonspur-Funktion [[project_german_audio_feature]]. Fix:
`if (lang) return false;` VOR dem Titel-Fallback (deckt sich mit der Kommentar-Absicht). Rot-bewiesen
(2 Tests: eng-Titel-"German" vs echte ger → audioRelIndex 1; eng-Titel-"Deutsch entfernt" → skip). tsc=6.
- **DOKUMENTIERT, nicht gefixt (LOW compound) PP-1/#13 Companion-Overwrite:** renameCompanionFiles/
moveCompanionFiles (dl-mgr 3735/3791) ohne Uniqueness-Guard (anders als MKV via buildUniqueFlattenTargetPath);
cross-volume copyFile ohne COPYFILE_EXCL (3696) → ueberschreibt einen vorhandenen Orphan-Companion still
(nur .srt/.idx/.nfo, nie Archiv/Video). Compound (Orphan + Namenskollision), Sekundaerdateien, Integrations-
TDD noetig → dokumentiert. Fix bekannt (Uniqueness-Guard spiegeln).
- **Deferred-Extraction-Cluster re-klassifiziert (aus Runde 2):** #10 CRC-Kleindatei-Delete BENIGN (failed
Archive aus cleanupSources ausgeschlossen, nicht geloescht); #12 7z-Exit-1-als-Erfolg BENIGN (nur Erfolg
wenn KEINE Error-Marker — echte CRC/Korruption korrekt als Fehler); #11 resume-empty-output NOT-CONFIRMED
(compound, unbewiesene Hybrid-Collect-Ordering-Annahme); #13 = PP-1.
## Release-Status (Stand Runde 5)
Round-4-Fixes (986fbab MED Routing + 03c908b No-Stranding-Test + f1e35f5 LOW Mega-Park) committet,
RELEASE BEWUSST AUFGESCHOBEN: beide MED/LOW (kein HIGH wie 216/217) → buendeln statt Thin-Release
(weniger Live-Server-Churn). Zusaetzlich Advisor (designierter Check vor Live-Deploy) ueber viele
Retries ueberlastet → kein Sign-off fuer den Mega-Park-Hot-Path-Klassifikations-Change. Fixes sind
durable. Plan: Runde 6 dazu buendeln, Advisor-Sign-off abwarten, dann v1.7.218 als Roll-up.
## Runde 6 (Retry/Backoff/Error-Klassifikations-State-Machine + Disk/IO) — Workflow wqurq83ma
3 Finder + adversarisch verifizieren. 1 confirmed (2/3), 3 refuted. Schliesst den deferred-LOW-Cluster ab.

View File

@ -63,30 +63,6 @@ ODER zwei unabhängige Pro-Modus-Schalter (mehr Kontrolle, aber UI + Migration n
## Erledigt in dieser Runde (zur Info, kein Handlungsbedarf)
- **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
Neuversuch (mit frischem Link) hängte die echten Bytes DAHINTER an — die Datei hatte am Ende exakt die
richtige Größe und galt deshalb als fertig, obwohl mittendrin Müll steckte. Bei .mkv/.mp4 ohne Prüfsumme
fiel das nie auf. Jetzt wird der verdächtige Datei-Schwanz vor der Linkerneuerung zurückgespult, sodass der
Neuversuch ihn sauber überschreibt. Rot-bewiesener Test (Inhalt byte-genau geprüft, nicht nur die Länge).
Ehrlich eingeordnet: Das ist behoben für Dateien, bei denen der Anbieter die Größe meldet (fast immer der
Fall). Für die seltenen Fälle ganz ohne Größenangabe bleibt eine separate, schon vorher bestehende Lücke
(ohne Längensignal nicht über dieses Rückspulen lösbar) — dokumentiert als langfristiges Thema.
- Zusätzlich gehärtet: Ein fehlgeschlagenes Zurückspulen (z.B. Datei kurz von Virenscanner gesperrt) wird
jetzt im nächsten Versuch erneut probiert statt still übergangen.
- **Deutsche Tonspur: falsche Spur-Auswahl behoben (Datenverlust-Schutz):** Die Erkennung der
deutschen Tonspur hat den Titel-Text einer Spur („...German...") auch dann ausgewertet, wenn die
Spur bereits ein anderssprachiges Tag hatte (z.B. eine englische Spur mit „German" im Titel, wie
„German Commentary"). Lag so eine Spur VOR der korrekt mit „ger" getaggten Spur, wurde die falsche
(englische) behalten und die echte deutsche Spur beim Remux unwiderruflich verworfen — das Ergebnis
wurde als Erfolg gemeldet. Jetzt wird der Titel nur noch dann herangezogen, wenn gar kein Sprach-Tag
vorhanden ist (so war es ohnehin gemeint). Korrekt getaggte deutsche Spuren gewinnen jetzt immer.
Rot-bewiesener Test. (Noch nicht released — wird mit der nächsten Runde gebündelt.)
- **Endlos-Wiederholung bei festem Wiederholungslimit behoben:** Wenn du ein FESTES Retry-Limit
(z.B. 5) eingestellt hattest UND ein Link sprunghaft verschiedene Fehlerarten produzierte
(mal Umwandlungs-Timeout, mal Abbruch mitten im Download, mal allgemeiner Fehler), konnte ein

View File

@ -1218,110 +1218,6 @@ describe("download manager", () => {
}
});
it("does not embed a final-attempt garbage tail across a fresh-link resume (known-total content integrity)", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-range1-"));
tempDirs.push(root);
const binary = Buffer.alloc(1024 * 1024, 0);
for (let i = 0; i < binary.length; i += 1) {
binary[i] = (i * 31 + 7) & 0xff;
}
const firstChunkBytes = 128 * 1024;
const injectedErrorChunk = Buffer.from("{\"error\":\"Missed session after 2000 ms\",\"success\":false}", "utf8");
let unrestrictCalls = 0;
const serveRange = (req: http.IncomingMessage): { start: number } => {
const range = String(req.headers.range || "");
const m = range.match(/bytes=(\d+)-/i);
return { start: m ? Number(m[1]) : 0 };
};
const server = http.createServer((req, res) => {
const route = req.url || "";
const { start } = serveRange(req);
if (route.startsWith("/corrupt")) {
if (start > 0) {
res.statusCode = 206;
res.setHeader("Content-Range", `bytes ${start}-${binary.length - 1}/${binary.length}`);
} else {
res.statusCode = 200;
}
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Content-Length", String(binary.length - start));
res.write(binary.subarray(start, Math.min(binary.length, start + firstChunkBytes)));
res.write(injectedErrorChunk);
setTimeout(() => {
try { res.socket?.destroy(); } catch { }
}, 60);
return;
}
if (route.startsWith("/clean")) {
const chunk = binary.subarray(start);
if (start > 0) {
res.statusCode = 206;
res.setHeader("Content-Range", `bytes ${start}-${binary.length - 1}/${binary.length}`);
} else {
res.statusCode = 200;
}
res.setHeader("Accept-Ranges", "bytes");
res.setHeader("Content-Length", String(chunk.length));
res.end(chunk);
return;
}
res.statusCode = 404;
res.end("not-found");
});
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("server address unavailable");
}
const base = `http://127.0.0.1:${address.port}`;
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
if (url.includes("/unrestrict/link")) {
unrestrictCalls += 1;
const target = unrestrictCalls === 1 ? `${base}/corrupt` : `${base}/clean`;
return new Response(
JSON.stringify({ download: target, filename: "range1.mkv", filesize: binary.length }),
{ status: 200, headers: { "Content-Type": "application/json" } }
);
}
return originalFetch(input, init);
};
try {
const manager = new DownloadManager(
{
...defaultSettings(),
token: "rd-token",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: false,
autoReconnect: false
},
emptySession(),
createStoragePaths(path.join(root, "state"))
);
manager.addPackages([{ name: "range1", links: ["https://dummy/range1"] }]);
await manager.start();
await waitFor(() => !manager.getSnapshot().session.running, 30000);
const item = Object.values(manager.getSnapshot().session.items)[0];
expect(item?.status).toBe("completed");
expect(unrestrictCalls).toBeGreaterThanOrEqual(2);
const finalBytes = fs.readFileSync(item.targetPath);
expect(finalBytes.length).toBe(binary.length);
expect(finalBytes.equals(binary)).toBe(true);
} finally {
server.close();
await once(server, "close");
}
});
it("requests a fresh direct link after repeated same-link download failures", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);

View File

@ -92,17 +92,6 @@ describe("pickAudioTrack", () => {
expect(d).toMatchObject({ action: "remux", audioRelIndex: 0, reason: "fallback-first-untagged" });
});
it("tag mode does NOT let an eng track titled 'German' beat a correctly-tagged German track", () => {
const engTitledGerman = { language: "eng", title: "German Commentary" };
const d = pickAudioTrack([engTitledGerman, ger], "tag");
expect(d).toMatchObject({ action: "remux", audioRelIndex: 1, reason: "german-tag" });
});
it("tag mode does NOT pick a non-German-tagged track just because its title contains 'Deutsch'", () => {
const d = pickAudioTrack([{ language: "eng", title: "Deutsch entfernt" }, { language: "fre", title: "" }], "tag");
expect(d).toMatchObject({ action: "skip", reason: "no-german-track" });
});
it("tag mode with single German -> single (no remux)", () => {
expect(pickAudioTrack([ger], "tag")).toMatchObject({ action: "single" });
});