Import: allow selecting specific audio files

Add support for selecting individual audio files during manual and bulk imports and pass that selection through the scan, API, job queue, processor and organizer.

Key changes:
- API: scan now returns audioFiles for each discovered book and emits a new 'grouping' progress phase; execute and manual-import routes accept file lists (audioFiles / selectedFiles) and validate them.
- Scanner: group loose audio files by metadata (title/author/narrator), deduplicate multi-part sets (CD1/CD2) across folders, and return audioFiles + groupingKey; add concurrency limit for ffprobe reads and merge groups post-scan.
- Job queue & processor: OrganizeFiles payload now includes selectedFiles; processors forward selectedFiles to the FileOrganizer and to cleanup logic.
- File organizer & cleanup: filter to only selectedFiles when organizing; cleanup now deletes only the selected files (if provided) instead of removing the whole directory.
- UI: Manual import browser and bulk import wizard updated to show per-file selection, track checkedFiles, toggle all, and send selected files to the API; ConfirmPhase updated to allow checking/unchecking files and prevents starting import with no files selected.
- Filesystem browse: removed expensive per-subfolder stats to keep browsing responsive (now lists subdirectories without nested stat calls).

Overall this change enables finer-grained imports, reduces accidental deletion of unselected files, and improves scan grouping for multi-folder audiobooks.
This commit is contained in:
kikootwo
2026-03-20 13:32:49 -04:00
parent 850e777a81
commit 8a757f5b67
15 changed files with 597 additions and 244 deletions
+4 -1
View File
@@ -76,6 +76,7 @@ export interface OrganizeFilesPayload extends JobPayload {
downloadPath: string;
targetPath?: string; // Optional - not used by processor (reads from database config)
cleanupSource?: boolean; // If true, delete source files after successful import
selectedFiles?: string[]; // If set, only import these specific files from downloadPath
}
export interface ScanPlexPayload extends JobPayload {
@@ -644,7 +645,8 @@ export class JobQueueService {
audiobookId: string,
downloadPath: string,
targetPath?: string,
cleanupSource?: boolean
cleanupSource?: boolean,
selectedFiles?: string[]
): Promise<string> {
return await this.addJob(
'organize_files',
@@ -654,6 +656,7 @@ export class JobQueueService {
downloadPath,
targetPath, // Not used by processor
cleanupSource,
selectedFiles,
} as OrganizeFilesPayload,
{
priority: 8,