Fix: Hybrid-Race liess .DL.+Doppeltonspur auf einzelnen Episoden zurueck

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.
This commit is contained in:
Sucukdeluxe 2026-06-10 15:53:47 +02:00
parent 2f2646c68a
commit cbb3fce7ef
2 changed files with 115 additions and 0 deletions

View File

@ -4924,6 +4924,14 @@ export class DownloadManager extends EventEmitter {
continue; 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) { if (sourceSize === 0) {
logger.warn(`MKV-Sammelordner: überspringe 0-Byte-Datei ${path.basename(sourcePath)}`); logger.warn(`MKV-Sammelordner: überspringe 0-Byte-Datei ${path.basename(sourcePath)}`);
const resolved = this.inferItemForMediaLog(pkg, sourcePath, path.basename(sourcePath), targetDir); const resolved = this.inferItemForMediaLog(pkg, sourcePath, path.basename(sourcePath), targetDir);

View File

@ -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);
});
});