Add optional flat MKV library collection per package
This commit is contained in:
@@ -50,6 +50,8 @@ export function defaultSettings(): AppSettings {
|
||||
autoExtract: true,
|
||||
autoRename4sf4sj: false,
|
||||
extractDir: path.join(baseDir, "_entpackt"),
|
||||
collectMkvToLibrary: false,
|
||||
mkvLibraryDir: path.join(baseDir, "_mkv"),
|
||||
createExtractSubfolder: true,
|
||||
hybridExtract: true,
|
||||
cleanupMode: "none",
|
||||
|
||||
@@ -1457,8 +1457,19 @@ export class DownloadManager extends EventEmitter {
|
||||
return removed;
|
||||
}
|
||||
|
||||
private collectVideoFiles(rootDir: string): string[] {
|
||||
if (!rootDir || !fs.existsSync(rootDir)) {
|
||||
private collectFilesByExtensions(rootDir: string, extensions: Set<string>): string[] {
|
||||
if (!rootDir || !fs.existsSync(rootDir) || extensions.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalizedExtensions = new Set<string>();
|
||||
for (const extension of extensions) {
|
||||
const normalized = String(extension || "").trim().toLowerCase();
|
||||
if (normalized) {
|
||||
normalizedExtensions.add(normalized);
|
||||
}
|
||||
}
|
||||
if (normalizedExtensions.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1483,7 +1494,7 @@ export class DownloadManager extends EventEmitter {
|
||||
continue;
|
||||
}
|
||||
const extension = path.extname(entry.name).toLowerCase();
|
||||
if (!SAMPLE_VIDEO_EXTENSIONS.has(extension)) {
|
||||
if (!normalizedExtensions.has(extension)) {
|
||||
continue;
|
||||
}
|
||||
files.push(fullPath);
|
||||
@@ -1493,6 +1504,10 @@ export class DownloadManager extends EventEmitter {
|
||||
return files;
|
||||
}
|
||||
|
||||
private collectVideoFiles(rootDir: string): string[] {
|
||||
return this.collectFilesByExtensions(rootDir, SAMPLE_VIDEO_EXTENSIONS);
|
||||
}
|
||||
|
||||
private buildSafeAutoRenameTargetPath(sourcePath: string, targetBaseName: string, sourceExt: string): string | null {
|
||||
const dirPath = path.dirname(sourcePath);
|
||||
const safeBaseName = sanitizeFilename(String(targetBaseName || "").trim());
|
||||
@@ -1662,6 +1677,112 @@ export class DownloadManager extends EventEmitter {
|
||||
return renamed;
|
||||
}
|
||||
|
||||
private moveFileWithExdevFallback(sourcePath: string, targetPath: string): void {
|
||||
try {
|
||||
fs.renameSync(sourcePath, targetPath);
|
||||
return;
|
||||
} catch (error) {
|
||||
const code = error && typeof error === "object" && "code" in error
|
||||
? String((error as NodeJS.ErrnoException).code || "")
|
||||
: "";
|
||||
if (code !== "EXDEV") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
fs.rmSync(sourcePath, { force: true });
|
||||
}
|
||||
|
||||
private buildUniqueFlattenTargetPath(targetDir: string, sourcePath: string, reserved: Set<string>): string {
|
||||
const parsed = path.parse(path.basename(sourcePath));
|
||||
const extension = parsed.ext || ".mkv";
|
||||
const baseName = sanitizeFilename(parsed.name || "video");
|
||||
|
||||
let index = 1;
|
||||
while (true) {
|
||||
const candidateName = index <= 1
|
||||
? `${baseName}${extension}`
|
||||
: `${baseName} (${index})${extension}`;
|
||||
const candidatePath = path.join(targetDir, candidateName);
|
||||
const candidateKey = pathKey(candidatePath);
|
||||
if (reserved.has(candidateKey)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (!fs.existsSync(candidatePath)) {
|
||||
reserved.add(candidateKey);
|
||||
return candidatePath;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private collectMkvFilesToLibrary(packageId: string, pkg: PackageEntry): void {
|
||||
if (!this.settings.collectMkvToLibrary) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceDir = this.settings.autoExtract ? pkg.extractDir : pkg.outputDir;
|
||||
const targetDirRaw = String(this.settings.mkvLibraryDir || "").trim();
|
||||
if (!sourceDir || !targetDirRaw) {
|
||||
logger.warn(`MKV-Sammelordner übersprungen: pkg=${pkg.name}, ungültiger Pfad`);
|
||||
return;
|
||||
}
|
||||
const targetDir = path.resolve(targetDirRaw);
|
||||
if (!fs.existsSync(sourceDir)) {
|
||||
logger.info(`MKV-Sammelordner: pkg=${pkg.name}, Quelle fehlt (${sourceDir})`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
} catch (error) {
|
||||
logger.warn(`MKV-Sammelordner konnte nicht erstellt werden: pkg=${pkg.name}, dir=${targetDir}, reason=${compactErrorText(error)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mkvFiles = this.collectFilesByExtensions(sourceDir, new Set([".mkv"]));
|
||||
if (mkvFiles.length === 0) {
|
||||
logger.info(`MKV-Sammelordner: pkg=${pkg.name}, keine MKV gefunden`);
|
||||
return;
|
||||
}
|
||||
|
||||
const reservedTargets = new Set<string>();
|
||||
let moved = 0;
|
||||
let skipped = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const sourcePath of mkvFiles) {
|
||||
if (isPathInsideDir(sourcePath, targetDir)) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
const targetPath = this.buildUniqueFlattenTargetPath(targetDir, sourcePath, reservedTargets);
|
||||
if (pathKey(sourcePath) === pathKey(targetPath)) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
this.moveFileWithExdevFallback(sourcePath, targetPath);
|
||||
moved += 1;
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
logger.warn(`MKV verschieben fehlgeschlagen: ${sourcePath} -> ${targetPath} (${compactErrorText(error)})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (moved > 0 && fs.existsSync(sourceDir)) {
|
||||
const removedDirs = this.removeEmptyDirectoryTree(sourceDir);
|
||||
if (removedDirs > 0) {
|
||||
logger.info(`MKV-Sammelordner entfernte leere Ordner: pkg=${pkg.name}, entfernt=${removedDirs}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`MKV-Sammelordner: pkg=${pkg.name}, packageId=${packageId}, moved=${moved}, skipped=${skipped}, failed=${failed}, target=${targetDir}`);
|
||||
}
|
||||
|
||||
public cancelPackage(packageId: string): void {
|
||||
const pkg = this.session.packages[packageId];
|
||||
if (!pkg) {
|
||||
@@ -4152,6 +4273,9 @@ export class DownloadManager extends EventEmitter {
|
||||
} else {
|
||||
pkg.status = "completed";
|
||||
}
|
||||
if (pkg.status === "completed") {
|
||||
this.collectMkvFilesToLibrary(packageId, pkg);
|
||||
}
|
||||
if (this.runPackageIds.has(packageId)) {
|
||||
if (pkg.status === "completed") {
|
||||
this.runCompletedPackages.add(packageId);
|
||||
|
||||
@@ -84,6 +84,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
autoExtract: Boolean(settings.autoExtract),
|
||||
autoRename4sf4sj: Boolean(settings.autoRename4sf4sj),
|
||||
extractDir: normalizeAbsoluteDir(settings.extractDir, defaults.extractDir),
|
||||
collectMkvToLibrary: Boolean(settings.collectMkvToLibrary),
|
||||
mkvLibraryDir: normalizeAbsoluteDir(settings.mkvLibraryDir, defaults.mkvLibraryDir),
|
||||
createExtractSubfolder: Boolean(settings.createExtractSubfolder),
|
||||
hybridExtract: Boolean(settings.hybridExtract),
|
||||
cleanupMode: settings.cleanupMode,
|
||||
|
||||
@@ -48,6 +48,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
||||
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
||||
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||
autoExtract: true, autoRename4sf4sj: false, extractDir: "", createExtractSubfolder: true, hybridExtract: true,
|
||||
collectMkvToLibrary: false, mkvLibraryDir: "",
|
||||
cleanupMode: "none", extractConflictMode: "overwrite", removeLinkFilesAfterExtract: false,
|
||||
removeSamplesAfterExtract: false, enableIntegrityCheck: true, autoResumeOnStart: true,
|
||||
autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never",
|
||||
@@ -1412,6 +1413,23 @@ export function App(): ReactElement {
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.autoRename4sf4sj} onChange={(e) => setBool("autoRename4sf4sj", e.target.checked)} /> Auto-Rename (4SF/4SJ)</label>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.createExtractSubfolder} onChange={(e) => setBool("createExtractSubfolder", e.target.checked)} /> Entpackte Dateien in Paket-Unterordner speichern</label>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.hybridExtract} onChange={(e) => setBool("hybridExtract", e.target.checked)} /> Hybrid-Extract</label>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.collectMkvToLibrary} onChange={(e) => setBool("collectMkvToLibrary", e.target.checked)} /> MKV nach Paketabschluss in Sammelordner verschieben (flach)</label>
|
||||
<label>MKV-Sammelordner</label>
|
||||
<div className="input-row">
|
||||
<input value={settingsDraft.mkvLibraryDir} onChange={(e) => setText("mkvLibraryDir", e.target.value)} disabled={!settingsDraft.collectMkvToLibrary} />
|
||||
<button
|
||||
className="btn"
|
||||
disabled={!settingsDraft.collectMkvToLibrary}
|
||||
onClick={() => {
|
||||
void performQuickAction(async () => {
|
||||
const s = await window.rd.pickFolder();
|
||||
if (s) { setText("mkvLibraryDir", s); }
|
||||
});
|
||||
}}
|
||||
>
|
||||
Wählen
|
||||
</button>
|
||||
</div>
|
||||
<label>Passwortliste (eine Zeile pro Passwort)</label>
|
||||
<textarea
|
||||
className="password-list"
|
||||
|
||||
@@ -50,6 +50,8 @@ export interface AppSettings {
|
||||
autoExtract: boolean;
|
||||
autoRename4sf4sj: boolean;
|
||||
extractDir: string;
|
||||
collectMkvToLibrary: boolean;
|
||||
mkvLibraryDir: string;
|
||||
createExtractSubfolder: boolean;
|
||||
hybridExtract: boolean;
|
||||
cleanupMode: CleanupMode;
|
||||
|
||||
Reference in New Issue
Block a user