mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user