Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for reading chapters from ID3 tags #979

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading