mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
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:
@@ -1771,7 +1771,7 @@ export function AudiobookshelfStep({
|
|||||||
className="w-full px-3 py-2 border rounded-lg"
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1147,11 +1147,11 @@ auth.require_admin_approval = 'false'
|
|||||||
|
|
||||||
**Instructions for users:**
|
**Instructions for users:**
|
||||||
1. Login to Audiobookshelf web UI as admin
|
1. Login to Audiobookshelf web UI as admin
|
||||||
2. Go to Settings → Users
|
2. Go to Settings → API Keys
|
||||||
3. Click on your user
|
3. Click "Add API Key"
|
||||||
4. Scroll to "API Token" section
|
4. Enter a descriptive name (e.g., "ReadMeABook")
|
||||||
5. Click "Generate Token"
|
5. Copy the generated API key
|
||||||
6. Copy token for ReadMeABook setup
|
6. Use the key in ReadMeABook setup
|
||||||
|
|
||||||
### B. OIDC Provider Setup Guides
|
### B. OIDC Provider Setup Guides
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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:**
|
**Critical Fixes:**
|
||||||
1. ✅ **Fixed corruption on long audiobooks** - Dynamic timeout calculation (16h book = 254min vs old 20min)
|
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 Metadata Generation
|
||||||
|
|
||||||
**Chapter Naming Strategy:**
|
**Chapter Naming Strategy (Updated v3):**
|
||||||
1. **From filename:** Extract "Chapter 1", "01", "Part 1"
|
|
||||||
2. **Fallback numbering:** "Chapter 1", "Chapter 2" if no name found
|
**Priority Order:**
|
||||||
3. **Preserve order:** Sort files naturally (ch1, ch2, ch10)
|
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:**
|
**Chapter Timing:**
|
||||||
- Calculate from individual file durations using ffprobe
|
- Calculate from individual file durations using ffprobe
|
||||||
@@ -206,6 +236,7 @@ All chapter merging decisions are **fully logged** for user transparency:
|
|||||||
|
|
||||||
**Analysis Phase Logs:**
|
**Analysis Phase Logs:**
|
||||||
- Sample filenames for debugging
|
- Sample filenames for debugging
|
||||||
|
- Book title detection in metadata (NEW v3)
|
||||||
- Metadata availability (track numbers)
|
- Metadata availability (track numbers)
|
||||||
- Ordering strategy chosen (metadata vs filename)
|
- Ordering strategy chosen (metadata vs filename)
|
||||||
- Sample chapter titles generated
|
- Sample chapter titles generated
|
||||||
@@ -220,23 +251,25 @@ All chapter merging decisions are **fully logged** for user transparency:
|
|||||||
- Final file size and chapter count
|
- Final file size and chapter count
|
||||||
- Cleanup status
|
- 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 merging enabled - analyzing files...
|
||||||
[FileOrganizer] Chapter detection: 30 files with format .mp3 - attempting chapter merge
|
[FileOrganizer] Chapter detection: 31 files with format .mp3 - attempting chapter merge
|
||||||
[FileOrganizer] Analyzing 30 chapter files...
|
[FileOrganizer] Analyzing 31 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] Sample filenames: The Let Them Theory - 01 - Opening Credits.mp3, The Let Them Theory - 02 - Introduction_ My Story.mp3, ...
|
||||||
[FileOrganizer] Metadata analysis: 30/30 files have track numbers
|
[FileOrganizer] Detected book title in metadata: "The Let Them Theory" (appears in 31/31 files)
|
||||||
[FileOrganizer] Track numbers: 1, 2, 3 ... 30
|
[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] Chapter ordering: Filename and metadata orders match - high confidence
|
||||||
[FileOrganizer] Using metadata-based ordering for 30 chapters
|
[FileOrganizer] Using metadata-based ordering for 31 chapters
|
||||||
[FileOrganizer] Sample chapter titles: Ch1: "Chapter 1", Ch2: "Chapter 2", Ch3: "Chapter 3", ...
|
[FileOrganizer] Sample chapter titles: Ch1: "Opening Credits", Ch2: "Introduction: My Story", Ch3: "Dedication", ...
|
||||||
[FileOrganizer] Starting chapter merge: "Project Hail Mary" by Andy Weir
|
[FileOrganizer] Starting chapter merge: "The Let Them Theory" by Mel Robbins
|
||||||
[FileOrganizer] Merge strategy: Re-encoding MP3 → AAC/M4B at 128k
|
[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] ✓ Chapter merge successful!
|
||||||
[FileOrganizer] - Chapters: 30
|
[FileOrganizer] - Chapters: 31
|
||||||
[FileOrganizer] - Duration: 16h 32m 10s
|
[FileOrganizer] - Duration: 16h 32m 10s
|
||||||
[FileOrganizer] - Size: 452MB
|
[FileOrganizer] - Size: 452MB
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ async function setupABSConfig() {
|
|||||||
// Configure these values for your Audiobookshelf instance
|
// Configure these values for your Audiobookshelf instance
|
||||||
const config = {
|
const config = {
|
||||||
'audiobookshelf.server_url': 'http://localhost:13378', // Change to your ABS server URL
|
'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
|
'audiobookshelf.library_id': 'YOUR_LIBRARY_ID', // Get from ABS or use test-abs endpoint
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1340,7 +1340,7 @@ export default function AdminSettings() {
|
|||||||
placeholder="Enter your Audiobookshelf API token"
|
placeholder="Enter your Audiobookshelf API token"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export function AudiobookshelfStep({
|
|||||||
onChange={(e) => onUpdate('absApiToken', e.target.value)}
|
onChange={(e) => onUpdate('absApiToken', e.target.value)}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -270,8 +270,8 @@ export function AudiobookshelfStep({
|
|||||||
About API Token
|
About API Token
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-blue-700 dark:text-blue-300 mt-1">
|
<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
|
Generate an API key in Audiobookshelf by navigating to Settings → API Keys → Add API Key.
|
||||||
→ selecting your user → and copying the API Token.
|
Give it a descriptive name and copy the generated key to use here.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function AudiobookDetailsModal({
|
|||||||
const [requestError, setRequestError] = useState<string | null>(null);
|
const [requestError, setRequestError] = useState<string | null>(null);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
const [showInteractiveSearch, setShowInteractiveSearch] = useState(false);
|
const [showInteractiveSearch, setShowInteractiveSearch] = useState(false);
|
||||||
|
const [asinCopied, setAsinCopied] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
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;
|
if (!isOpen || !mounted) return null;
|
||||||
|
|
||||||
const modalContent = (
|
const modalContent = (
|
||||||
@@ -291,6 +302,40 @@ export function AudiobookDetailsModal({
|
|||||||
</div>
|
</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 */}
|
{/* Availability Status */}
|
||||||
{isAvailable && (
|
{isAvailable && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface ChapterFile {
|
|||||||
bitrate?: number; // kbps
|
bitrate?: number; // kbps
|
||||||
trackNumber?: number; // from metadata
|
trackNumber?: number; // from metadata
|
||||||
titleMetadata?: string; // from metadata
|
titleMetadata?: string; // from metadata
|
||||||
|
titleIsBookTitle?: boolean; // true if titleMetadata is the book title (not chapter-specific)
|
||||||
chapterTitle: string; // final computed title
|
chapterTitle: string; // final computed title
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +197,9 @@ function extractChapterNameFromFilename(filename: string): string | null {
|
|||||||
// Try to extract meaningful name after chapter indicator
|
// Try to extract meaningful name after chapter indicator
|
||||||
// "01 - The Beginning" -> "The Beginning"
|
// "01 - The Beginning" -> "The Beginning"
|
||||||
// "Chapter 1 - Introduction" -> "Introduction"
|
// "Chapter 1 - Introduction" -> "Introduction"
|
||||||
|
// "Book Title - 01 - Chapter Name" -> "Chapter Name"
|
||||||
const patterns = [
|
const patterns = [
|
||||||
|
/[\s._-]+\d+[\s._-]+(.+)$/, // "BookTitle - 01 - ChapterName" (extract after last digit sequence)
|
||||||
/^\d+[\s._-]+(.+)$/, // "01 - Title" or "01_Title"
|
/^\d+[\s._-]+(.+)$/, // "01 - Title" or "01_Title"
|
||||||
/^chapter\s*\d+[\s._-]+(.+)$/i, // "Chapter 1 - Title"
|
/^chapter\s*\d+[\s._-]+(.+)$/i, // "Chapter 1 - Title"
|
||||||
/^ch\s*\d+[\s._-]+(.+)$/i, // "Ch1 - 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 {
|
function getChapterTitle(file: ChapterFile, index: number): string {
|
||||||
// Priority 1: Title metadata (if meaningful)
|
// Priority 1: Extract from filename (most reliable for chapter-specific names)
|
||||||
if (file.titleMetadata && !isGenericTitle(file.titleMetadata)) {
|
|
||||||
return file.titleMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: Extract from filename
|
|
||||||
const extracted = extractChapterNameFromFilename(file.filename);
|
const extracted = extractChapterNameFromFilename(file.filename);
|
||||||
if (extracted) {
|
if (extracted) {
|
||||||
return 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"
|
// Priority 3: Fallback to "Chapter X"
|
||||||
return `Chapter ${index + 1}`;
|
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
|
* Analyze and order chapter files
|
||||||
* Returns files in correct order with metadata populated
|
* Returns files in correct order with metadata populated
|
||||||
@@ -255,6 +299,7 @@ export async function analyzeChapterFiles(
|
|||||||
bitrate: probe.bitrate,
|
bitrate: probe.bitrate,
|
||||||
trackNumber: probe.trackNumber,
|
trackNumber: probe.trackNumber,
|
||||||
titleMetadata: probe.title,
|
titleMetadata: probe.title,
|
||||||
|
titleIsBookTitle: false, // Will be updated if book title detected
|
||||||
chapterTitle: '', // Will be computed after ordering
|
chapterTitle: '', // Will be computed after ordering
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -266,6 +311,22 @@ export async function analyzeChapterFiles(
|
|||||||
const sampleFilenames = files.slice(0, sampleCount).map(f => f.filename);
|
const sampleFilenames = files.slice(0, sampleCount).map(f => f.filename);
|
||||||
await logger?.info(`Sample filenames: ${sampleFilenames.join(', ')}${files.length > sampleCount ? ', ...' : ''}`);
|
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)
|
// Create filename-based order (natural sort)
|
||||||
const filenameOrder = [...files].sort((a, b) =>
|
const filenameOrder = [...files].sort((a, b) =>
|
||||||
naturalSortCompare(a.filename, b.filename)
|
naturalSortCompare(a.filename, b.filename)
|
||||||
|
|||||||
Reference in New Issue
Block a user