Skip to content

Commit

Permalink
Add support for reading chapters from ID3 tags (#979)
Browse files Browse the repository at this point in the history
  • Loading branch information
rdbende authored Feb 11, 2025
1 parent c02e73e commit ff983a0
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 42 deletions.
94 changes: 55 additions & 39 deletions cozy/media/tag_reader.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import os
from urllib.parse import unquote, urlparse

import mutagen
from gi.repository import GLib, Gst, GstPbutils
from mutagen import File
from mutagen.mp3 import MP3
from mutagen.mp4 import MP4

from cozy.media.chapter import Chapter
from cozy.media.media_file import MediaFile

NS_TO_SEC = 10**9


class TagReader:
def __init__(self, uri: str, discoverer_info: GstPbutils.DiscovererInfo):
Expand Down Expand Up @@ -55,15 +54,15 @@ def _get_book_name_fallback(self):
def _get_author(self):
authors = self._get_string_list(Gst.TAG_COMPOSER)

if len(authors) > 0 and authors[0]:
if authors and authors[0]:
return "; ".join(authors)
else:
return _("Unknown")

def _get_reader(self):
readers = self._get_string_list(Gst.TAG_ARTIST)

if len(readers) > 0 and readers[0]:
if readers and readers[0]:
return "; ".join(readers)
else:
return _("Unknown")
Expand All @@ -89,20 +88,15 @@ def _get_track_name_fallback(self):
return unquote(filename_without_extension)

def _get_chapters(self):
if self.uri.lower().endswith("m4b") and self._mutagen_supports_chapters():
mutagen_tags = self._parse_with_mutagen()
return self._get_m4b_chapters(mutagen_tags)
else:
return self._get_single_chapter()
path = unquote(urlparse(self.uri).path)
mutagen_file = File(path)

def _get_single_chapter(self):
chapter = Chapter(
name=self._get_track_name(),
position=0,
length=self._get_length_in_seconds(),
number=self._get_track_number(),
)
return [chapter]
if isinstance(mutagen_file, MP4):
return self._get_mp4_chapters(mutagen_file)
elif isinstance(mutagen_file, MP3):
return self._get_mp3_chapters(mutagen_file)
else:
return self._get_single_file_chapter()

def _get_cover(self):
success, sample = self.tags.get_sample_index(Gst.TAG_IMAGE, 0)
Expand Down Expand Up @@ -131,46 +125,68 @@ def _get_string_list(self, tag: str):

values = []
for i in range(self.tags.get_tag_size(tag)):
(success, value) = self.tags.get_string_index(tag, i)
success, value = self.tags.get_string_index(tag, i)
if success:
values.append(value.strip())

return values

def _get_m4b_chapters(self, mutagen_tags: MP4) -> list[Chapter]:
chapters = []
def _get_single_file_chapter(self):
chapter = Chapter(
name=self._get_track_name(),
position=0,
length=self._get_length_in_seconds(),
number=self._get_track_number(),
)
return [chapter]

def _get_mp4_chapters(self, file: MP4) -> list[Chapter]:
if not file.chapters or len(file.chapters) == 0:
return self._get_single_file_chapter()

if not mutagen_tags.chapters or len(mutagen_tags.chapters) == 0:
return self._get_single_chapter()
chapters = []

for index, chapter in enumerate(mutagen_tags.chapters):
if index < len(mutagen_tags.chapters) - 1:
length = mutagen_tags.chapters[index + 1].start - chapter.start
for index, chapter in enumerate(file.chapters):
if index < len(file.chapters) - 1:
length = file.chapters[index + 1].start - chapter.start
else:
length = self._get_length_in_seconds() - chapter.start

title = chapter.title or ""

chapters.append(
Chapter(
name=title,
position=int(chapter.start * NS_TO_SEC),
name=chapter.title or "",
position=int(chapter.start * Gst.SECOND),
length=length,
number=index + 1,
)
)

return chapters

def _parse_with_mutagen(self) -> MP4:
path = unquote(urlparse(self.uri).path)
mutagen_mp4 = MP4(path)
def _get_mp3_chapters(self, file: MP3) -> list[Chapter]:
chaps = file.tags.getall("CHAP")
if not chaps:
return self._get_single_file_chapter()

chapters = []
chaps.sort(key=lambda k: k.element_id)

for index, chapter in enumerate(chaps):
if index < len(chaps) - 1:
length = chapter.end_time - chapter.start_time
else:
length = self._get_length_in_seconds() - chapter.start_time

return mutagen_mp4
sub_frames = chapter.sub_frames.get("TIT2", ())
title = sub_frames.text[0] if sub_frames else ""

@staticmethod
def _mutagen_supports_chapters() -> bool:
if mutagen.version[0] > 1:
return True
chapters.append(
Chapter(
name=title,
position=int(chapter.start_time * Gst.SECOND),
length=length,
number=index + 1,
)
)

return mutagen.version[0] == 1 and mutagen.version[1] >= 45
return chapters
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distro
mutagen
mutagen>=1.47
peewee>=3.9.6
pytz
requests
Expand Down
4 changes: 2 additions & 2 deletions test/cozy/media/test_tag_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def test_get_m4b_chapters(mocker, discoverer_mocks):
from cozy.media.tag_reader import TagReader

tag_reader = TagReader("file://abc/def/01 a nice file.m4b", discoverer_mocks.info)
chapters = tag_reader._get_m4b_chapters(M4B([M4BChapter("test", 0), M4BChapter("testa", 1)]))
chapters = tag_reader._get_mp4_chapters(M4B([M4BChapter("test", 0), M4BChapter("testa", 1)]))

assert len(chapters) == 2
assert chapters[0].name == "test"
Expand All @@ -144,7 +144,7 @@ def test_get_m4b_chapters_creates_single_chapter_if_none_present(mocker, discove
from cozy.media.tag_reader import TagReader

tag_reader = TagReader("file://abc/def/01 a nice file.m4b", discoverer_mocks.info)
chapters = tag_reader._get_m4b_chapters(M4B([]))
chapters = tag_reader._get_mp4_chapters(M4B([]))

assert len(chapters) == 1
assert chapters[0].name == "a nice file"
Expand Down

0 comments on commit ff983a0

Please sign in to comment.