diff --git a/documentation/phase3/file-organization.md b/documentation/phase3/file-organization.md index 4953138..933c774 100644 --- a/documentation/phase3/file-organization.md +++ b/documentation/phase3/file-organization.md @@ -6,23 +6,28 @@ Copies completed downloads to standardized directory structure for Plex. Automat ## Target Structure +Target directory read from database config `media_dir` (configurable in setup wizard and settings). + ``` -/media/audiobooks/ +[media_dir]/ └── Author Name/ └── Book Title (Year)/ ├── Book Title.m4b └── cover.jpg ``` +Default: `/media/audiobooks/` (if not configured) + ## Process 1. Download completes in `/downloads/[torrent-name]/` or `/downloads/[filename]` (single file) 2. Identify audiobook files (.m4b, .m4a, .mp3) - supports both directories and single files -3. Create `/media/audiobooks/[Author]/[Title]/` -4. **Copy** files (not move - originals stay for seeding) -5. **Tag metadata** (if enabled) - writes correct title, author, narrator to audio files -6. Copy cover art if found, else download from Audible -7. Originals remain until seeding requirements met +3. Read media directory from database config `media_dir` +4. Create `[media_dir]/[Author]/[Title]/` +5. **Copy** files (not move - originals stay for seeding) +6. **Tag metadata** (if enabled) - writes correct title, author, narrator to audio files +7. Copy cover art if found, else download from Audible +8. Originals remain until seeding requirements met ## Metadata Tagging @@ -110,12 +115,19 @@ async function organize( - Limit to 200 chars - Example: `Author: The ! Book?` → `Author The Best! Book` +## Configuration + +- **Media directory:** Read from database config key `media_dir` (set in setup wizard or settings) +- **Fallback:** `/media/audiobooks` if not configured +- **Temp directory:** `/tmp/readmeabook` (or `TEMP_DIR` env var) + ## Fixed Issues ✅ **1. EPERM errors** - Fixed with `fs.readFile/writeFile` instead of `copyFile` **2. Immediate deletion** - Changed to copy-only, scheduled cleanup after seeding **3. Files moved not copied** - Now copies to support seeding **4. Single file downloads** - Now supports files directly in downloads folder (not just directories) +**5. Hardcoded media path** - Now reads `media_dir` from database config instead of hardcoded `/media/audiobooks` ## Tech Stack diff --git a/src/app/api/requests/[id]/route.ts b/src/app/api/requests/[id]/route.ts index 16a8549..43467be 100644 --- a/src/app/api/requests/[id]/route.ts +++ b/src/app/api/requests/[id]/route.ts @@ -194,8 +194,7 @@ export async function PATCH( await jobQueue.addOrganizeJob( id, requestWithData.audiobook.id, - downloadPath, - `/media/audiobooks/${requestWithData.audiobook.author}/${requestWithData.audiobook.title}` + downloadPath ); updated = await prisma.request.update({ diff --git a/src/lib/processors/monitor-download.processor.ts b/src/lib/processors/monitor-download.processor.ts index 2e4d369..874c595 100644 --- a/src/lib/processors/monitor-download.processor.ts +++ b/src/lib/processors/monitor-download.processor.ts @@ -117,13 +117,12 @@ export async function processMonitorDownload(payload: MonitorDownloadPayload): P throw new Error('Request or audiobook not found'); } - // Trigger organize files job + // Trigger organize files job (target path determined by database config) const jobQueue = getJobQueueService(); await jobQueue.addOrganizeJob( requestId, request.audiobook.id, - `${downloadPath}/${torrent.name}`, - `/media/audiobooks/${request.audiobook.author}/${request.audiobook.title}` + `${downloadPath}/${torrent.name}` ); await logger?.info(`Triggered organize_files job for request ${requestId}`); diff --git a/src/lib/processors/organize-files.processor.ts b/src/lib/processors/organize-files.processor.ts index 9cc32dc..9f14f10 100644 --- a/src/lib/processors/organize-files.processor.ts +++ b/src/lib/processors/organize-files.processor.ts @@ -43,8 +43,8 @@ export async function processOrganizeFiles(payload: OrganizeFilesPayload): Promi await logger?.info(`Organizing: ${audiobook.title} by ${audiobook.author}`); - // Get file organizer - const organizer = getFileOrganizer(); + // Get file organizer (reads media_dir from database config) + const organizer = await getFileOrganizer(); // Organize files (pass logger to file organizer) const result = await organizer.organize( diff --git a/src/lib/processors/retry-failed-imports.processor.ts b/src/lib/processors/retry-failed-imports.processor.ts index c37ba4f..05aee43 100644 --- a/src/lib/processors/retry-failed-imports.processor.ts +++ b/src/lib/processors/retry-failed-imports.processor.ts @@ -72,8 +72,7 @@ export async function processRetryFailedImports(payload: RetryFailedImportsPaylo await jobQueue.addOrganizeJob( request.id, request.audiobook.id, - downloadPath, - `/media/audiobooks/${request.audiobook.author}/${request.audiobook.title}` + downloadPath ); triggered++; await logger?.info(`Triggered organize job for request ${request.id}: ${request.audiobook.title}`); diff --git a/src/lib/services/job-queue.service.ts b/src/lib/services/job-queue.service.ts index e7de02b..1baef7f 100644 --- a/src/lib/services/job-queue.service.ts +++ b/src/lib/services/job-queue.service.ts @@ -58,7 +58,7 @@ export interface OrganizeFilesPayload extends JobPayload { requestId: string; audiobookId: string; downloadPath: string; - targetPath: string; + targetPath?: string; // Optional - not used by processor (reads from database config) } export interface ScanPlexPayload extends JobPayload { @@ -499,12 +499,13 @@ export class JobQueueService { /** * Add organize files job + * Note: targetPath parameter is deprecated and unused (reads from database config instead) */ async addOrganizeJob( requestId: string, audiobookId: string, downloadPath: string, - targetPath: string + targetPath?: string ): Promise { return await this.addJob( 'organize_files', @@ -512,7 +513,7 @@ export class JobQueueService { requestId, audiobookId, downloadPath, - targetPath, + targetPath, // Not used by processor } as OrganizeFilesPayload, { priority: 8, diff --git a/src/lib/utils/file-organizer.ts b/src/lib/utils/file-organizer.ts index bd29ae2..2a0f4f4 100644 --- a/src/lib/utils/file-organizer.ts +++ b/src/lib/utils/file-organizer.ts @@ -464,16 +464,18 @@ export class FileOrganizer { } } -// Singleton instance -let fileOrganizer: FileOrganizer | null = null; +/** + * Get FileOrganizer instance configured from database settings + * Reads media_dir from database configuration, falls back to /media/audiobooks if not configured + */ +export async function getFileOrganizer(): Promise { + // Read media_dir from database config + const config = await prisma.configuration.findUnique({ + where: { key: 'media_dir' }, + }); -export function getFileOrganizer(): FileOrganizer { - if (!fileOrganizer) { - const mediaDir = process.env.MEDIA_DIR || '/media/audiobooks'; - const tempDir = process.env.TEMP_DIR || '/tmp/readmeabook'; + const mediaDir = config?.value || process.env.MEDIA_DIR || '/media/audiobooks'; + const tempDir = process.env.TEMP_DIR || '/tmp/readmeabook'; - fileOrganizer = new FileOrganizer(mediaDir, tempDir); - } - - return fileOrganizer; + return new FileOrganizer(mediaDir, tempDir); }