Fix: Deferred-Final-Pass benennt frische Dateien vor dem Collect um

Folge-Fund zu 18eada9 (Opus-Verifikation des deferFreshFiles-Konzepts):
18eada9 schloss den "frische Datei landet mit Original-Scene-Namen in der
Library"-Bug nur fuer den Hybrid-Pfad (deferFreshFiles=true + Mehrfach-Paesse).
Der finale Deferred-Pass blieb betroffen.

Root Cause (verifiziert via failing Test gegen HEAD):
- runDeferredPostExtraction macht Rename -> Collect (deferFreshFiles=false). Ist
  eine Datei beim Deferred-Rename noch "frisch" (juenger als fileStabilizeMinAgeMs,
  prod=2000ms) -- v.a. eine eben per Nested-Extraction geschriebene Datei --
  ueberspringt der Frische-Gate sie, und der Collect moved sie mit Original-
  Scene-Namen in die Library. collectMkvFilesToLibrary benennt selbst nicht um
  (buildUniqueFlattenTargetPath, nur Flatten).
- Im Deferred-FINAL-Pass gibt es keinen concurrent Extractor-Write mehr
  (Extraktion inkl. Nested ist awaited) -- der Frische-Gate ist dort ein False
  Positive. Pre-existierender Gap (Frische-Skip aelter als 18eada9), auch
  v1.7.162 betroffen.

Fix (minimal): treatFilesAsStable-Param durch autoRenameExtractedVideoFiles(Impl).
Der Deferred-Final-Pass ruft mit treatFilesAsStable=true -> Frische-Gate umgangen
-> alle Dateien werden umbenannt, bevor der Collect sie sammelt. Hybrid-Pfad
unangetastet (nutzt ...Impl mit Default false -> Frische-Skip bleibt aktiv).

Regressionstest: frische Datei im Deferred-Pass landet UMBENANNT in der Library.
623 Tests gruen, tsc unveraendert (9 pre-existing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-05-28 22:16:01 +02:00
co-authored by Claude Opus 4.8
parent 30dbbbae9e
commit 35622445da
3 changed files with 122 additions and 6 deletions
+81
View File
@@ -9429,6 +9429,87 @@ describe("download manager", () => {
void manager;
}, 20000);
it("deferred final pass renames fresh files before collecting them (no scene names in library)", async () => {
// Folge-Fund zu 18eada9 (verifiziert via Advisor-Gate): 18eada9 schloss den
// "frische Datei landet unbenannt"-Bug nur fuer den HYBRID-Pfad (deferFreshFiles=true
// + Mehrfach-Pässe). Der finale Deferred-Pass (runDeferredPostExtraction) macht
// Rename (treatFilesAsStable? nein) -> Collect (deferFreshFiles=false). Ist eine
// Datei beim Deferred-Rename noch "frisch" (< fileStabilizeMinAgeMs) — z.B. eine
// gerade per Nested-Extraction (12045) geschriebene Datei — ueberspringt der
// Frische-Gate sie, und der Collect moved sie mit Original-Scene-Namen in die
// Library. Im Deferred-FINAL-Pass laeuft aber KEIN concurrent Extractor mehr
// (Extraktion abgeschlossen/awaited), der Frische-Gate ist dort ein False
// Positive. Fix: der Final-Pass-Rename behandelt alle Dateien als stabil
// (treatFilesAsStable=true) → benennt um, bevor der Collect sie sammelt.
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
const packageName = "Test.Show.S02.GERMAN.WS.720p.HDTV.x264-aWake";
const outputDir = path.join(root, "downloads", packageName);
const extractDir = path.join(root, "extract", packageName);
// Episoden-Ordner liefert den kanonischen Zielnamen (enthaelt SxxExx).
const epFolder = path.join(extractDir, "Test.Show.S02E05.Title.GERMAN.WS.720p.HDTV.x264-aWake");
fs.mkdirSync(epFolder, { recursive: true });
const sceneName = "awa-testshow02e05hd.mkv";
const scenePath = path.join(epFolder, sceneName);
fs.writeFileSync(scenePath, Buffer.alloc(4096, 5)); // mtime = jetzt → "frisch"
const session = emptySession();
const packageId = `${packageName}-pkg`;
const createdAt = Date.now() - 20_000;
session.packageOrder = [packageId];
session.packages[packageId] = {
id: packageId,
name: packageName,
outputDir,
extractDir,
status: "completed",
itemIds: [],
cancelled: false,
enabled: true,
createdAt,
updatedAt: createdAt
};
const mkvLibraryDir = path.join(root, "mkv-library");
const manager = new DownloadManager(
{
...defaultSettings(),
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: true,
autoRename4sf4sj: true,
collectMkvToLibrary: true,
mkvLibraryDir,
enableIntegrityCheck: false,
cleanupMode: "none"
},
session,
createStoragePaths(path.join(root, "state"))
);
// Produktion: fileStabilizeMinAgeMs=2000. Hier 30s, damit die gerade erstellte
// Datei garantiert als "frisch" gilt — wie eine eben extrahierte Datei, die der
// Deferred-Pass sofort danach verarbeitet.
(manager as any).fileStabilizeMinAgeMs = 30_000;
const expectedBase = "Test.Show.S02E05.Title.GERMAN.WS.720p.HDTV.x264-aWake";
const renamedLibPath = path.join(mkvLibraryDir, `${expectedBase}.mkv`);
const sceneLibPath = path.join(mkvLibraryDir, sceneName);
// Deferred-FINAL-Pass-Sequenz, exakt wie runDeferredPostExtraction:
// 1) Rename — treatFilesAsStable=true (Extraktion abgeschlossen, kein Frische-Skip)
// 2) Collect — deferFreshFiles=false
await (manager as any).autoRenameExtractedVideoFiles(extractDir, session.packages[packageId], undefined, true);
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId], undefined, false);
// Die Datei landet UMBENANNT in der Library — nicht mit dem Scene-Namen.
expect(fs.existsSync(renamedLibPath)).toBe(true);
expect(fs.existsSync(sceneLibPath)).toBe(false);
void manager;
}, 20000);
it("moves direct MKV download from outputDir to library when no archive present (Mega-Debrid flow)", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);