Files
ReadMeABook/documentation/phase3/file-organization.md
T
Claude ef98dcf438 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
2026-01-28 11:41:56 -05:00

4.5 KiB

File Organization System

Status: Implemented

Copies completed downloads to standardized directory structure for Plex. Automatically tags audio files with correct metadata. Originals kept for seeding, cleaned up by scheduled job after requirements met.

Target Structure

Target directory read from database config media_dir (configurable in setup wizard and settings).

[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. 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

Status: Implemented

Purpose: Automatically writes correct metadata to audio files during file organization to improve Plex matching accuracy.

Supported Formats:

  • m4b, m4a, mp4 (AAC audiobooks)
  • mp3 (ID3v2 tags)

Metadata Written:

  • title - Book title
  • album - Book title (PRIMARY field for Plex matching)
  • album_artist - Author (PRIMARY field for Plex matching)
  • artist - Author (fallback)
  • composer - Narrator (standard audiobook field)
  • date - Year

Configuration:

  • Key: metadata_tagging_enabled (Configuration table)
  • Default: true
  • Configurable in: Setup wizard (Paths step), Admin settings (Paths tab)

Implementation:

  • Uses ffmpeg with -codec copy (no re-encoding, metadata only)
  • Fast (no audio transcoding)
  • Lossless (original audio preserved)
  • Runs after file copy, before cover art download
  • Non-blocking (errors don't fail file organization)
  • Logs success/failure per file

Benefits:

  • Fixes torrents with missing/incorrect metadata
  • Ensures Plex can match audiobooks correctly
  • Writes metadata from Audible/Audnexus (known accurate)
  • Prevents "[Various Albums]" and other metadata issues

Tech Stack:

  • ffmpeg (system dependency - included in Docker image)
  • src/lib/utils/metadata-tagger.ts - Tagging utility
  • Integrated into src/lib/utils/file-organizer.ts

Requirements:

  • ffmpeg must be installed in the container
  • Multi-container setup (Dockerfile): Added at line 56 via apk add ffmpeg
  • Unified setup (dockerfile.unified): Added at line 16 via apt-get install ffmpeg
  • Verify installation:
    • Multi-container: docker exec readmeabook ffmpeg -version
    • Unified: docker exec readmeabook-unified ffmpeg -version

Seeding Support

Config: seeding_time_minutes (0 = unlimited, never cleanup)

Cleanup Job: cleanup_seeded_torrents (every 30 mins)

  1. Check 'available' and 'downloaded' status requests with download history
  2. Query qBittorrent for actual seeding_time field
  3. Delete torrent + files only after requirement met
  4. Respects config (0 = never cleanup)

Interface

interface OrganizationResult {
  success: boolean;
  targetPath: string;
  filesMovedCount: number;
  errors: string[];
  audioFiles: string[];
  coverArtFile?: string;
}

async function organize(
  downloadPath: string,
  audiobook: {title: string, author: string, year?: number, coverArtUrl?: string}
): Promise<OrganizationResult>;

Path Sanitization

  • Remove invalid chars: <>:"/\|?*
  • Trim dots/spaces
  • Collapse multiple spaces
  • 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

  • Node.js fs/promises
  • path module
  • axios (cover art download)