diff --git a/eyed3/id3/tag.py b/eyed3/id3/tag.py index 5222d94c..18a9e8fc 100644 --- a/eyed3/id3/tag.py +++ b/eyed3/id3/tag.py @@ -5,7 +5,7 @@ import tempfile import textwrap -from ..utils import requireUnicode, chunkCopy, datePicker, b +from ..utils import requireUnicode, requireBytes, chunkCopy, datePicker, b from .. import core from ..core import TXXX_ALBUM_TYPE, TXXX_ARTIST_ORIGIN, ALBUM_TYPE_IDS, ArtistOrigin from .. import Error @@ -1814,11 +1814,32 @@ def get(self, email): class ChaptersAccessor(AccessorBase): def __init__(self, fs): + self._next_id = 0 + def match_func(frame, element_id): return frame.element_id == element_id super().__init__(frames.CHAPTER_FID, fs, match_func) - def set(self, element_id, times, offsets=(None, None), sub_frames=None): + @property + def chapter_ids(self): + return tuple([chap.element_id for chap in self]) + + @property + def next_chapter_id(self): + curr_ids = self.chapter_ids + next_id = None + + while not next_id: + self._next_id += 1 + chap_id = f"ch{self._next_id}".encode("latin1") + + if chap_id not in curr_ids: + next_id = chap_id + + return next_id + + @requireBytes(1, "element_id") + def set(self, element_id: bytes, times: tuple, offsets: tuple = (None, None), sub_frames=None): flist = self._fs[frames.CHAPTER_FID] or [] for chap in flist: if chap.element_id == element_id: diff --git a/eyed3/plugins/classic.py b/eyed3/plugins/classic.py index 0de57675..1df135a2 100644 --- a/eyed3/plugins/classic.py +++ b/eyed3/plugins/classic.py @@ -10,7 +10,7 @@ from eyed3.utils.console import ( printMsg, printError, printWarning, boldText, getTtySize, ) -from eyed3.id3.frames import ImageFrame +from eyed3.id3.frames import ImageFrame, StartEndTuple from eyed3.mimetype import guessMimetype from eyed3.utils.log import getLogger @@ -162,6 +162,14 @@ def CommentArg(arg): lang = vals[2] if len(vals) > 2 else id3.DEFAULT_LANG return text, desc, b(lang)[:3] + def ChapterArg(arg): + try: + start, end, title = _splitArgs(arg) + except Exception: + raise ValueError("Invalid chapter argument") + + return int(start), int(end), title + def LyricsArg(arg): text, desc, lang = CommentArg(arg) try: @@ -330,6 +338,11 @@ def PopularityArg(arg): dest="remove_all_comments", help=ARGS_HELP["--remove-all-comments"]) + # chapters + gid3.add_argument("--add-chapter", action="append", dest="chapters", + metavar="START:END:TITLE", default=[], + type=ChapterArg, help=ARGS_HELP["--add-chapter"]) + gid3.add_argument("--add-lyrics", action="append", type=LyricsArg, dest="lyrics", default=[], metavar="LYRICS_FILE[:DESCRIPTION[:LANG]]", @@ -711,6 +724,13 @@ def printTag(self, tag): printMsg("\nTerms of Use (%s): %s" % (boldText("USER"), tag.terms_of_use)) + # CHAP + if tag.chapters is not None: + for chapter in tag.chapters: + printMsg(f"[{boldText('Chapter:')} {chapter.element_id} " + f"Start time: {chapter.times.start} End time: {chapter.times.end}, " + f"Title: {chapter.title}]") + # --verbose if self.args.verbose: printMsg(self._getHardRule(self.terminal_width)) @@ -906,6 +926,15 @@ def _checkNumberedArgTuples(curr, new): accessor.set(text, desc, b(lang)) retval = True + # --add-chapter + for index, (start, end, title) in enumerate(self.args.chapters, 1): + elem_id = tag.chapters.next_chapter_id + printWarning(f"Setting chapter '{title}': {start:d} {end:d}") + tag.chapters.set(element_id=elem_id, times=StartEndTuple(start, end)) + chapter = tag.chapters.get(elem_id) + chapter.title = title + retval = True + # --play-count playcount_arg = self.args.play_count if playcount_arg: @@ -1106,6 +1135,15 @@ def _getTemplateKeys(): "--write-images": "Causes all attached images (APIC frames) to be " "written to the specified directory.", + # chapters + "--add-chapter": + "Add a chapter. There may be more than one chapter in a " + "tag, as long as the START and END values are unique. " + "Start and end times is milliseconds offset from start. " + "End as well as TITLE are optional. If TITLE should be " + "given without END, use 4294967295 for END", + + "--add-object": "Add or replace an object. There may be more than one " "object in a tag, as long as the DESCRIPTION values " "are unique. The default DESCRIPTION is ''.",