Files
ReadMeABook/documentation/features/chapter-merging.md
T
kikootwo 288421012d Implement chapter merging feature and update ranking algorithm
Added automatic chapter merging to M4B with admin/config toggles, UI controls, and backend logic. Updated documentation to reflect implementation. Refactored ranking algorithm: increased Title/Author match points, removed size scoring, and improved Usenet/torrent handling. Enhanced Prowlarr integration for protocol detection and filtering. Improved file organizer to support chapter merging. Various bug fixes and logging improvements.
2026-01-28 11:41:58 -05:00

422 lines
12 KiB
Markdown

# Chapter Merging Feature
**Status:** ✅ Implemented | Auto-merge multi-file chapters to M4B
## 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:**
1. **From filename:** Extract "Chapter 1", "01", "Part 1"
2. **Fallback numbering:** "Chapter 1", "Chapter 2" if no name found
3. **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):**
```bash
# 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):**
```bash
# 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
1. Download completes: 25 chapter MP3 files
2. File organization starts
3. System detects chapter pattern
4. 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)"
5. Copies merged M4B to target directory
6. Deletes temp files and originals (if configured)
7. Plex scans single M4B with full chapter navigation
### Fallback Flow
**If merge fails:**
1. Log error: "Chapter merge failed: [reason]"
2. Fall back to current behavior: copy individual files
3. Mark request as "available" (not failed)
4. 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:**
```typescript
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:**
```typescript
// 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
1. **M4A chapter files (20 files)**
- Verify merge succeeds
- Verify chapter count matches file count
- Verify metadata preserved
- Verify chapter navigation works in Plex
2. **MP3 chapter files (15 files)**
- Verify conversion to M4B
- Verify audio quality (bitrate ~128kbps)
- Verify no audio glitches at chapter boundaries
3. **Mixed formats**
- Verify merge skipped
- Verify fallback to individual files
4. **Failed merge**
- Verify fallback behavior
- Verify original files preserved
- Verify request marked available (not failed)
5. **Chapter naming**
- "Ch1.mp3" → "Chapter 1"
- "001 - Introduction.mp3" → "Introduction"
- "Part 1.mp3" → "Part 1"
6. **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.ts` utility
- [ ] 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](../phase3/file-organization.md) - File copying and tagging
- [Metadata Tagging](../phase3/file-organization.md#metadata-tagging) - Current tagging system
- [Background Jobs](../backend/services/jobs.md) - Job processing system
- [Configuration](../backend/services/config.md) - Settings management
## Open Questions
1. **Chapter naming strategy:** Should we try to extract from embedded metadata first, or always use filename?
2. **MP3 default behavior:** Should MP3 merging be opt-in separately (slower, lossy)?
3. **Parallel processing:** Merge multiple books at once, or serialize?
4. **Preview mode:** Let users review chapter detection before merge?
5. **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