v1.7.154 collectMkvFilesToLibrary scannt jetzt extractDir UND outputDir

Bug: Direct .mkv Downloads (z.B. von Mega-Debrid bei mega.nz, die
KEIN Archiv liefern) blieben mit autoExtract=true im outputDir liegen
und kamen nie in die MKV-Library. collectMkvFilesToLibrary scannte
binary nur extractDir wenn autoExtract aktiv war.

Fix: Beide Source-Dirs scannen, dedupe by basename (extractDir wins),
Safety-Check + Existenz-Check pro Dir. Cleanup-Loop läuft auch pro Dir.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-05-23 00:43:43 +02:00
parent 8ec5d17e09
commit 7ba8dd07b9
2 changed files with 157 additions and 26 deletions

View File

@ -4600,27 +4600,57 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
const sourceDir = this.settings.autoExtract ? pkg.extractDir : pkg.outputDir; // SOURCE DIRECTORIES:
// - Wenn autoExtract aktiv: extractDir ist primäre Quelle (entpackte Videos).
// - IMMER zusätzlich outputDir: Provider wie Mega-Debrid liefern direkte
// .mkv (kein Archiv), die sonst im outputDir liegen bleiben und nie in
// der Library landen würden.
const sourceDirCandidates: string[] = [];
if (this.settings.autoExtract && pkg.extractDir) {
sourceDirCandidates.push(pkg.extractDir);
}
if (pkg.outputDir) {
sourceDirCandidates.push(pkg.outputDir);
}
// Dedupe nach resolved Pfad (extractDir kann == outputDir sein).
const sourceDirSeen = new Set<string>();
const sourceDirsAll: string[] = [];
for (const dir of sourceDirCandidates) {
const resolved = path.resolve(dir).toLowerCase();
if (sourceDirSeen.has(resolved)) continue;
sourceDirSeen.add(resolved);
sourceDirsAll.push(dir);
}
const targetDirRaw = String(this.settings.mkvLibraryDir || "").trim(); const targetDirRaw = String(this.settings.mkvLibraryDir || "").trim();
if (!sourceDir || !targetDirRaw) { if (sourceDirsAll.length === 0 || !targetDirRaw) {
logger.warn(`MKV-Sammelordner übersprungen: pkg=${pkg.name}, ungültiger Pfad`); logger.warn(`MKV-Sammelordner übersprungen: pkg=${pkg.name}, ungültiger Pfad`);
return; return;
} }
const targetDir = path.resolve(targetDirRaw); const targetDir = path.resolve(targetDirRaw);
// SAFETY: never move files WITHIN the library tree, and never treat the // SAFETY: never move files WITHIN the library tree, and never treat the
// library itself as a source. sourceDir == targetDir would scan the // library itself as a source. sourceDir == targetDir would scan the
// library, match files collected from OTHER packages via the same rename // library, match files collected from OTHER packages via the same rename
// heuristics, and move them around — a cross-package corruption vector. // heuristics, and move them around — a cross-package corruption vector.
if (isPathInsideDir(sourceDir, targetDir) || isPathInsideDir(targetDir, sourceDir)) { // Pro Source-Dir prüfen — einer kann safe sein, der andere nicht.
logger.warn(`MKV-Sammelordner ABGEBROCHEN: pkg=${pkg.name}, sourceDir=${sourceDir} ueberlappt mit mkvLibraryDir=${targetDir}`); const sourceDirs: string[] = [];
this.logPackageForPackage(pkg, "ERROR", "MKV-Sammelordner abgebrochen: sourceDir ueberlappt mit MKV-Bibliothek", { for (const dir of sourceDirsAll) {
sourceDir, if (isPathInsideDir(dir, targetDir) || isPathInsideDir(targetDir, dir)) {
targetDir logger.warn(`MKV-Sammelordner: Source uebersprungen (ueberlappt mit mkvLibraryDir): pkg=${pkg.name}, dir=${dir}, target=${targetDir}`);
}); this.logPackageForPackage(pkg, "WARN", "MKV-Sammelordner: Source uebersprungen (ueberlappt mit MKV-Bibliothek)", {
return; sourceDir: dir,
targetDir
});
continue;
}
if (!await this.existsAsync(dir)) {
continue;
}
sourceDirs.push(dir);
} }
if (!await this.existsAsync(sourceDir)) { if (sourceDirs.length === 0) {
logger.info(`MKV-Sammelordner: pkg=${pkg.name}, Quelle fehlt (${sourceDir})`); logger.info(`MKV-Sammelordner: pkg=${pkg.name}, keine nutzbare Quelle (alle Source-Dirs fehlen oder ueberlappen mit Library)`);
return; return;
} }
@ -4631,8 +4661,21 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
const allMkvFiles = await this.collectFilesByExtensions(sourceDir, SAMPLE_VIDEO_EXTENSIONS); // Sammle aus ALLEN safe source dirs. Dedupe nach basename (lowercase) —
if (allMkvFiles.length === 0) { // extractDir wird zuerst gescannt und gewinnt bei Kollision (entpackte
// Datei hat Vorrang vor evtl. noch liegengebliebenem Quell-File).
const seenBasenames = new Set<string>();
const collected: { filePath: string; sourceRoot: string }[] = [];
for (const dir of sourceDirs) {
const filesInDir = await this.collectFilesByExtensions(dir, SAMPLE_VIDEO_EXTENSIONS);
for (const filePath of filesInDir) {
const baseLower = path.basename(filePath).toLowerCase();
if (seenBasenames.has(baseLower)) continue;
seenBasenames.add(baseLower);
collected.push({ filePath, sourceRoot: dir });
}
}
if (collected.length === 0) {
logger.info(`MKV-Sammelordner: pkg=${pkg.name}, keine MKV gefunden`); logger.info(`MKV-Sammelordner: pkg=${pkg.name}, keine MKV gefunden`);
return; return;
} }
@ -4646,7 +4689,7 @@ export class DownloadManager extends EventEmitter {
const mkvFiles: string[] = []; const mkvFiles: string[] = [];
let sampleSkipped = 0; let sampleSkipped = 0;
let bonusSkipped = 0; let bonusSkipped = 0;
for (const filePath of allMkvFiles) { for (const { filePath, sourceRoot } of collected) {
if (shouldAbort?.()) { if (shouldAbort?.()) {
return; return;
} }
@ -4656,9 +4699,9 @@ export class DownloadManager extends EventEmitter {
sampleSkipped += 1; sampleSkipped += 1;
continue; continue;
} }
if (isInsideBonusDir(filePath, sourceDir) || BONUS_FILENAME_RE.test(stem)) { if (isInsideBonusDir(filePath, sourceRoot) || BONUS_FILENAME_RE.test(stem)) {
bonusSkipped += 1; bonusSkipped += 1;
logger.info(`MKV-Sammelordner: Bonus-Datei uebersprungen: ${path.basename(filePath)} (Pfad: ${path.relative(sourceDir, filePath)})`); logger.info(`MKV-Sammelordner: Bonus-Datei uebersprungen: ${path.basename(filePath)} (Pfad: ${path.relative(sourceRoot, filePath)})`);
continue; continue;
} }
mkvFiles.push(filePath); mkvFiles.push(filePath);
@ -4675,7 +4718,7 @@ export class DownloadManager extends EventEmitter {
} }
this.logRenameProcess(pkg, "INFO", "mkv-move", "MKV-Sammelordner Scan gestartet", { this.logRenameProcess(pkg, "INFO", "mkv-move", "MKV-Sammelordner Scan gestartet", {
sourceDir, sourceDirs: sourceDirs.join(" | "),
targetDir, targetDir,
mkvFiles: mkvFiles.length mkvFiles: mkvFiles.length
}); });
@ -4787,20 +4830,25 @@ export class DownloadManager extends EventEmitter {
} }
} }
if ((sourceArtifactsChanged || sourceCleanupRelevant) && await this.existsAsync(sourceDir)) { if (sourceArtifactsChanged || sourceCleanupRelevant) {
const removedResidual = await this.cleanupNonMkvResidualFiles(sourceDir, targetDir); // Cleanup pro Source-Dir — beide können Restdateien hinterlassen haben
if (removedResidual > 0) { // (Mega-Direct .mkv weg aus outputDir, oder extracted .mkv weg aus extractDir).
logger.info(`MKV-Sammelordner entfernte Restdateien: pkg=${pkg.name}, entfernt=${removedResidual}`); for (const dir of sourceDirs) {
} if (!await this.existsAsync(dir)) continue;
const removedDirs = await this.removeEmptyDirectoryTree(sourceDir); const removedResidual = await this.cleanupNonMkvResidualFiles(dir, targetDir);
if (removedDirs > 0) { if (removedResidual > 0) {
logger.info(`MKV-Sammelordner entfernte leere Ordner: pkg=${pkg.name}, entfernt=${removedDirs}`); 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}`);
}
} }
} }
logger.info(`MKV-Sammelordner: pkg=${pkg.name}, packageId=${packageId}, moved=${moved}, skipped=${skipped}, failed=${failed}, target=${targetDir}`); logger.info(`MKV-Sammelordner: pkg=${pkg.name}, packageId=${packageId}, moved=${moved}, skipped=${skipped}, failed=${failed}, target=${targetDir}`);
this.logRenameProcess(pkg, "INFO", "mkv-move", "MKV-Sammelordner abgeschlossen", { this.logRenameProcess(pkg, "INFO", "mkv-move", "MKV-Sammelordner abgeschlossen", {
sourceDir, sourceDirs: sourceDirs.join(" | "),
targetDir, targetDir,
moved, moved,
skipped, skipped,

View File

@ -9358,6 +9358,89 @@ describe("download manager", () => {
expect(fs.existsSync(originalExtractedPath)).toBe(false); expect(fs.existsSync(originalExtractedPath)).toBe(false);
}, 20000); }, 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);
const packageName = "Mega-Direct-Pack";
const outputDir = path.join(root, "downloads", packageName);
const extractDir = path.join(root, "extract", packageName);
fs.mkdirSync(outputDir, { recursive: true });
// Direct .mkv download (no archive) — wie es Mega-Debrid bei mega.nz liefert.
const directMkvName = "Direct.Show.S01E01.German.1080p.WEB.x264-DIRECT.mkv";
const directMkvPath = path.join(outputDir, directMkvName);
fs.writeFileSync(directMkvPath, Buffer.alloc(2048, 1));
const directMkvSize = fs.statSync(directMkvPath).size;
const session = emptySession();
const packageId = `${packageName}-pkg`;
const itemId = `${packageName}-item`;
const createdAt = Date.now() - 20_000;
session.packageOrder = [packageId];
session.packages[packageId] = {
id: packageId,
name: packageName,
outputDir,
extractDir,
status: "completed",
itemIds: [itemId],
cancelled: false,
enabled: true,
createdAt,
updatedAt: createdAt
};
session.items[itemId] = {
id: itemId,
packageId,
url: `https://mega.nz/file/${packageName}`,
provider: "megadebrid-api",
status: "completed",
retries: 0,
speedBps: 0,
downloadedBytes: directMkvSize,
totalBytes: directMkvSize,
progressPercent: 100,
fileName: directMkvName,
targetPath: directMkvPath,
resumable: true,
attempts: 1,
lastError: "",
fullStatus: "Fertig",
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: "none"
},
session,
createStoragePaths(path.join(root, "state"))
);
const libraryPath = path.join(mkvLibraryDir, directMkvName);
await waitFor(() => fs.existsSync(libraryPath), 12000);
expect(fs.existsSync(libraryPath)).toBe(true);
// Filename darf NICHT umbenannt werden (Mega-Files sind oft schon korrekt benannt).
expect(fs.readFileSync(libraryPath).length).toBe(directMkvSize);
// Quelle ist weg (verschoben).
expect(fs.existsSync(directMkvPath)).toBe(false);
void manager;
}, 20000);
it("does NOT move bonus files from Extras subdirectory to flat library", async () => { it("does NOT move bonus files from Extras subdirectory to flat library", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root); tempDirs.push(root);