Pairing an M4B shows one chapter instead of its embedded chapters when the moov atom is at the end of the file
Bug Description
When I pair a local M4B audiobook, Readest lists the whole file as a single audio chapter, even though the file has a valid chapter list. This happens when the file's index (the moov box) comes after the audio data (mdat), a layout often called "moov at end" or non-faststart. ffmpeg writes files this way unless -movflags +faststart is passed, so it is common among converted audiobooks.
The chapter parsing runs in the shared web code, so this should affect every platform.
Cause
parseAudiobookFile takes chapters only from music-metadata's parseBlob(file, { includeChapters: true }). If none come back, it falls back to one chapter covering the whole file.
music-metadata parses in a single forward pass. It extracts QuickTime chapter titles only inside its mdat handler, and only if it has already parsed the tracks in moov. When moov comes after mdat, the handler has no tracks yet, so it skips the audio and never goes back. No error is raised. Running music-metadata 11.14.0 on my file returns the correct duration (48,693 s) and format.chapters: [].
This is tracked upstream as Borewit/music-metadata#2510, open since September 2025. The maintainer confirmed the cause there and called it "certainly not a trivial fix". The latest release, 11.15.0, has the same logic, so updating the dependency won't help.
File details
- Top-level layout:
ftyp→mdat(764 MB) →moov(9.4 MB) - 50 chapters stored as a QuickTime chapter track (
tref/chappointing to a text track), covering the full 13 h 31 m - No Nero
chplatom
Proposed fix
Keep music-metadata as the primary parser. When it returns no chapters, fall back to a small MP4 chapter reader in services/audiobook/ that:
- walks the top-level box headers with
Blob.slice()to findmoovwherever it is, and returns immediately if the file doesn't start withftyp - follows the
tref/chapreference to the chapter text track and reads only the title samples
On the file above, this finds all 50 chapters in about 20 ms and reads about 9.4 MB (the moov box plus the titles) out of 774 MB. NativeFile.slice() does ranged reads, so the audio is never streamed. If the fallback throws, the current single-chapter behaviour is kept.
The same reader could also read Nero chapters (moov/udta/chpl). music-metadata 11.15.0 has no chpl handling, even though its request for it (Borewit/music-metadata#800) is closed. I can include that or leave it out.
I have this implemented with unit tests covering moov at end, moov first, Nero chapters, and non-MP4 input. I can open a PR if the approach sounds good.
Steps to Reproduce
- Open an EPUB that has a table of contents.
- In the sidebar, open the book menu and choose Pair Audiobook, then Select Audio Files.
- Pick an M4B that has a QuickTime chapter track and whose
moovatom comes aftermdat. Bento4'smp4dump, for example, shows the order of the top-level atoms. - Look at the audio chapter list in the next step.
Result: one chapter, named after the file's title tag, spanning the whole 13 h 31 m.
Expected Behavior
The audio chapter list shows the 50 chapters embedded in the file, the same as for an M4B whose moov comes before mdat.
Reproduction File
The file is a personal Audible purchase, so I can't share it. Its structure is described above, and any chaptered M4B with moov after mdat should reproduce the bug.
Operating System
iOS/iPadOS
OS Version
iOS 27.0
Readest Version
0.12.8
Custom CSS
No response
Source: readest/readest