12 KiB
Chapter Merging Feature
Status: ❌ Not Started | Product Requirements Document
Overview
Automatically merge multi-file audiobook downloads (separate MP3/M4A files per chapter) into a single M4B file with proper chapter markers during file organization.
Problem Statement
Current Behavior:
- Torrents with individual chapter files (e.g.,
ch01.mp3,ch02.mp3) are copied as-is - Results in 10-50+ individual files in Plex library
- Poor playback experience (no chapter navigation, file switching)
- Inconsistent with single-file audiobook standard
User Impact:
- Must manually skip between files
- No chapter bookmarks/navigation
- Cluttered library view
- Some audiobook players don't handle multi-file books well
Solution
Detect multi-file chapter downloads and merge into single M4B with embedded chapters.
Key Requirements
Detection Logic
Chapter File Patterns (auto-detect):
- Numeric:
01.mp3,001.mp3,1.mp3 - Named:
Chapter 1.mp3,Chapter 01.mp3,Ch1.mp3,Ch 01.mp3 - Part-based:
Part 1.mp3,Part01.mp3 - Combined:
Harry Potter - 01 - Chapter 1.mp3
Trigger Conditions:
- 2+ audio files in download
- Files match chapter naming pattern
- All files same format (m4a, m4b, mp3)
- Feature enabled in config
Exclusions (do NOT merge):
- Mixed formats (some MP3, some M4A)
- Non-sequential numbering
- Files without clear chapter indicators
- Single file downloads
Chapter Metadata Generation
Chapter Naming Strategy:
- From filename: Extract "Chapter 1", "01", "Part 1"
- Fallback numbering: "Chapter 1", "Chapter 2" if no name found
- Preserve order: Sort files naturally (ch1, ch2, ch10)
Chapter Timing:
- Calculate from individual file durations using ffprobe
- Format: FFMETADATA1 standard
- Timestamps in milliseconds
Example:
;FFMETADATA1
[CHAPTER]
TIMEBASE=1/1000
START=0
END=2700000
title=Chapter 1: The Beginning
[CHAPTER]
TIMEBASE=1/1000
START=2700000
END=5400000
title=Chapter 2: The Journey
FFmpeg Implementation
For M4A/M4B files (same format, no re-encode):
# 1. Create concat list
echo "file '/path/ch01.m4a'" > filelist.txt
echo "file '/path/ch02.m4a'" >> filelist.txt
# 2. Generate chapter metadata
# [Create chapters.txt with timing from durations]
# 3. Merge with chapters
ffmpeg -f concat -safe 0 -i filelist.txt \
-i chapters.txt \
-map_metadata 1 \
-codec copy \
-metadata title="Book Title" \
-metadata album="Book Title" \
-metadata album_artist="Author" \
-metadata artist="Author" \
-metadata composer="Narrator" \
-metadata date="2024" \
-f mp4 \
output.m4b
For MP3 files (requires conversion):
# Must re-encode to M4B (AAC)
ffmpeg -f concat -safe 0 -i filelist.txt \
-i chapters.txt \
-map_metadata 1 \
-codec:a aac -b:a 128k \ # Quality preservation
-metadata title="Book Title" \
# ... (same metadata)
-f mp4 \
output.m4b
Quality Settings (MP3 → M4B):
- Bitrate: 128kbps AAC (transparent for audiobooks, 64kbps minimum)
- Sampling rate: Match source (44.1kHz or 48kHz)
- Channels: Preserve mono/stereo
File Naming
Output filename:
[Author]/[Title] ([Year])/[Title].m4b
Cover art: Extract from first file or download from Audible (existing logic)
Configuration
New config keys:
chapter_merging_enabled(boolean, default: false)chapter_merging_mp3_bitrate(string, default: "128k")chapter_merging_delete_originals(boolean, default: true - after successful merge)
Settings UI (Admin → Paths tab):
☐ Merge multi-file chapter downloads into single M4B
↳ Audio quality for MP3 conversion: [128kbps ▼]
↳ ☑ Delete original chapter files after merge
Setup wizard (Paths step):
- Checkbox: "Merge chapter files" (default: unchecked)
- Tooltip: "Combines separate chapter files into single audiobook with chapter markers"
User Experience
Success Flow
- Download completes: 25 chapter MP3 files
- File organization starts
- System detects chapter pattern
- Merges files with progress logging:
- "Detected 25 chapter files, merging into single M4B..."
- "Processing chapter 1/25..."
- "Merge complete: BookTitle.m4b (15.2 GB, 25 chapters)"
- Copies merged M4B to target directory
- Deletes temp files and originals (if configured)
- Plex scans single M4B with full chapter navigation
Fallback Flow
If merge fails:
- Log error: "Chapter merge failed: [reason]"
- Fall back to current behavior: copy individual files
- Mark request as "available" (not failed)
- User can manually merge later
Failure scenarios:
- FFmpeg crash/timeout
- Insufficient disk space for temp file
- Corrupted source files
- Unsupported audio codec
Technical Implementation
File: src/lib/utils/chapter-merger.ts
Exports:
interface ChapterFile {
path: string;
filename: string;
duration: number; // seconds
chapterName: string; // extracted from filename
}
interface MergeOptions {
title: string;
author: string;
narrator?: string;
year?: number;
outputPath: string;
mp3Bitrate?: string; // default: "128k"
}
interface MergeResult {
success: boolean;
outputPath?: string;
chapterCount?: number;
duration?: number; // total seconds
error?: string;
}
// Main functions
async function detectChapterFiles(files: string[]): Promise<boolean>;
async function sortChapterFiles(files: string[]): Promise<ChapterFile[]>;
async function getAudioDuration(filePath: string): Promise<number>;
async function generateChapterMetadata(chapters: ChapterFile[]): Promise<string>;
async function mergeChapters(chapters: ChapterFile[], options: MergeOptions): Promise<MergeResult>;
Integration Points
File: src/lib/utils/file-organizer.ts
Modify organize() method:
// After finding audiobook files (line ~73)
if (audioFiles.length > 1) {
const config = await prisma.configuration.findUnique({
where: { key: 'chapter_merging_enabled' }
});
const mergingEnabled = config?.value === 'true';
const isChapterDownload = await detectChapterFiles(audioFiles);
if (mergingEnabled && isChapterDownload) {
// Merge chapters instead of copying individually
const mergeResult = await mergeChapters(audioFiles, {
title: audiobook.title,
author: audiobook.author,
narrator: audiobook.narrator,
year: audiobook.year,
outputPath: path.join(targetPath, `${audiobook.title}.m4b`)
});
if (mergeResult.success) {
result.audioFiles = [mergeResult.outputPath];
result.filesMovedCount = 1;
// Skip individual file copying
} else {
// Fallback to individual file copying
await logger?.warn(`Chapter merge failed, copying files individually`);
// Continue with existing logic
}
}
}
Database Schema
No changes required - uses existing Configuration table
Dependencies
Already available:
- ffmpeg (installed in Docker images)
- ffprobe (for duration detection)
Edge Cases & Error Handling
Edge Cases
| Scenario | Behavior |
|---|---|
| Mixed formats (MP3 + M4A) | Skip merge, copy individually |
| Non-sequential numbering (1, 3, 5) | Attempt merge, log warning |
| Duplicate chapter numbers | Sort by filename, log warning |
| Very large file count (100+ chapters) | Continue merge, increase timeout |
| Missing chapters (1, 2, 4) | Merge available, log warning |
| Single chapter file | Skip merge (not a multi-file book) |
| No chapter indicators | Skip merge, copy individually |
Error Handling
Disk space checks:
- Estimate merged file size (sum of source files + 10% overhead)
- Check available space before merge
- Fail gracefully if insufficient space
Timeouts:
- Set timeout based on file count and size
- Default: 5 minutes + (1 minute per chapter)
- Log progress every 10 chapters
Cleanup:
- Always remove temp concat lists
- Remove temp merged file on failure
- Keep original files if merge fails
Performance Considerations
Processing Time Estimates
M4A/M4B merge (no re-encode):
- 10 chapters: ~30 seconds
- 25 chapters: ~1 minute
- 50 chapters: ~2 minutes
MP3 → M4B conversion:
- 10 hours audiobook: ~5-10 minutes (depends on CPU)
- Real-time encoding speed varies by hardware
Resource Usage
- CPU: High during MP3 conversion, low for M4A copy
- Disk: Requires space for temp merged file (= sum of source files)
- Memory: Low (streaming processing)
Optimization
- Process in background job (already async)
- Don't block other downloads
- Limit concurrent merges (1 at a time recommended)
Testing Strategy
Test Cases
-
M4A chapter files (20 files)
- Verify merge succeeds
- Verify chapter count matches file count
- Verify metadata preserved
- Verify chapter navigation works in Plex
-
MP3 chapter files (15 files)
- Verify conversion to M4B
- Verify audio quality (bitrate ~128kbps)
- Verify no audio glitches at chapter boundaries
-
Mixed formats
- Verify merge skipped
- Verify fallback to individual files
-
Failed merge
- Verify fallback behavior
- Verify original files preserved
- Verify request marked available (not failed)
-
Chapter naming
- "Ch1.mp3" → "Chapter 1"
- "001 - Introduction.mp3" → "Introduction"
- "Part 1.mp3" → "Part 1"
-
Edge cases
- Single file: no merge
- 100+ chapters: successful merge
- Missing chapters (gaps): successful merge with warning
Success Metrics
Functional
- ✅ Successful merge rate > 95% (for valid chapter downloads)
- ✅ Chapter navigation works in Plex
- ✅ Zero audio quality degradation (M4A copy mode)
- ✅ Fallback works 100% of time on merge failure
Performance
- ✅ M4A merge: < 2 minutes for 25 chapters
- ✅ MP3 conversion: < 15 minutes for 10-hour audiobook
- ✅ No impact on concurrent downloads
User Experience
- ✅ Feature opt-in (default disabled)
- ✅ Clear logging of merge progress
- ✅ Single file in Plex instead of dozens
- ✅ Proper chapter markers in audiobook players
Implementation Phases
Phase 1: Core Functionality (MVP)
- Implement
chapter-merger.tsutility - Detection logic (chapter file patterns)
- Natural sorting algorithm
- Duration extraction (ffprobe)
- Chapter metadata generation (FFMETADATA1)
- M4A/M4B merge (codec copy mode)
- Integration with file-organizer.ts
- Configuration keys in database
Phase 2: MP3 Support
- MP3 → M4B conversion logic
- Quality preservation settings
- Bitrate configuration UI
Phase 3: UI & Polish
- Setup wizard integration
- Admin settings UI (Paths tab)
- Progress logging improvements
- Error messaging UX
Phase 4: Advanced Features (Future)
- Custom chapter naming from file metadata
- Chapter art extraction (if embedded in files)
- Preview merged file before finalizing
- Manual chapter editing UI
Related Documentation
- File Organization - File copying and tagging
- Metadata Tagging - Current tagging system
- Background Jobs - Job processing system
- Configuration - Settings management
Open Questions
- Chapter naming strategy: Should we try to extract from embedded metadata first, or always use filename?
- MP3 default behavior: Should MP3 merging be opt-in separately (slower, lossy)?
- Parallel processing: Merge multiple books at once, or serialize?
- Preview mode: Let users review chapter detection before merge?
- Retry logic: Auto-retry failed merges with different settings?
References
- FFmpeg concat demuxer: https://trac.ffmpeg.org/wiki/Concatenate
- FFmpeg metadata: https://ffmpeg.org/ffmpeg-formats.html#Metadata-1
- M4B format spec: ISO/IEC 14496-12 (MPEG-4 Part 12)
- Natural sorting: https://en.wikipedia.org/wiki/Natural_sort_order