Update Audiobookshelf API key instructions and improve chapter merging

Replaces outdated Audiobookshelf API token instructions with new API key generation steps across documentation and UI. Enhances chapter merging logic to detect and handle book titles in metadata, prioritizing filename extraction for chapter names, and updates logging and documentation to reflect these changes. Adds ASIN copy-to-clipboard feature in AudiobookDetailsModal.
This commit is contained in:
kikootwo
2026-01-14 12:17:41 -05:00
parent 307b63fab4
commit b3f89d67bb
8 changed files with 175 additions and 36 deletions
@@ -1771,7 +1771,7 @@ export function AudiobookshelfStep({
className="w-full px-3 py-2 border rounded-lg"
/>
<p className="text-xs text-gray-500 mt-1">
Find this in Audiobookshelf Settings Users Your User API Token
Generate this in Audiobookshelf Settings API Keys Add API Key
</p>
</div>
@@ -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
+51 -18
View File
@@ -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
```
+1 -1
View File
@@ -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
};
+1 -1
View File
@@ -1340,7 +1340,7 @@ export default function AdminSettings() {
placeholder="Enter your Audiobookshelf API token"
/>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Found in Audiobookshelf Settings Users Your Account API Tokens
Generate in Audiobookshelf: Settings API Keys Add API Key
</p>
</div>
+3 -3
View File
@@ -136,7 +136,7 @@ export function AudiobookshelfStep({
onChange={(e) => onUpdate('absApiToken', e.target.value)}
/>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Find this in Audiobookshelf Settings Users Your User API Token
Generate this in Audiobookshelf Settings API Keys Add API Key
</p>
</div>
@@ -270,8 +270,8 @@ export function AudiobookshelfStep({
About API Token
</p>
<p className="text-sm text-blue-700 dark:text-blue-300 mt-1">
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.
</p>
</div>
</div>
@@ -43,6 +43,7 @@ export function AudiobookDetailsModal({
const [requestError, setRequestError] = useState<string | null>(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({
</div>
)}
{/* ASIN */}
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">ASIN</p>
<button
onClick={handleCopyAsin}
className="group flex items-center gap-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
title="Click to copy ASIN"
>
<span className="font-mono text-sm">{asin}</span>
<svg
className="w-4 h-4 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{asinCopied ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
)}
</svg>
</button>
</div>
{/* Availability Status */}
{isAvailable && (
<div>
+68 -7
View File
@@ -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<string, number>();
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)