From cbb3fce7ef95f1053488afde53e03b4961253ea0 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 10 Jun 2026 15:53:47 +0200 Subject: [PATCH] Fix: Hybrid-Race liess .DL.+Doppeltonspur auf einzelnen Episoden zurueck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: Bei manchen Episoden (z.B. Desperate Housewives S03E08/E11/E17) blieb der .DL.-Marker stehen UND beide Tonspuren (DE+EN) erhalten — obwohl keepGermanAudioOnly aktiv war und alle anderen Episoden des Pakets sauber verarbeitet wurden. Die Vermutung "langer Titel" war falsch; die betroffenen Episoden hatten gleich lange Namen wie die sauberen. Ursache (im Support-Bundle bewiesen): genau die 3 Episoden hatten null "Tonspur-Bereinigung OK"-Zeilen. Im Hybrid-Modus laeuft der Entpacker SEPARAT von der per-Paket-Kette (autoRename -> keepGermanAudio -> collect). Waehrend ein langsamer Remux einer anderen Episode (1,5 GB, ~3 s) lief, entpackte der Extraktor frische .DL.-MKVs in denselben extractDir — also NACH dem Datei-Scan von keepGermanAudio, aber bevor der Ketten-Abschluss- collect lief. collectMkvFilesToLibrary griff sie dann mit .DL. ab und verschob sie in die MKV-Bibliothek, bevor ihre eigene Tonspur-Bereinigung je lief. Einmal aus extractDir heraus, fand der finale Deferred- keepGermanAudio sie nicht mehr. Das 2000-ms-Frische-Gate schuetzte nicht, weil die Dateien beim collect schon ~2,3 s alt waren. Fix: collectMkvFilesToLibrary haelt im Hybrid-Lauf (deferFreshFiles=true) bei aktivem keepGermanAudioOnly jede remuxbare Datei (.mkv/.mp4) zurueck, die noch den .DL.-Marker traegt — sie hat die Tonspur-Bereinigung sicher noch nicht durchlaufen. Sie bleibt in extractDir und wird von einer spaeteren Hybrid-Runde ODER dem finalen Deferred-Pass (erst keepGermanAudio, dann collect mit deferFreshFiles=false) bereinigt + gesammelt. Praezise auf remuxbare Dateien begrenzt: eine .DL.avi ruehrt keepGermanAudio nie an, also darf sie nicht haengen bleiben. Der Deferred-Pass (deferFreshFiles=false) ist NICHT betroffen — dort darf eine legitim doppeltonige Datei (kein DE-Track) weiterhin mit .DL. gesammelt werden, statt fuer immer liegen zu bleiben. Regressionstest tests/hybrid-collect-race.test.ts (3 Faelle): Hybrid haelt remuxbare .DL.-Datei zurueck (sammelt bereinigte mkv + nicht-remuxbare .DL.avi trotzdem), Deferred sammelt sie, keepGermanAudioOnly aus haelt nichts zurueck. --- src/main/download-manager.ts | 8 +++ tests/hybrid-collect-race.test.ts | 107 ++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 tests/hybrid-collect-race.test.ts diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 7dc66ef..6c88be9 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -4924,6 +4924,14 @@ export class DownloadManager extends EventEmitter { continue; } } + if (deferFreshFiles && this.settings.keepGermanAudioOnly) { + const baseName = path.basename(sourcePath); + if (isRemuxableVideoFile(baseName) && hasDualLangMarker(baseName)) { + logger.info(`MKV-Sammelordner: ${baseName} uebersprungen — .DL. noch nicht tonspur-bereinigt (Race-Schutz), wird im finalen Durchlauf gesammelt`); + skipped += 1; + continue; + } + } if (sourceSize === 0) { logger.warn(`MKV-Sammelordner: überspringe 0-Byte-Datei ${path.basename(sourcePath)}`); const resolved = this.inferItemForMediaLog(pkg, sourcePath, path.basename(sourcePath), targetDir); diff --git a/tests/hybrid-collect-race.test.ts b/tests/hybrid-collect-race.test.ts new file mode 100644 index 0000000..d16b06d --- /dev/null +++ b/tests/hybrid-collect-race.test.ts @@ -0,0 +1,107 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import { DownloadManager } from "../src/main/download-manager"; +import { defaultSettings } from "../src/main/constants"; +import { createStoragePaths, emptySession } from "../src/main/storage"; +import { shutdownItemLogs } from "../src/main/item-log"; +import { shutdownPackageLogs } from "../src/main/package-log"; +import { shutdownRenameLog } from "../src/main/rename-log"; + +const tempDirs: string[] = []; + +afterEach(() => { + shutdownItemLogs(); + shutdownPackageLogs(); + shutdownRenameLog(); + for (const dir of tempDirs.splice(0)) { + try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } + } +}); + +const DL_MKV = "Show.S01E01.German.DL.720p.x264.mkv"; +const PLAIN_MKV = "Show.S01E02.German.720p.x264.mkv"; +const DL_AVI = "Show.S01E03.German.DL.avi"; + +function setup(keepGermanAudioOnly: boolean): { extractDir: string; libraryDir: string; manager: DownloadManager; pkg: any } { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-race-")); + tempDirs.push(root); + const extractDir = path.join(root, "extract"); + const stateDir = path.join(root, "state"); + const libraryDir = path.join(root, "library"); + fs.mkdirSync(extractDir, { recursive: true }); + fs.mkdirSync(stateDir, { recursive: true }); + const manager = new DownloadManager( + { + ...defaultSettings(), + token: "rd-token", + autoExtract: true, + collectMkvToLibrary: true, + keepGermanAudioOnly, + germanAudioMode: "tag", + autoRename4sf4sj: false, + outputDir: path.join(root, "out"), + extractDir, + mkvLibraryDir: libraryDir + }, + emptySession(), + createStoragePaths(stateDir) + ); + const pkg: any = { + id: "race-pkg-1", + name: "Show.S01.GERMAN.DL.720p", + outputDir: path.join(root, "out", "Show.S01"), + extractDir, + status: "completed", + itemIds: [], + cancelled: false, + enabled: true, + priority: "normal", + createdAt: 0, + updatedAt: 0 + }; + for (const f of [DL_MKV, PLAIN_MKV, DL_AVI]) { + fs.writeFileSync(path.join(extractDir, f), "x"); + } + return { extractDir, libraryDir, manager, pkg }; +} + +function libraryNames(libraryDir: string): string[] { + try { return fs.readdirSync(libraryDir); } catch { return []; } +} + +describe("Hybrid-Sammel Race-Schutz (.DL. noch nicht tonspur-bereinigt)", () => { + it("haelt eine remuxbare .DL.-Datei im Hybrid-Lauf zurueck (keepGermanAudioOnly an)", async () => { + const { extractDir, libraryDir, manager, pkg } = setup(true); + + await (manager as any).collectMkvFilesToLibrary(pkg.id, pkg, undefined, true); + + // Race-Opfer bleibt in extractDir, damit eine spaetere Runde / der Deferred-Pass es bereinigt + expect(fs.existsSync(path.join(extractDir, DL_MKV))).toBe(true); + expect(libraryNames(libraryDir)).not.toContain(DL_MKV); + + // Praezision: bereits bereinigte mkv (kein .DL.) wird gesammelt + expect(fs.existsSync(path.join(extractDir, PLAIN_MKV))).toBe(false); + // Praezision: .DL.avi ist nicht remuxbar -> wird NICHT zurueckgehalten, sondern gesammelt + expect(fs.existsSync(path.join(extractDir, DL_AVI))).toBe(false); + }); + + it("sammelt die remuxbare .DL.-Datei im Deferred-Lauf (deferFreshFiles=false)", async () => { + const { extractDir, libraryDir, manager, pkg } = setup(true); + + await (manager as any).collectMkvFilesToLibrary(pkg.id, pkg, undefined, false); + + expect(fs.existsSync(path.join(extractDir, DL_MKV))).toBe(false); + expect(libraryNames(libraryDir)).toContain(DL_MKV); + }); + + it("haelt nichts zurueck wenn keepGermanAudioOnly aus ist (.DL. ist dann normaler Output)", async () => { + const { extractDir, manager, pkg } = setup(false); + + await (manager as any).collectMkvFilesToLibrary(pkg.id, pkg, undefined, true); + + expect(fs.existsSync(path.join(extractDir, DL_MKV))).toBe(false); + }); +});