Fix file copy location to respect configured media directory

Previously, files were always being copied to /media/audiobooks regardless
of the configured media directory in settings. This was caused by:

1. FileOrganizer singleton reading from MEDIA_DIR env var (never set)
   instead of database config 'media_dir'
2. Hardcoded /media/audiobooks fallback being used when env var not found
3. Three locations passing hardcoded paths to addOrganizeJob (unused)

Changes:
- Modified getFileOrganizer() to read media_dir from database config
- Made targetPath parameter optional in addOrganizeJob (not used by processor)
- Removed hardcoded /media/audiobooks paths from all addOrganizeJob calls
- Updated organize-files processor to await getFileOrganizer()
- Updated documentation to reflect configuration behavior

Files now correctly copy to the directory configured in setup wizard or
settings page, with /media/audiobooks only as fallback if not configured.

Fixes: User-reported issue where configured media directory was ignored
This commit is contained in:
Claude
2025-12-22 00:05:19 +00:00
committed by kikootwo
parent a59bbedd00
commit ef98dcf438
7 changed files with 40 additions and 28 deletions
+18 -6
View File
@@ -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 <Best>! 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
+1 -2
View File
@@ -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({
@@ -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}`);
@@ -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(
@@ -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}`);
+4 -3
View File
@@ -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<string> {
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,
+12 -10
View File
@@ -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<FileOrganizer> {
// 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);
}