Debugging Audiobookshelf: The Case of the Missing Folder-Based Audiobooks
November 25, 2025
In my recent update to Abookio, I encountered a perplexing issue with the Audiobookshelf integration: audiobooks organized in folders were simply vanishing from the import list. While single-file audiobooks (like individual M4B files) appeared perfectly, those structured as folders with multiple audio files were nowhere to be found.
Here’s a deep dive into how I tracked down and squashed this bug.
The Symptoms
Users reported that while their Audiobookshelf library connection was successful, a significant portion of their library was missing from the import screen. The common denominator? The missing items were all “folder-based” audiobooks—typically multi-file MP3 albums organized into directories.
The Investigation
My initial hypothesis was that the app was filtering out these items intentionally. I looked at the fetchItemsWithPagination method, which is responsible for retrieving the library content.
I found this validation logic:
// Old validation logic
if item.media.audioFiles == nil || item.media.audioFiles!.isEmpty {
continue // Skip items without audio files
}
This seemed reasonable. If an item has no audio files, it’s probably not an audiobook I can import, right?
Wrong.
By inspecting the raw JSON response from the Audiobookshelf API, I discovered a subtle difference between single-file items and folder-based items in the list view response:
- Single-file items: The API returns a populated
media.audioFilesarray and top-levelsizeanddurationstats. - Folder-based items: The API often returns an empty or nil
media.audioFilesarray in the list response to save bandwidth. Furthermore, the top-levelmedia.sizewas often0andmedia.durationwasnull.
Because the app relied on these top-level stats for validation, these valid audiobooks were being aggressively filtered out as “empty” or “invalid.”
The Fix
The solution required a two-pronged approach: smarter fetching and dynamic calculation.
1. Smarter Fetching
I modified the iteration loop to detect these “incomplete” folder items. Instead of skipping them, I now recognize them as potential candidates that need more information.
// Check if this is a folder-based audiobook with missing audio files
if !item.isFile && (item.media.audioFiles == nil || item.media.audioFiles!.isEmpty) {
// Fetch full details to get the file list
if let fullItem = await fetchFullItemDetails(itemId: item.id) {
itemToUse = fullItem
}
}
By fetching the full item details for these specific cases, I retrieve the libraryFiles array, which contains the actual audio files for folder-based items.
2. Dynamic Calculation
Even with the full details, the top-level size and duration metadata were sometimes still missing or zero. To ensure these books appeared with correct metadata in the UI, I implemented a fallback calculation strategy.
I updated the convertToCloudAudiobook method to sum up the stats from the individual files if the top-level metadata is missing:
// Calculate total size if missing
if totalSize == 0 {
if let itemAudioFiles = item.media.audioFiles, !itemAudioFiles.isEmpty {
totalSize = itemAudioFiles.reduce(0) { $0 + $1.metadata.size }
} else if let libraryFiles = item.libraryFiles, !libraryFiles.isEmpty {
// Fallback to libraryFiles for folder items
let audioLibraryFiles = libraryFiles.filter { $0.fileType == "audio" }
totalSize = audioLibraryFiles.reduce(0) { $0 + $1.metadata.size }
}
}
// Calculate total duration if missing
if totalDuration == nil || totalDuration == 0 {
// Sum up duration from individual audio files
// ... (similar reduction logic)
}
The Result
With these changes, Abookio now correctly handles the nuances of Audiobookshelf’s API. Folder-based audiobooks are detected, their full file lists are retrieved, and their size and duration are accurately calculated on the fly.
The “missing” books are back, and the import list now accurately reflects the user’s entire library, regardless of how the files are organized on the server.