mutagen icon indicating copy to clipboard operation
mutagen copied to clipboard

Chapter tags write sequence

Open ping opened this issue 4 years ago • 7 comments

I'm trying to use mutagen to write chapters info into an audiobook. Example code is below.

import logging
import shutil
import datetime
from mutagen.mp3 import MP3
from mutagen.id3 import (
    ID3, TIT2, CHAP, CTOC, CTOCFlags, Encoding,
)

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)

SOURCE_MP3 = 'example.mp3'
CHAPTERS_MP3 = 'example_with_chapters.mp3'


def run():
    chapters = [
        {
            "id": "ch01",
            "text": "ABCD",
            "start_time": 0,
            "end_time": 10000
        },
        {
            "id": "ch02",
            "text": "ABCDEF",
            "start_time": 10000,
            "end_time": 20000
        },
        {
            "id": "ch03",
            "text": "ABC",
            "start_time": 20000,
            "end_time": 30000
        },
        {
            "id": "ch04",
            "text": "A",
            "start_time": 30000,
            "end_time": 40000
        }
    ]

    # make a copy of the source file
    shutil.copyfile(SOURCE_MP3, CHAPTERS_MP3)

    # write chapter tags to mp3
    audio_book = MP3(CHAPTERS_MP3, ID3=ID3)
    for m in chapters:
        audio_book.tags.add(
            CHAP(element_id=m['id'], start_time=m['start_time'], end_time=m['end_time'],
                 sub_frames=[TIT2(encoding=Encoding.UTF8, text=[m['text']])]))

        start_time = datetime.timedelta(milliseconds=m['start_time'])
        end_time = datetime.timedelta(milliseconds=m['end_time'])
        logger.debug(
            'Added chap tag => "{}": {}-{} "{}"'.format(
                m['id'],
                start_time, end_time,
                m['text']),
        )
    audio_book.tags.add(
        CTOC(element_id='toc', flags=CTOCFlags.TOP_LEVEL | CTOCFlags.ORDERED,
             child_element_ids=[ch['id'] for ch in chapters],
             sub_frames=[TIT2(encoding=Encoding.UTF8, text=['Table of Contents'])]))
    logger.debug('CTOC child_element_ids: %s', [ch['id'] for ch in chapters])
    audio_book.save()


if __name__ == '__main__':
    run()

The log output looks fine.

DEBUG:__main__:Added chap tag => "ch01": 0:00:00-0:00:10 "ABCD"
DEBUG:__main__:Added chap tag => "ch02": 0:00:10-0:00:20 "ABCDEF"
DEBUG:__main__:Added chap tag => "ch03": 0:00:20-0:00:30 "ABC"
DEBUG:__main__:Added chap tag => "ch04": 0:00:30-0:00:40 "A"
DEBUG:__main__:CTOC child_element_ids: ['ch01', 'ch02', 'ch03', 'ch04']

A check with ffprobe ffprobe -hide_banner example_with_chapters.mp3 reveals

    Chapter #0:0: start 30.000000, end 40.000000
    Metadata:
      title           : A
    Chapter #0:1: start 20.000000, end 30.000000
    Metadata:
      title           : ABC
    Chapter #0:2: start 0.000000, end 10.000000
    Metadata:
      title           : ABCD
    Chapter #0:3: start 10.000000, end 20.000000
    Metadata:
      title           : ABCDEF

It looks like to me that the chapters tags are being written to file in sequence according to the length of the title text.

This causes an issue when I try to use ffmpeg to convert the mp3 into an m4b.

ffmpeg -i example_with_chapters.mp3 -map 0:a -c:a aac -b:a 64k example_with_chapters.m4b

ffprobe of the resultant m4b show the chapters sequence and timings become completely messed up:

    Chapter #0:0: start 0.000000, end 19.950000
    Metadata:
      title           : A
    Chapter #0:1: start 19.950000, end 19.951000
    Metadata:
      title           : ABC
    Chapter #0:2: start 19.951000, end 19.952000
    Metadata:
      title           : ABCD
    Chapter #0:3: start 19.952000, end 29.952000
    Metadata:
      title           : ABCDEF

As an additional note, similar code using eyed3 worked flawlessly. The chapters were written out in expected sequence and coversion with ffmpeg to m4b had no issue.

Am I doing something wrong with mutagen? Or is this expected behaviour?

ping avatar Dec 22 '20 07:12 ping

The order is defined in CTOC. In id3 the order of how frames appear in the file is not significant.

I guess ffmpeg doesn't support it fully.

We might be able to sort it... I guess based on start time?

lazka avatar Dec 22 '20 08:12 lazka

Thanks for the response.

I'm not familiar with the internal workings of mutagen, but yes, sorting based on start time sounds like a pretty reasonable choice.

ping avatar Dec 22 '20 08:12 ping

Hello @lazka,

I also came accross this issue in ffmpeg recently when writing metadata with mutagen.

It would be great to have a solution in mutagen to sort the chapter marks by start time when writing CHAP tags like you suggested.

From what I could understand of the code the sorting is made here : https://github.com/quodlibet/mutagen/blob/30f373fa6d56e1afa17d48a0c6f3e111267fbd32/mutagen/id3/_tags.py#L199

girardof avatar Jun 15 '21 14:06 girardof

Hello!

Just stumbled upon this as well. Is there anything we can do to support? A hint on how you wish this to be fixed would be appreciated, so one could get things going.

Thanks!

spiderpug avatar Jul 09 '21 13:07 spiderpug

@lazka I took a shot at fixing this. Appreciate if you can take a look at the PR.

ping avatar Sep 25 '21 12:09 ping

thanks

lazka avatar Sep 25 '21 12:09 lazka

It seems if CHAP frames aren't ordered according to start_time, Apple Podcasts simply won't render them if though according to the spec the order should be defined by CTOC.

ikalnytskyi avatar Nov 29 '22 21:11 ikalnytskyi