diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..023050d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.ogg +*.opus +*.mp3 +*.mp4 +/virtenv diff --git a/README.md b/README.md new file mode 100644 index 0000000..8482c67 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +Multitag +======== + +A simple tool for adding metadata to multiple audio files in different formats. +Metadata is read from a yaml file and an additional jpg (for coverart) +and can be applied to multiple files at once. +It was developed with linux in mind, but might work on other platforms as well. + + +**Warning:** This is tool is a quick and dirty one day hack and has only been tested using mpv. +Expect bugs and other random weirdness. + + +Features +--------- + +Support audio formats: +* mp3 +* mp4 +* ogg vorbis +* ogg opus + +Supported metadata entries: +* Title +* Artist +* Date +* Language +* Coverart (only jpg format supported) +* Chapters + + +Usage +----- +`multitag.py metadatafile audiofile1 audiofile2 ... audiofileN` + + +Dependencies +------------ +* python3 +* pyyaml +* mutagen +* libm4v2 (for the m4chaps utility) diff --git a/cover.jpg b/cover.jpg new file mode 100644 index 0000000..1817594 Binary files /dev/null and b/cover.jpg differ diff --git a/example.yaml b/example.yaml index 2f36c22..aad27a3 100644 --- a/example.yaml +++ b/example.yaml @@ -3,7 +3,6 @@ artist: "Tony the tester" date: "2017-01-14" comment: "This is comment" language: "de" - cover: "cover.jpg" chapters: diff --git a/multitag.py b/multitag.py index 0862d80..7a9a079 100644 --- a/multitag.py +++ b/multitag.py @@ -3,16 +3,19 @@ import sys import os import subprocess +import base64 import yaml import mutagen -from mutagen.id3 import ID3, CTOC, CHAP, TIT2, CTOCFlags +from mutagen.id3 import ID3, CTOC, CHAP, TIT2, CTOCFlags, APIC from mutagen.easyid3 import EasyID3 from mutagen.easymp4 import EasyMP4 + YAML_KEYS = ["title", "artist", "date", "comment", "cover", "language", "chapters"] + def time_to_milliseconds(time_str): time_parts = list(map(lambda t: float(t), time_str.split(':'))) time = 0 @@ -23,6 +26,7 @@ def time_to_milliseconds(time_str): return time + def read_tag_file(chapter_file): if not os.path.isfile(chapter_file): raise RuntimeError("Chapter file %s does not exist" % chapter_file) @@ -50,6 +54,16 @@ def make_mp3_tags(tags, path): id3.save() +def make_mp3_cover(cover, path): + audio = mutagen.File(path) + + jpeg_data = open(cover, "rb").read() + image_tag = APIC(3, 'image/jpeg', 3, 'Cover', jpeg_data) + audio[image_tag.HashKey] = image_tag + + audio.save() + + def make_mp3_chapters(chapters, path): audio = mutagen.File(path) @@ -88,6 +102,17 @@ def make_mp4_tags(tags, path): mp4.save() + +def make_mp4_cover(cover, path): + jpeg_data = open(cover, "rb").read() + cover = mutagen.mp4.MP4Cover(jpeg_data) + + mp4 = mutagen.mp4.MP4(path) + mp4.tags['covr'] = [cover] + + mp4.save() + + def make_mp4_chapters(chapters, path): chap_path = "%s.chapters.txt" % os.path.splitext(path)[0] chapter_file = open(chap_path, 'w') @@ -100,6 +125,8 @@ def make_mp4_chapters(chapters, path): os.remove(chap_path) + + def make_ogg_tags(tags, audio): audio.tags['TITLE'] = tags['title'] audio.tags['ARTIST'] = tags['artist'] @@ -109,8 +136,19 @@ def make_ogg_tags(tags, audio): audio.save() -def make_ogg_chapters(chapters, audio): +def make_ogg_cover(cover, audio): + jpeg_data = open(cover, "rb").read() + + audio['coverartmime'] = 'image/jpeg' + audio['coverartdescription'] = 'Cover' + audio['coverarttype'] = '3' + audio['coverart'] = base64.b64encode(jpeg_data).decode("utf-8") + + audio.save() + + +def make_ogg_chapters(chapters, audio): for i in range(0, len(chapters)): num = str(i).zfill(3) audio.tags['CHAPTER%s' % num] = chapters[i][0] @@ -119,6 +157,7 @@ def make_ogg_chapters(chapters, audio): audio.save() + def main(): if len(sys.argv) < 3: @@ -130,6 +169,12 @@ def main(): tags = read_tag_file(tag_file_path) + if not os.path.isfile(tags['cover']): + raise RuntimeError("%s does not exist" % cover) + + if os.path.splitext(tags['cover'])[1] != ".jpg": + raise RuntimeError("Only jpgs are supported as covers") + for path in media_files: print("Adding chapters to: %s" % path) @@ -137,12 +182,15 @@ def main(): if isinstance(audio, mutagen.mp3.MP3): make_mp3_tags(tags, path) + make_mp3_cover(tags['cover'], path) make_mp3_chapters(tags['chapters'], path) elif isinstance(audio, mutagen.mp4.MP4): make_mp4_tags(tags, path) + make_mp4_cover(tags['cover'], path) make_mp4_chapters(tags['chapters'], path) elif isinstance(audio, mutagen.oggvorbis.OggVorbis) or isinstance(audio, mutagen.oggopus.OggOpus): make_ogg_tags(tags, audio) + make_ogg_cover(tags['cover'], audio) make_ogg_chapters(tags['chapters'], audio) else: print("Skipping unsupported file: %s" % path) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e78043e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +mutagen==1.36 +PyYAML==3.12