From ceda9817f84497837cd28cdeb3a1338ad0522546 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 23 May 2026 15:12:00 +0200 Subject: [PATCH] v1.7.156 HOTFIX: MKV-Collection loescht keine pending Archive im outputDir mehr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KRITISCHER Datenverlust-Fix (Regression aus v1.7.154): collectMkvFilesToLibrary lief seit v1.7.154 mit einem Cleanup-Loop ueber BEIDE Source-Dirs (extractDir + outputDir). cleanupNonMkvResidualFiles loescht alle Nicht-Video-Dateien — auf dem outputDir traf das auch die RAR-Archive. Bei Multi-Archive-Set-Paketen (z.B. S01 + S02 RARs im selben outputDir) wurde nach dem Extrahieren von S01 die MKV-Collection getriggert, die dann die noch nicht entpackten S02-RAR-Parts als "Restdateien" loeschte. Folge: S02 ging verloren (missing_file beim spaeteren Extract). Fix: Destruktiver Cleanup (Restdateien + leere Ordner) laeuft jetzt NUR noch auf dem cleanupDir: - autoExtract=true -> extractDir (entpackter Inhalt, fertig verarbeitet) - autoExtract=false -> outputDir (kein Extract, finaler Inhalt) Der outputDir wird bei autoExtract=true nie hier aufgeraeumt — das macht die separate Archive-Cleanup-Pipeline mit Extraktions-Guards. Das MKV-Scannen beider Dirs (v1.7.154 Mega-Direct-.mkv) bleibt erhalten, nur der Cleanup ist eingegrenzt. Regressionstest verifiziert: 2 RAR-Sets im outputDir, S01-MKVs in extractDir -> collectMkvFilesToLibrary darf S02-RARs nicht loeschen. Co-Authored-By: Claude Opus 4.7 --- src/main/download-manager.ts | 38 ++++++++++----- tests/download-manager.test.ts | 86 ++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 13 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 3169971..2fc2969 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -4654,6 +4654,21 @@ export class DownloadManager extends EventEmitter { return; } + // CLEANUP-DIR: NUR dieser Ordner darf nach dem Move destruktiv aufgeraeumt + // werden (Restdateien loeschen + leere Ordner entfernen). + // - autoExtract=true -> extractDir (entpackter Inhalt, fertig verarbeitet) + // - autoExtract=false -> outputDir (kein Extract, das ist der finale Inhalt) + // + // WICHTIG: Bei autoExtract=true wird der outputDir NICHT hier aufgeraeumt! + // Dort liegen die RAR-Archive, die von der separaten Archive-Cleanup-Pipeline + // (mit Extraktions-Guards) verwaltet werden. Ein blindes Loeschen aller + // Nicht-Video-Dateien im outputDir wuerde noch nicht entpackte Archive-Sets + // anderer Staffeln/Items zerstoeren (Regression v1.7.154, gefixt v1.7.156). + const cleanupDirCandidate = this.settings.autoExtract ? pkg.extractDir : pkg.outputDir; + const cleanupDir = (cleanupDirCandidate && sourceDirs.some( + (d) => path.resolve(d).toLowerCase() === path.resolve(cleanupDirCandidate).toLowerCase() + )) ? cleanupDirCandidate : null; + try { await fs.promises.mkdir(targetDir, { recursive: true }); } catch (error) { @@ -4830,19 +4845,16 @@ export class DownloadManager extends EventEmitter { } } - if (sourceArtifactsChanged || sourceCleanupRelevant) { - // Cleanup pro Source-Dir — beide können Restdateien hinterlassen haben - // (Mega-Direct .mkv weg aus outputDir, oder extracted .mkv weg aus extractDir). - for (const dir of sourceDirs) { - if (!await this.existsAsync(dir)) continue; - const removedResidual = await this.cleanupNonMkvResidualFiles(dir, targetDir); - if (removedResidual > 0) { - logger.info(`MKV-Sammelordner entfernte Restdateien: pkg=${pkg.name}, dir=${dir}, entfernt=${removedResidual}`); - } - const removedDirs = await this.removeEmptyDirectoryTree(dir); - if (removedDirs > 0) { - logger.info(`MKV-Sammelordner entfernte leere Ordner: pkg=${pkg.name}, dir=${dir}, entfernt=${removedDirs}`); - } + if ((sourceArtifactsChanged || sourceCleanupRelevant) && cleanupDir && await this.existsAsync(cleanupDir)) { + // NUR cleanupDir aufraeumen — niemals den outputDir bei autoExtract=true, + // sonst werden noch nicht entpackte Archive-Sets geloescht (s.o.). + const removedResidual = await this.cleanupNonMkvResidualFiles(cleanupDir, targetDir); + if (removedResidual > 0) { + logger.info(`MKV-Sammelordner entfernte Restdateien: pkg=${pkg.name}, dir=${cleanupDir}, entfernt=${removedResidual}`); + } + const removedDirs = await this.removeEmptyDirectoryTree(cleanupDir); + if (removedDirs > 0) { + logger.info(`MKV-Sammelordner entfernte leere Ordner: pkg=${pkg.name}, dir=${cleanupDir}, entfernt=${removedDirs}`); } } diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index bc8262f..0a2f236 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -9441,6 +9441,92 @@ describe("download manager", () => { void manager; }, 20000); + it("does NOT delete pending RAR archive sets in outputDir when collecting MKVs from extractDir", async () => { + // Regression v1.7.156: bei einem Multi-Archive-Set-Paket (z.B. S01 + S02 RARs + // im selben outputDir) wurde nach dem Extrahieren von S01 die MKV-Collection + // getriggert. Diese loeschte als "Restdateien" ALLE Nicht-Video-Files im + // outputDir — also auch die noch nicht entpackten S02-RAR-Parts. Folge: + // S02 ging verloren ("missing_file" beim spaeteren Extract). + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); + tempDirs.push(root); + + const packageName = "Ugly.Americans.MultiSeason-Pack"; + const outputDir = path.join(root, "downloads", packageName); + const extractDir = path.join(root, "extract", packageName); + fs.mkdirSync(outputDir, { recursive: true }); + fs.mkdirSync(extractDir, { recursive: true }); + + // outputDir: S02-RAR-Set noch NICHT entpackt (pending). Muss erhalten bleiben. + const s02Parts = [ + "Ugly.Americans.S02.COMPLETE.German.part1.rar", + "Ugly.Americans.S02.COMPLETE.German.part2.rar", + "Ugly.Americans.S02.COMPLETE.German.part3.rar" + ]; + for (const part of s02Parts) { + fs.writeFileSync(path.join(outputDir, part), Buffer.alloc(1024, 7)); + } + // Auch eine harmlose Nicht-Video-Restdatei im outputDir (z.B. .nfo). + fs.writeFileSync(path.join(outputDir, "info.nfo"), Buffer.from("nfo")); + + // extractDir: S01 wurde bereits entpackt → MKVs liegen hier. + const s01Mkvs = [ + "Ugly.Americans.S01E01.German.mkv", + "Ugly.Americans.S01E02.German.mkv" + ]; + for (const mkv of s01Mkvs) { + fs.writeFileSync(path.join(extractDir, mkv), Buffer.alloc(4096, 9)); + } + + 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: "downloading", + 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: false, + collectMkvToLibrary: true, + mkvLibraryDir, + enableIntegrityCheck: false, + cleanupMode: "delete" + }, + session, + createStoragePaths(path.join(root, "state")) + ); + + // Direkt aufrufen (umgeht die volle Download/Extract-Pipeline). + await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId]); + + // S01-MKVs sind in der Library angekommen. + for (const mkv of s01Mkvs) { + expect(fs.existsSync(path.join(mkvLibraryDir, mkv))).toBe(true); + } + // KRITISCH: S02-RAR-Parts im outputDir wurden NICHT geloescht. + for (const part of s02Parts) { + expect(fs.existsSync(path.join(outputDir, part))).toBe(true); + } + + void manager; + }, 20000); + it("does NOT move bonus files from Extras subdirectory to flat library", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); tempDirs.push(root);