Add Plex format coercion (.mp4 → .m4b)

Implement Plex-compatible file-extension coercion to avoid Plex silently ignoring .mp4 (and single-file .m4a) audiobooks (issue #166). Adds a DB migration and configuration key (plex_format_coercion_enabled, default true), exposes a toggle in the setup wizard and Admin Paths settings, and persists/reads the setting in the admin/setup APIs.

Introduces src/lib/utils/format-coercion.ts (coerceToPlexCompatible) and related constants in src/lib/constants/audio-formats.ts (PLEX_COMPATIBLE_EXTENSIONS, COERCION_RENAME_MAP, DRM_EXTENSIONS, TRANSCODE_REQUIRED_EXTENSIONS). The organize-files processor now runs coercion after organizing/tagging and before generating the filesHash and triggering scans; coercion is idempotent, never overwrites existing targets, logs warnings on DRM/transcode/permission errors, and is non-fatal.

Adds unit tests for the coercion util and updates processor & setup UI tests. Updates documentation (TABLEOFCONTENTS, file-organization, fixes/file-hash-matching, settings-pages) describing behavior, config, and constraints.
This commit is contained in:
kikootwo
2026-05-15 19:33:59 -04:00
parent 6f8ac86a43
commit f23afc1ba2
18 changed files with 815 additions and 7 deletions
+1
View File
@@ -69,6 +69,7 @@
- **qBittorrent integration (torrents)** → [phase3/qbittorrent.md](phase3/qbittorrent.md)
- **SABnzbd integration (Usenet/NZB)** → [phase3/sabnzbd.md](phase3/sabnzbd.md)
- **File organization, seeding** → [phase3/file-organization.md](phase3/file-organization.md)
- **Plex-compatible format coercion (.mp4 → .m4b)** → [phase3/file-organization.md](phase3/file-organization.md#plex-format-coercion)
- **Chapter merging (auto-merge to M4B)** → [features/chapter-merging.md](features/chapter-merging.md)
## Background Jobs
@@ -154,6 +154,12 @@ model Audiobook {
- Hash generated AFTER merging
- **Works correctly:** Hash reflects final organized state
### Coerced Files (Plex Format Coercion)
- Files renamed from `.mp4``.m4b` (or single-file `.m4a``.m4b`) by Plex format coercion
- Hash generated AFTER coercion → reflects post-coercion filenames
- **Works correctly going forward:** ABS sees post-coercion names, hash matches
- **Pre-existing library entries** hashed before coercion was enabled will NOT match post-coercion files — retroactive library sweep is out of scope (see issue #166)
### Multiple Downloads (Same Book)
- User re-downloads same audiobook (different edition/request)
- Multiple records with same `filesHash`
+61 -4
View File
@@ -44,10 +44,11 @@ Result: Douglas Adams/Stephen Fry/The Hitchhiker's Guide to the Galaxy/
5. **Copy** files (not move - originals stay for seeding)
6. **Tag metadata** (if enabled) - writes correct title, author, narrator, ASIN to audio files
7. Copy cover art if found, else download from Audible
8. **Generate file hash** - SHA256 of sorted audio filenames for library matching (see: [fixes/file-hash-matching.md](../fixes/file-hash-matching.md))
9. Update request status to `downloaded` and store file hash in `audiobooks.files_hash`
10. **Trigger filesystem scan** (if enabled) - tells Plex/ABS to scan for new files
11. Originals remain until seeding requirements met
8. **Coerce file formats** (if enabled) - rename .mp4 → .m4b and single-file .m4a → .m4b for Plex compatibility (see: Plex Format Coercion below)
9. **Generate file hash** - SHA256 of sorted audio filenames for library matching (see: [fixes/file-hash-matching.md](../fixes/file-hash-matching.md))
10. Update request status to `downloaded` and store file hash in `audiobooks.files_hash`
11. **Trigger filesystem scan** (if enabled) - tells Plex/ABS to scan for new files
12. Originals remain until seeding requirements met
## Filesystem Scan Triggering
@@ -150,6 +151,61 @@ exiftool "audiobook.m4b" | grep -i asin
- Multi-container: `docker exec readmeabook ffmpeg -version`
- Unified: `docker exec readmeabook-unified ffmpeg -version`
## Plex Format Coercion
**Status:** ✅ Implemented | Issue #166
**Purpose:** Rename audiobook files to Plex-recognized extensions before the library scan. Plex silently ignores `.mp4` files in audiobook libraries; this step prevents that silent-failure mode. Rename-only — no transcoding.
**When:** After file organization and metadata tagging, before file-hash generation and before library scan trigger.
**Scope:** Audio path only. Not applied to ebook organization.
**Coercion Table:**
| Source ext | Action |
|---|---|
| `.mp4` | Rename to `.m4b` |
| `.m4a` (single audio file in folder) | Rename to `.m4b` |
| `.m4a` (multi-file folder) | No-op |
| `.m4b`, `.mp3`, `.flac`, `.aac`, `.wav`, `.alac` | No-op |
| `.aa`, `.aax` | No-op + warn ("DRM, Plex cannot import") |
| `.ogg`, `.opus`, `.wma`, other | No-op + warn ("requires transcode, not supported in v1") |
**Configuration:**
- Key: `plex_format_coercion_enabled` (Configuration table)
- Default: `true`
- Read contract: `value !== 'false'` enables (default-on semantics)
- Configurable in: Setup wizard (Paths step), Admin settings (Paths tab)
**Behavior:**
- Each audio file evaluated independently (mixed-format folders supported).
- Pre-rename collision check: if target exists → no-op + info log. Never overwrites.
- Idempotent: re-running on already-coerced folder is a no-op (extension is the signal — no marker files).
- Operates on `targetPath` (organized library files) only — never touches `/downloads` (seeding-safe).
**Failure Isolation:**
- Coercion wrapped in try/catch at processor level.
- Any failure (e.g., EPERM) logs a warning; request remains organized; original file untouched.
- A failed rename never regresses the request to "stuck."
**Tech Stack:**
- `src/lib/utils/format-coercion.ts` — coercion module
- `src/lib/constants/audio-formats.ts``PLEX_COMPATIBLE_EXTENSIONS`, `COERCION_RENAME_MAP`, `DRM_EXTENSIONS`, `TRANSCODE_REQUIRED_EXTENSIONS`
- Invoked from `src/lib/processors/organize-files.processor.ts` between file organization and `generateFilesHash`
- `fs.rename` (same filesystem — no cross-mount issues)
**Hash Interaction:**
- File hash (`audiobooks.files_hash`) is generated AFTER coercion → reflects post-coercion filenames.
- See: [fixes/file-hash-matching.md](../fixes/file-hash-matching.md) for hash semantics.
**Out of Scope (v1):**
- Transcoding (`.ogg`, `.opus`, `.wma`)
- DRM decoding (`.aa`, `.aax`)
- FLAC → M4B (already Plex-recognized)
- Per-request override UI
- Retroactive library sweep (new downloads only)
## Seeding Support
**Config:** `seeding_time_minutes` (0 = unlimited, never cleanup)
@@ -203,6 +259,7 @@ async function organize(
- **Path template:** Read from database config key `audiobook_path_template` (default: `{author}/{title} {asin}`)
- **Metadata tagging:** `metadata_tagging_enabled` (boolean, default: true)
- **Chapter merging:** `chapter_merging_enabled` (boolean, default: false)
- **Plex format coercion:** `plex_format_coercion_enabled` (boolean, default: true)
- **Fallback:** `/media/audiobooks` if media_dir not configured
- **Temp directory:** `/tmp/readmeabook` (or `TEMP_DIR` env var)
+22 -1
View File
@@ -68,7 +68,7 @@ src/app/admin/settings/
2. **Audiobookshelf** - URL, API token (masked), library ID, Audible region, filesystem scan trigger toggle
3. **Prowlarr** - URL, API key (masked), indexer selection with priority, seeding time, RSS monitoring toggle, **audiobook/ebook categories per indexer**
4. **Download Client** - Type (qBittorrent, Transmission, SABnzbd), URL, credentials (masked), custom download path (per-client relative sub-path with live preview)
5. **Paths** - Download + media directories, audiobook organization template, metadata tagging toggle, chapter merging toggle
5. **Paths** - Download + media directories, audiobook organization template, metadata tagging toggle, chapter merging toggle, Plex format coercion toggle
6. **E-book Sidecar** - Multi-source ebook downloads (Anna's Archive + Indexer Search), preferred format
7. **BookDate** - AI provider, API key (encrypted), model selection, library scope, custom prompt, swipe history
8. **Notifications** - Multiple backends (Discord, Pushover), event subscriptions, test functionality
@@ -222,6 +222,27 @@ src/app/admin/settings/
- When disabled: User relies on media server's filesystem watcher or manual scans
- Error handling: Scan failures logged but don't fail organize job (graceful degradation)
## Plex Format Coercion
**Purpose:** Rename audiobook files to Plex-recognized extensions (`.mp4` → `.m4b`, single-file `.m4a` → `.m4b`) before the library scan. Prevents Plex silently ignoring `.mp4` audiobooks. Rename-only — no transcoding. See: [phase3/file-organization.md](phase3/file-organization.md#plex-format-coercion).
**Configuration:**
- Key: `plex_format_coercion_enabled` (boolean, default: `true`)
- Read contract: `value !== 'false'` enables (default-on)
- Configurable in: Setup wizard (Paths step), Admin settings (Paths tab)
**UI:**
- Checkbox toggle in PathsTab, between metadata tagging and chapter merging
- Default: Checked (enabled)
- Label: "Coerce file formats for Plex compatibility"
- Sub-text: "Rename .mp4 audiobook files (and single-file .m4a) to .m4b before Plex scans. No re-encoding."
**Behavior:**
- When enabled: After organize, rename files per coercion table before scan trigger
- When disabled: Files left as-is (Plex may silently skip `.mp4`)
- Failure isolation: Rename errors logged but don't fail organize job
- Universal (Plex + ABS) — rename is lossless, no per-backend distinction
## Validation Flow
**Plex, Download Client, Paths:**