Skip to content

Misleading "mutagen is not installed" warning: MP4Chapter does not exist in any released mutagen #195

Description

@cajones86

Version: abogen 1.3.1 (web UI) Platform: Windows 11, Python 3.12 mutagen: 1.48.1 (installed, as required by mutagen>=1.47.0)

Summary
Every m4b conversion logs this warning:

Unable to write MP4 chapter atoms because mutagen is not installed.
mutagen is installed. The ImportError being caught is not a missing package - it is a missing symbol. mutagen.mp4.MP4Chapter (singular) does not exist in any released version of mutagen, so _apply_m4b_chapters_with_mutagen() can never run, and the message points at the wrong cause.

The output audio is not affected - ffmpeg has already written the chapter data by this point (see "Impact" below). This is a diagnostics bug plus some dead code, not a corruption bug. But the warning is alarming for anyone checking whether their audiobook is properly chaptered.

Reproduce
$ python -c "from mutagen.mp4 import MP4, MP4Chapter"
ImportError: cannot import name 'MP4Chapter' from 'mutagen.mp4'

$ python -c "import mutagen.mp4 as m; print([n for n in dir(m) if 'hap' in n])"
['Chapter', 'MP4Chapters']
Convert anything to m4b and the warning appears in the job log.

Root cause
abogen/webui/conversion_runner.py, _apply_m4b_chapters_with_mutagen() (line 1365):

try:
    from fractions import Fraction
    from mutagen.mp4 import MP4, MP4Chapter  # line 1375 - MP4Chapter does not exist
except ImportError:
    job.add_log(
        "Unable to write MP4 chapter atoms because mutagen is not installed.",
        level="warning",
    )
    return False

The except ImportError handler assumes the only possible cause is a missing package, so a missing symbol is reported as a missing install.

Two further problems make this function unreachable even if the import were fixed, so it is effectively dead code:

mutagen's chapter support is read-only. MP4Chapters subclasses Sequence and only parses the moov.udta.chpl box. There is no documented API to create or modify chapters. (https://mutagen.readthedocs.io/en/latest/api/mp4.html)
MP4 has no settable .chapters attribute, so line 1420 (mp4.chapters = cast(Any, chapter_objects)) would attach an attribute that save() ignores.
So the code targets an API that mutagen does not provide - possibly a proposed or planned one (cf. quodlibet/mutagen#295).

Impact: none on output
Worth stating explicitly, since the warning implies otherwise. ffmpeg writes the full set of chapter structures before this function is reached, verified on a generated file:

$ ffprobe -v error -show_chapters test_book.m4b # 4 chapters, correct titles
$ # raw atom scan: chpl: True chap: True tref: True text: True
chpl - Nero-style chapter list
chap + tref - QuickTime chapter track reference
a timed-text track carrying the titles
That is everything players need, so the mutagen pass is redundant as well as broken.

Suggested fix
The cleanest fix is probably to drop _apply_m4b_chapters_with_mutagen() entirely along with its call site, since mutagen cannot write chapters and ffmpeg already covers this. That removes ~60 lines of unreachable code.

If you would rather keep it in place pending a future mutagen release that adds write support, the minimal change is to distinguish the two failure modes and stop crying wolf:

  • except ImportError:
  •    job.add_log(
    
  •        "Unable to write MP4 chapter atoms because mutagen is not installed.",
    
  •        level="warning",
    
  •    )
    
  •    return False
    
  • except ImportError as exc:
  •    try:
    
  •        import mutagen  # noqa: F401
    
  •    except ImportError:
    
  •        job.add_log(
    
  •            "Unable to write MP4 chapter atoms because mutagen is not installed.",
    
  •            level="warning",
    
  •        )
    
  •    else:
    
  •        # mutagen is installed but this release has no writable chapter API
    
  •        # (1.48 exposes read-only Chapter/MP4Chapters, not MP4Chapter).
    
  •        # ffmpeg has already written the chpl/chap atoms and the timed-text
    
  •        # chapter track, so the output is fully chaptered either way.
    
  •        job.add_log(
    
  •            "Skipping the mutagen chapter pass: installed mutagen "
    
  •            f"({getattr(mutagen, 'version_string', 'unknown')}) has no writable "
    
  •            f"chapter API ({exc}). Chapters written by ffmpeg are already in place.",
    
  •            level="debug",
    
  •        )
    
  •    return False
    

This keeps the genuine "mutagen missing" warning for the case it was written for, and demotes the expected case to debug with an accurate explanation.

I went with the second option locally (lower risk of touching working behaviour), but the first is likely the better upstream change - happy to open a PR for either.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions