diff --git a/documentation/features/audiobookshelf-implementation-guide.md b/documentation/features/audiobookshelf-implementation-guide.md index 183ea35..cbf5fa3 100644 --- a/documentation/features/audiobookshelf-implementation-guide.md +++ b/documentation/features/audiobookshelf-implementation-guide.md @@ -1771,7 +1771,7 @@ export function AudiobookshelfStep({ className="w-full px-3 py-2 border rounded-lg" />

- Find this in Audiobookshelf → Settings → Users → Your User → API Token + Generate this in Audiobookshelf → Settings → API Keys → Add API Key

diff --git a/documentation/features/audiobookshelf-integration.md b/documentation/features/audiobookshelf-integration.md index b9ae0a7..7a3c14e 100644 --- a/documentation/features/audiobookshelf-integration.md +++ b/documentation/features/audiobookshelf-integration.md @@ -1147,11 +1147,11 @@ auth.require_admin_approval = 'false' **Instructions for users:** 1. Login to Audiobookshelf web UI as admin -2. Go to Settings → Users -3. Click on your user -4. Scroll to "API Token" section -5. Click "Generate Token" -6. Copy token for ReadMeABook setup +2. Go to Settings → API Keys +3. Click "Add API Key" +4. Enter a descriptive name (e.g., "ReadMeABook") +5. Copy the generated API key +6. Use the key in ReadMeABook setup ### B. OIDC Provider Setup Guides diff --git a/documentation/features/chapter-merging.md b/documentation/features/chapter-merging.md index 58a25b9..19d9443 100644 --- a/documentation/features/chapter-merging.md +++ b/documentation/features/chapter-merging.md @@ -6,9 +6,26 @@ Automatically merge multi-file audiobook downloads (separate MP3/M4A files per chapter) into a single M4B file with proper chapter markers during file organization. -## Recent Updates (v2 - Corruption Fixes) +## Recent Updates -**Status:** ✅ Implemented (2026-01-09) +### v3 - Book Title Detection (2026-01-14) + +**Status:** ✅ Implemented + +**Critical Fix:** +- ✅ **Fixed identical chapter names bug** - Detects when title metadata contains book title instead of chapter names +- ✅ **Smart book title detection** - Analyzes files; if >80% have same title, flags it as book title +- ✅ **Updated filename patterns** - Added support for "BookTitle - 01 - ChapterName" format +- ✅ **Revised priority logic** - Prioritizes filename extraction over metadata when book title detected +- ✅ **Enhanced logging** - Reports book title detection and filename extraction strategy + +**Impact:** +- Before: Files with book title in metadata → All chapters named "The Let Them Theory" +- After: Filename extraction prioritized → Ch1: "Opening Credits", Ch2: "Introduction: My Story", etc. + +### v2 - Corruption Fixes (2026-01-09) + +**Status:** ✅ Implemented **Critical Fixes:** 1. ✅ **Fixed corruption on long audiobooks** - Dynamic timeout calculation (16h book = 254min vs old 20min) @@ -74,10 +91,23 @@ Detection now uses a **permissive heuristic** instead of strict filename pattern ### 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 Naming Strategy (Updated v3):** + +**Priority Order:** +1. **From filename:** Extract chapter name from filename patterns (most reliable) + - "01 - The Beginning" → "The Beginning" + - "Chapter 1 - Introduction" → "Introduction" + - "BookTitle - 01 - ChapterName" → "ChapterName" (NEW: supports book title prefix) +2. **From metadata:** Use embedded title tag (only if chapter-specific) + - Automatically detects if title metadata is the book title (appears in >80% of files) + - Skips metadata that matches the book title to avoid "every chapter named the same" +3. **Fallback numbering:** "Chapter 1", "Chapter 2" if no name found + +**Book Title Detection (NEW):** +- Analyzes all files to detect if title metadata contains the book title instead of chapter names +- If >80% of files have identical title metadata, flags it as the book title +- Prioritizes filename extraction when book title is detected in metadata +- Logs detection: "Detected book title in metadata: [title] (appears in X/Y files)" **Chapter Timing:** - Calculate from individual file durations using ffprobe @@ -206,6 +236,7 @@ All chapter merging decisions are **fully logged** for user transparency: **Analysis Phase Logs:** - Sample filenames for debugging +- Book title detection in metadata (NEW v3) - Metadata availability (track numbers) - Ordering strategy chosen (metadata vs filename) - Sample chapter titles generated @@ -220,23 +251,25 @@ All chapter merging decisions are **fully logged** for user transparency: - Final file size and chapter count - Cleanup status -**Example Log Output:** +**Example Log Output (v3 with book title detection):** ``` -[FileOrganizer] Multiple audio files detected (30 files) - checking chapter merge settings... +[FileOrganizer] Multiple audio files detected (31 files) - checking chapter merge settings... [FileOrganizer] Chapter merging enabled - analyzing files... -[FileOrganizer] Chapter detection: 30 files with format .mp3 - attempting chapter merge -[FileOrganizer] Analyzing 30 chapter files... -[FileOrganizer] Sample filenames: Andy Weir - Project Hail Mary - 01.mp3, Andy Weir - Project Hail Mary - 02.mp3, Andy Weir - Project Hail Mary - 03.mp3, ... -[FileOrganizer] Metadata analysis: 30/30 files have track numbers -[FileOrganizer] Track numbers: 1, 2, 3 ... 30 +[FileOrganizer] Chapter detection: 31 files with format .mp3 - attempting chapter merge +[FileOrganizer] Analyzing 31 chapter files... +[FileOrganizer] Sample filenames: The Let Them Theory - 01 - Opening Credits.mp3, The Let Them Theory - 02 - Introduction_ My Story.mp3, ... +[FileOrganizer] Detected book title in metadata: "The Let Them Theory" (appears in 31/31 files) +[FileOrganizer] Title metadata flagged as book title - will prioritize filename extraction for chapter names +[FileOrganizer] Metadata analysis: 31/31 files have track numbers +[FileOrganizer] Track numbers: 1, 2, 3 ... 31 [FileOrganizer] Chapter ordering: Filename and metadata orders match - high confidence -[FileOrganizer] Using metadata-based ordering for 30 chapters -[FileOrganizer] Sample chapter titles: Ch1: "Chapter 1", Ch2: "Chapter 2", Ch3: "Chapter 3", ... -[FileOrganizer] Starting chapter merge: "Project Hail Mary" by Andy Weir +[FileOrganizer] Using metadata-based ordering for 31 chapters +[FileOrganizer] Sample chapter titles: Ch1: "Opening Credits", Ch2: "Introduction: My Story", Ch3: "Dedication", ... +[FileOrganizer] Starting chapter merge: "The Let Them Theory" by Mel Robbins [FileOrganizer] Merge strategy: Re-encoding MP3 → AAC/M4B at 128k -[FileOrganizer] Executing FFmpeg merge (timeout: 20 minutes)... +[FileOrganizer] Executing FFmpeg merge (timeout: 254 minutes)... [FileOrganizer] ✓ Chapter merge successful! -[FileOrganizer] - Chapters: 30 +[FileOrganizer] - Chapters: 31 [FileOrganizer] - Duration: 16h 32m 10s [FileOrganizer] - Size: 452MB ``` diff --git a/scripts/setup-abs-config.ts b/scripts/setup-abs-config.ts index 17a4f1e..605075e 100644 --- a/scripts/setup-abs-config.ts +++ b/scripts/setup-abs-config.ts @@ -9,7 +9,7 @@ async function setupABSConfig() { // Configure these values for your Audiobookshelf instance const config = { 'audiobookshelf.server_url': 'http://localhost:13378', // Change to your ABS server URL - 'audiobookshelf.api_token': 'YOUR_ABS_API_TOKEN', // Get from ABS Settings -> Users -> Your User -> API Token + 'audiobookshelf.api_token': 'YOUR_ABS_API_TOKEN', // Generate from ABS Settings -> API Keys -> Add API Key 'audiobookshelf.library_id': 'YOUR_LIBRARY_ID', // Get from ABS or use test-abs endpoint }; diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index 7913d17..f5ef370 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -1340,7 +1340,7 @@ export default function AdminSettings() { placeholder="Enter your Audiobookshelf API token" />

- Found in Audiobookshelf Settings → Users → Your Account → API Tokens + Generate in Audiobookshelf: Settings → API Keys → Add API Key

diff --git a/src/app/setup/steps/AudiobookshelfStep.tsx b/src/app/setup/steps/AudiobookshelfStep.tsx index 23464ae..4573517 100644 --- a/src/app/setup/steps/AudiobookshelfStep.tsx +++ b/src/app/setup/steps/AudiobookshelfStep.tsx @@ -136,7 +136,7 @@ export function AudiobookshelfStep({ onChange={(e) => onUpdate('absApiToken', e.target.value)} />

- Find this in Audiobookshelf → Settings → Users → Your User → API Token + Generate this in Audiobookshelf → Settings → API Keys → Add API Key

@@ -270,8 +270,8 @@ export function AudiobookshelfStep({ About API Token

- You can generate an API token in Audiobookshelf by going to Settings → Users - → selecting your user → and copying the API Token. + Generate an API key in Audiobookshelf by navigating to Settings → API Keys → Add API Key. + Give it a descriptive name and copy the generated key to use here.

diff --git a/src/components/audiobooks/AudiobookDetailsModal.tsx b/src/components/audiobooks/AudiobookDetailsModal.tsx index 4964355..fced29c 100644 --- a/src/components/audiobooks/AudiobookDetailsModal.tsx +++ b/src/components/audiobooks/AudiobookDetailsModal.tsx @@ -43,6 +43,7 @@ export function AudiobookDetailsModal({ const [requestError, setRequestError] = useState(null); const [mounted, setMounted] = useState(false); const [showInteractiveSearch, setShowInteractiveSearch] = useState(false); + const [asinCopied, setAsinCopied] = useState(false); useEffect(() => { setMounted(true); @@ -123,6 +124,16 @@ export function AudiobookDetailsModal({ } }; + const handleCopyAsin = async () => { + try { + await navigator.clipboard.writeText(asin); + setAsinCopied(true); + setTimeout(() => setAsinCopied(false), 2000); + } catch (err) { + console.error('Failed to copy ASIN:', err); + } + }; + if (!isOpen || !mounted) return null; const modalContent = ( @@ -291,6 +302,40 @@ export function AudiobookDetailsModal({ )} + {/* ASIN */} +
+

ASIN

+ +
+ {/* Availability Status */} {isAvailable && (
diff --git a/src/lib/utils/chapter-merger.ts b/src/lib/utils/chapter-merger.ts index b2936c9..9039800 100644 --- a/src/lib/utils/chapter-merger.ts +++ b/src/lib/utils/chapter-merger.ts @@ -42,6 +42,7 @@ export interface ChapterFile { bitrate?: number; // kbps trackNumber?: number; // from metadata titleMetadata?: string; // from metadata + titleIsBookTitle?: boolean; // true if titleMetadata is the book title (not chapter-specific) chapterTitle: string; // final computed title } @@ -196,7 +197,9 @@ function extractChapterNameFromFilename(filename: string): string | null { // Try to extract meaningful name after chapter indicator // "01 - The Beginning" -> "The Beginning" // "Chapter 1 - Introduction" -> "Introduction" + // "Book Title - 01 - Chapter Name" -> "Chapter Name" const patterns = [ + /[\s._-]+\d+[\s._-]+(.+)$/, // "BookTitle - 01 - ChapterName" (extract after last digit sequence) /^\d+[\s._-]+(.+)$/, // "01 - Title" or "01_Title" /^chapter\s*\d+[\s._-]+(.+)$/i, // "Chapter 1 - Title" /^ch\s*\d+[\s._-]+(.+)$/i, // "Ch1 - Title" @@ -217,24 +220,65 @@ function extractChapterNameFromFilename(filename: string): string | null { } /** - * Get chapter title with priority: metadata > filename > fallback + * Get chapter title with priority: filename > metadata (if not book title) > fallback */ function getChapterTitle(file: ChapterFile, index: number): string { - // Priority 1: Title metadata (if meaningful) - if (file.titleMetadata && !isGenericTitle(file.titleMetadata)) { - return file.titleMetadata; - } - - // Priority 2: Extract from filename + // Priority 1: Extract from filename (most reliable for chapter-specific names) const extracted = extractChapterNameFromFilename(file.filename); if (extracted) { return extracted; } + // Priority 2: Title metadata (only if meaningful AND not the book title) + if (file.titleMetadata && !file.titleIsBookTitle && !isGenericTitle(file.titleMetadata)) { + return file.titleMetadata; + } + // Priority 3: Fallback to "Chapter X" return `Chapter ${index + 1}`; } +/** + * Detect if a title appearing in metadata is the book title (not chapter-specific) + * Returns the book title if >80% of files have the same title metadata + */ +function detectBookTitle(files: { titleMetadata?: string }[]): string | null { + if (files.length === 0) return null; + + // Count occurrences of each title + const titleCounts = new Map(); + let filesWithTitle = 0; + + for (const file of files) { + if (file.titleMetadata && file.titleMetadata.trim().length > 0) { + const title = file.titleMetadata.trim(); + titleCounts.set(title, (titleCounts.get(title) || 0) + 1); + filesWithTitle++; + } + } + + if (filesWithTitle === 0) return null; + + // Find most common title + let mostCommonTitle: string | null = null; + let maxCount = 0; + + for (const [title, count] of titleCounts.entries()) { + if (count > maxCount) { + maxCount = count; + mostCommonTitle = title; + } + } + + // If >80% of files have the same title, it's likely the book title + const threshold = files.length * 0.8; + if (mostCommonTitle && maxCount >= threshold) { + return mostCommonTitle; + } + + return null; +} + /** * Analyze and order chapter files * Returns files in correct order with metadata populated @@ -255,6 +299,7 @@ export async function analyzeChapterFiles( bitrate: probe.bitrate, trackNumber: probe.trackNumber, titleMetadata: probe.title, + titleIsBookTitle: false, // Will be updated if book title detected chapterTitle: '', // Will be computed after ordering }; }); @@ -266,6 +311,22 @@ export async function analyzeChapterFiles( const sampleFilenames = files.slice(0, sampleCount).map(f => f.filename); await logger?.info(`Sample filenames: ${sampleFilenames.join(', ')}${files.length > sampleCount ? ', ...' : ''}`); + // Detect if title metadata is actually the book title (not chapter-specific) + const bookTitle = detectBookTitle(files); + if (bookTitle) { + const filesWithBookTitle = files.filter(f => f.titleMetadata?.trim() === bookTitle).length; + await logger?.info(`Detected book title in metadata: "${bookTitle}" (appears in ${filesWithBookTitle}/${files.length} files)`); + + // Flag all files that have the book title as metadata + for (const file of files) { + if (file.titleMetadata?.trim() === bookTitle) { + file.titleIsBookTitle = true; + } + } + + await logger?.info(`Title metadata flagged as book title - will prioritize filename extraction for chapter names`); + } + // Create filename-based order (natural sort) const filenameOrder = [...files].sort((a, b) => naturalSortCompare(a.filename, b.filename)