#6262·readest

Pairing an M4B shows one chapter instead of its embedded chapters when the moov atom is at the end of the file

Author: knigamCreated Sep 18, 2026Updated Sep 18, 2026

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: ftypmdat (764 MB) → moov (9.4 MB)
  • 50 chapters stored as a QuickTime chapter track (tref/chap pointing to a text track), covering the full 13 h 31 m
  • No Nero chpl atom

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 find moov wherever it is, and returns immediately if the file doesn't start with ftyp
  • follows the tref/chap reference 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

  1. Open an EPUB that has a table of contents.
  2. In the sidebar, open the book menu and choose Pair Audiobook, then Select Audio Files.
  3. Pick an M4B that has a QuickTime chapter track and whose moov atom comes after mdat. Bento4's mp4dump, for example, shows the order of the top-level atoms.
  4. 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