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.
20 KiB
Settings Pages
Status: ✅ Implemented | ♻️ Refactored (Jan 2026)
Single tabbed interface for admins to view/modify system configuration post-setup with mandatory validation before saving.
Architecture (Refactored Jan 2026)
Original: Monolithic 2,971-line component Current: Modular architecture with 89% code reduction (2,971 → 325 lines)
Structure:
src/app/admin/settings/
├── page.tsx # Shell component (325 lines)
├── lib/
│ ├── types.ts # Shared TypeScript interfaces
│ └── helpers.ts # Business logic (206 lines)
├── hooks/
│ └── useSettings.ts # Global settings hook
└── tabs/ # Feature modules
├── LibraryTab/ # Plex/Audiobookshelf config
│ ├── LibraryTab.tsx
│ ├── useLibrarySettings.ts
│ ├── PlexSection.tsx
│ ├── AudiobookshelfSection.tsx
│ └── index.ts
├── AuthTab/ # Authentication (OIDC + Manual)
│ ├── AuthTab.tsx
│ ├── useAuthSettings.ts
│ ├── OIDCSection.tsx
│ ├── RegistrationSection.tsx
│ ├── PendingUsersTable.tsx
│ └── index.ts
├── IndexersTab/ # Prowlarr/indexers
│ ├── IndexersTab.tsx
│ ├── useIndexersSettings.ts
│ └── index.ts
├── DownloadTab/ # qBittorrent/Transmission/SABnzbd
│ ├── DownloadTab.tsx
│ ├── useDownloadSettings.ts
│ └── index.ts
├── PathsTab/ # Directory paths
│ ├── PathsTab.tsx
│ ├── usePathsSettings.ts
│ └── index.ts
├── EbookTab/ # E-book sidecar
│ ├── EbookTab.tsx
│ ├── useEbookSettings.ts
│ └── index.ts
└── BookDateTab/ # AI recommendations
├── BookDateTab.tsx
├── useBookDateSettings.ts
└── index.ts
Benefits:
- Single Responsibility: Each tab manages its own state/logic
- Testability: Individual tabs can be unit tested
- Maintainability: Changes to one feature don't affect others
- Performance: Lazy loading possible (future optimization)
- Reusability: Custom hooks can be used elsewhere
- Code Quality: Follows React best practices
Sections
- Plex - URL, token (masked), library ID, Audible region, filesystem scan trigger toggle
- Audiobookshelf - URL, API token (masked), library ID, Audible region, filesystem scan trigger toggle
- Prowlarr - URL, API key (masked), indexer selection with priority, seeding time, RSS monitoring toggle, audiobook/ebook categories per indexer
- Download Client - Type (qBittorrent, Transmission, SABnzbd), URL, credentials (masked), custom download path (per-client relative sub-path with live preview)
- Paths - Download + media directories, audiobook organization template, metadata tagging toggle, chapter merging toggle, Plex format coercion toggle
- E-book Sidecar - Multi-source ebook downloads (Anna's Archive + Indexer Search), preferred format
- BookDate - AI provider, API key (encrypted), model selection, library scope, custom prompt, swipe history
- Notifications - Multiple backends (Discord, Pushover), event subscriptions, test functionality
E-book Sidecar
Purpose: Configure ebook download sources and preferences to accompany audiobook downloads.
Tab Structure (3 sections):
-
Anna's Archive Section
- Enable toggle for Anna's Archive downloads
- Base URL (default:
https://annas-archive.gl) - FlareSolverr URL (optional, for Cloudflare bypass)
-
Indexer Search Section
- Enable toggle for indexer-based ebook search via Prowlarr
- Hint directing users to Indexers tab for category configuration
-
General Settings Section (visible when any source enabled)
- Preferred format: EPUB (recommended), PDF, MOBI, AZW3, Any
- Auto-grab toggle: Automatically create ebook requests after audiobook downloads
- Kindle fix toggle: Apply compatibility fixes to EPUB files (only visible when EPUB format selected)
Configuration Keys:
| Key | Default | Description |
|---|---|---|
ebook_annas_archive_enabled |
false |
Enable Anna's Archive |
ebook_indexer_search_enabled |
false |
Enable Indexer Search via Prowlarr |
ebook_sidecar_preferred_format |
epub |
Preferred format |
ebook_auto_grab_enabled |
true |
Auto-create ebook requests after audiobook downloads |
ebook_kindle_fix_enabled |
false |
Apply Kindle compatibility fixes to EPUB files |
ebook_sidecar_base_url |
https://annas-archive.gl |
Anna's Archive mirror |
ebook_sidecar_flaresolverr_url |
`` | FlareSolverr URL |
Behavior:
- If Anna's Archive enabled → Searches Anna's Archive first
- If Indexer Search enabled → Falls back to indexer search if Anna's Archive fails/disabled
- If both disabled → Ebook downloads completely off
- If auto-grab disabled → Manual "Fetch Ebook" button only (admin buttons still work)
- If Kindle fix enabled (and EPUB format) → Applies compatibility fixes during organization
Indexer Categories (Tabbed)
Purpose: Configure separate category sets for audiobook and ebook searches per indexer.
UI: Edit Indexer modal has Categories section with two tabs:
- AudioBook tab - Categories for audiobook searches (default:
[3030]) - EBook tab - Categories for ebook searches (default:
[7020])
Storage: prowlarr_indexers JSON config stores:
{
"id": 1,
"name": "MyIndexer",
"audiobookCategories": [3030],
"ebookCategories": [7020],
...
}
Auto-Search Behavior (Indexers tab)
Purpose: Control how ReadMeABook performs automatic indexer searches. Lives on the Indexers tab between the Prowlarr connection block and the IndexerManagement list.
Toggle: Skip unreleased books in automatic searches
- When ON: auto-search skips books whose release date is in the future. Those requests automatically start searching once the book is released. Manual searches are unaffected.
- When OFF: auto-search proceeds regardless of release date.
Configuration Key:
| Key | Default | Category | Description |
|---|---|---|---|
indexer.skip_unreleased |
true (ON) |
indexer |
Skip auto-searches for books with future release dates |
Read contract (consumed by background workers):
value !== 'false'→ ON (skip enabled). Missing key OR any non-'false'value → ON.- Only the exact string
'false'disables the toggle. Workers MUST match this.
API: Persisted via PUT /api/admin/settings/indexer-options. Saved alongside Prowlarr connection + indexer config when the Indexers tab Save button is clicked.
Audible Region
Purpose: Configure which Audible region to use for metadata and search to ensure accurate ASIN matching with your metadata engine.
Configuration:
- Key:
audible.region(string, default: 'us') - Supported regions: US, Canada, UK, Australia, India
- UI: Dropdown selector in Library tab (both Plex and Audiobookshelf settings)
- No validation required (immediate save)
Why It Matters:
- Each Audible region uses different ASINs for the same audiobook
- Users must match their RMAB region to their Plex/Audiobookshelf metadata engine region
- Mismatched regions cause poor search results and failed metadata matching
Help Text: "Select the Audible region that matches your metadata engine (Audnexus/Audible Agent) configuration in [Plex/Audiobookshelf]. This ensures accurate book matching and metadata."
Implementation:
- Affects all Audible API calls (base URL changes per region)
- Affects all Audnexus API calls (region parameter added)
- Changes apply immediately on next API call (no restart required)
- Automatic refresh: Changing region automatically triggers
audible_refreshjob to fetch popular/new releases for the new region - Cache management: ConfigService cache and AudibleService initialization are cleared when region changes
- Smart re-initialization: Service automatically detects region changes and re-initializes before each request
- See:
documentation/integrations/audible.mdfor technical details
Audiobook Organization Template
Purpose: Customize how audiobooks are organized within the media directory using variable-based templates.
Configuration:
- Key:
audiobook_path_template(string, default:{author}/{title} {asin}) - Variables:
{author},{title},{narrator},{asin},{year} - Optional variables (narrator, asin, year) removed if not available
- Template validated on test, shows preview examples
UI (PathsTab):
- Text input with monospace font
- Placeholder:
{author}/{title} {asin} - Variable reference panel showing all available variables
- Template validation on "Test Paths" with success/error feedback
- Preview examples showing 2-3 sample paths with actual data
Validation:
- Must contain at least
{author}or{title}(required variables) - Cannot be empty or only contain optional variables
- Invalid templates show error message
- Valid templates show preview paths
Examples:
{author}/{title} {asin}→Douglas Adams/The Hitchhiker's Guide to the Galaxy B0009JKV9W/{author}/{title} ({year})→Douglas Adams/The Hitchhiker's Guide to the Galaxy (2005)/{author}/{narrator}/{title}→Douglas Adams/Stephen Fry/The Hitchhiker's Guide to the Galaxy/
Filesystem Scan Trigger
Purpose: Trigger Plex/Audiobookshelf to scan filesystem after organizing files for users with disabled filesystem watchers.
Configuration:
- Plex:
plex.trigger_scan_after_import(boolean, default: false) - Audiobookshelf:
audiobookshelf.trigger_scan_after_import(boolean, default: false)
UI:
- Checkbox toggle in both Plex and Audiobookshelf settings tabs
- Default: Unchecked (disabled)
- Help text: "Only enable this if you have [Plex/Audiobookshelf]'s filesystem watcher (automatic scanning) disabled. Most users should leave this disabled and rely on [Plex/Audiobookshelf]'s built-in automatic detection."
Behavior:
- When enabled: After
organize_filesjob completes, RMAB triggers filesystem scan in media server - 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.
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:
- User modifies settings (URL, credentials, paths)
- User clicks "Test Connection" or "Test Paths"
- System validates settings
- On success: "Save Changes" button enabled
- On failure: Error shown, "Save Changes" remains disabled
Prowlarr (special handling):
- On tab load: Current indexer configuration loaded from database automatically
- Changing indexer settings (enable/disable, priority, seeding time, RSS):
- No test required
- Can save immediately if URL/API key unchanged
- Changing URL or API key:
- Validation required before saving
- User clicks "Test Connection"
- On success: Indexers refresh automatically, "Save Changes" enabled
- Button text adapts:
- "Test Connection" when URL/API key changed
- "Refresh Indexers" when connection info unchanged
BookDate (Admin Settings):
- On tab load: Current BookDate global configuration loaded from database automatically
- Changing AI provider: Resets model selection
- Test connection: Required to fetch available models before saving
- Changing API key: Must test connection to verify and fetch models
- Saving configuration: Validates all fields (provider, API key, model)
- Note: Library scope and custom prompt are now per-user settings (configured in BookDate page)
- Clear swipe history: Confirmation dialog, removes ALL users' swipes and cached recommendations
- No "Save Changes" button - uses dedicated "Save BookDate Configuration" button
- Accessible to admins only
BookDate (User Preferences - in /bookdate page):
- Settings icon: Opens modal with per-user preferences
- Library scope: Full library or rated books only (default: full)
- Custom prompt: Optional text (max 1000 chars, default: blank)
- Save: Updates user preferences immediately
- Accessible to all authenticated users
Validation state resets when:
- Plex: URL or token modified
- Prowlarr: URL or API key modified (NOT indexer config)
- Download Client: URL, username, or password modified
- Paths: Directory paths or template modified
API Endpoints
GET /api/admin/settings
- Returns all config (passwords masked as ••••)
- Admin auth required
GET /api/admin/settings/prowlarr/indexers
- Returns current indexer configuration merged with available Prowlarr indexers
- Loads saved settings (enabled, priority, seeding time, RSS) from database
- Merges with live indexer list from Prowlarr
- Admin auth required
PUT /api/admin/settings/plex
- Updates Plex config
- Requires prior successful test if URL/token changed
PUT /api/admin/settings/prowlarr
- Updates Prowlarr URL and API key
- Requires prior successful test if values changed
PUT /api/admin/settings/audible
- Updates Audible region
- Body:
{ region: string }(one of: us, ca, uk, au, in, es, fr) - No validation required
PUT /api/admin/settings/prowlarr/indexers
- Updates indexer configuration (enabled, priority, seeding time, RSS)
- No test required if URL/API key unchanged
- Saves only enabled indexers to database
GET /api/admin/settings/indexer-options
- Returns
{ skipUnreleased: boolean } - Default ON: missing or non-
'false'value resolves totrue - Admin auth required
PUT /api/admin/settings/indexer-options
- Updates indexer-wide auto-search options
- Body:
{ skipUnreleased: boolean }(strict boolean validation) - Persists
indexer.skip_unreleased(category:indexer) - No connection test required
PUT /api/admin/settings/download-client
- Updates download client config
- Requires prior successful test if credentials changed
PUT /api/admin/settings/paths
- Updates paths and audiobook organization template
- Requires prior successful test if paths or template changed
- Body:
{ downloadDir, mediaDir, audiobookPathTemplate, metadataTaggingEnabled, chapterMergingEnabled }
Test Endpoints (authenticated, handle masked values):
- POST /api/admin/settings/test-plex - Tests Plex connection, uses stored token if masked, returns libraries
- POST /api/admin/settings/test-prowlarr - Tests connection, uses stored API key if masked, returns indexers
- POST /api/admin/settings/test-download-client - Tests qBittorrent/Transmission, uses stored password if masked
- POST /api/setup/test-paths - Validates paths writable and template format, returns
{success, message, templateValidation: {isValid, error?, previewPaths?}}
BookDate Endpoints:
- GET /api/bookdate/config - Get global BookDate configuration (API key excluded, admin only)
- POST /api/bookdate/config - Save/update global BookDate configuration (admin only)
- POST /api/bookdate/test-connection - Test AI provider connection and fetch available models
- DELETE /api/bookdate/swipes - Clear ALL users' swipe history and cached recommendations (admin only)
- GET /api/bookdate/preferences - Get user's preferences (libraryScope, customPrompt)
- PUT /api/bookdate/preferences - Update user's preferences (all authenticated users)
Features
- Password visibility toggle
- Mandatory "Test Connection" buttons per tab
- "Save Changes" disabled until current tab validated
- Test result display (success/error messages)
- Toast notifications for save confirmations
- Form validation with Zod schemas
- Reuses setup wizard connection test endpoints
- Visual warning when validation required
Security
- Admin role required
- Passwords never returned in GET (masked)
- Connection tests validate before saving
- HTTPS required in production
Validation
Plex: Valid HTTP/HTTPS URL, non-empty token, library ID selected
Prowlarr: Valid URL, non-empty API key, ≥1 indexer configured, priority 1-25, seedingTimeMinutes ≥0, rssEnabled boolean
Download Client: Valid URL, credentials required, type must be 'qbittorrent', 'transmission', or 'sabnzbd'
Paths: Absolute paths, exist or creatable, writable, cannot be same directory, template must contain {author} or {title}
Tech Stack
- React Hook Form
- Zod validation
- Tab/sidebar navigation
- Toast notifications
Fixed Issues ✅
1. Settings Save Without Validation
- Issue: Users could save invalid/broken settings (wrong URLs, bad credentials, invalid paths)
- Cause: No validation enforcement before save
- Fix: Added mandatory "Test Connection"/"Test Paths" buttons per tab, disabled "Save Changes" until validated
- Behavior: Now matches wizard flow - test first, then save
2. Testing with Masked Credentials
- Issue: Test connection failed because it was testing with masked
••••values instead of actual credentials - Cause: Test endpoints didn't handle masked values, tried to authenticate with literal
•••••••• - Fix: Created authenticated test endpoints that read actual values from database when masked values detected
- Endpoints:
/api/admin/settings/test-plex,/test-prowlarr,/test-download-client - Behavior: Users can test without re-entering unchanged passwords
3. Indexer Configuration Workflow
- Issue: Indexer settings required re-testing before saving, current settings weren't loading, workflow confusing
- Cause: Indexers only loaded after test, changing any indexer setting invalidated connection
- Fix:
- Load current indexer config from database on tab load (GET
/api/admin/settings/prowlarr/indexers) - Track which values changed (URL/API key vs indexer config)
- Only require test if URL/API key changed
- Allow saving indexer config changes without re-testing connection
- Button text adapts: "Test Connection" vs "Refresh Indexers"
- Load current indexer config from database on tab load (GET
- Behavior: Natural workflow - see current settings, modify indexers, save immediately
Notifications
Purpose: Configure notification backends to receive alerts for audiobook request events.
Configuration:
- Multiple backends per type (Discord, Pushover)
- Per-backend event subscriptions (4 events)
- Encrypted sensitive values (webhook URLs, API keys)
- Enable/disable toggle per backend
UI (NotificationsTab):
- Type selector cards: Discord (indigo "D"), Pushover (blue "P")
- Grid layout for configured backends (3 columns)
- Card shows: type icon, name, enabled status, event count
- Edit/delete actions per card
Modal (NotificationConfigModal):
- Type-specific forms (Discord: webhook/username/avatar, Pushover: keys/priority)
- Event subscription checkboxes (4 events)
- Enable/disable toggle
- Test button (sends sample notification)
- Password masking for sensitive values
Event Types:
- Request Pending Approval - Admin approval required
- Request Approved - Approved (manual or auto)
- Request Available - Available in library
- Request Error - Failed at any stage
Validation:
- Name required
- Discord: webhook URL required
- Pushover: user key + app token required
- At least one event selected