2017-01-14 18:31:47 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import subprocess
|
2017-01-15 04:12:45 +01:00
|
|
|
import base64
|
2017-01-14 18:31:47 +01:00
|
|
|
|
2018-01-20 21:52:42 +01:00
|
|
|
import codecs
|
2017-01-14 22:36:06 +01:00
|
|
|
import yaml
|
|
|
|
|
2017-01-14 18:31:47 +01:00
|
|
|
import mutagen
|
2017-01-15 04:12:45 +01:00
|
|
|
from mutagen.id3 import ID3, CTOC, CHAP, TIT2, CTOCFlags, APIC
|
2017-01-14 22:36:06 +01:00
|
|
|
from mutagen.easyid3 import EasyID3
|
|
|
|
from mutagen.easymp4 import EasyMP4
|
2017-01-14 18:31:47 +01:00
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
YAML_KEYS = ["title", "artist", "date", "comment", "cover", "language", "chapters"]
|
2017-01-14 18:31:47 +01:00
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
|
2017-01-14 18:31:47 +01:00
|
|
|
def time_to_milliseconds(time_str):
|
|
|
|
time_parts = list(map(lambda t: float(t), time_str.split(':')))
|
|
|
|
time = 0
|
|
|
|
for part in time_parts[:-1]:
|
|
|
|
time = part + time * 60
|
|
|
|
|
|
|
|
time = int((time_parts[-1] + time) * 1000)
|
|
|
|
|
|
|
|
return time
|
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
def read_tag_file(chapter_file):
|
2017-01-14 18:31:47 +01:00
|
|
|
if not os.path.isfile(chapter_file):
|
|
|
|
raise RuntimeError("Chapter file %s does not exist" % chapter_file)
|
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
tags = yaml.load(open(chapter_file, 'r'))
|
|
|
|
|
|
|
|
for key in YAML_KEYS:
|
|
|
|
if not key in tags:
|
|
|
|
raise RuntimeError("Expected key %s not found in yaml" % key)
|
|
|
|
|
|
|
|
|
|
|
|
tags['chapters'] = sorted(tags['chapters'].items(), key=lambda x: x[0])
|
|
|
|
|
|
|
|
return tags
|
|
|
|
|
|
|
|
|
|
|
|
def make_mp3_tags(tags, path):
|
|
|
|
id3 = EasyID3(path)
|
|
|
|
|
|
|
|
id3['title'] = tags['title']
|
|
|
|
id3['artist'] = tags['artist']
|
|
|
|
id3['date'] = tags['date']
|
|
|
|
id3['language'] = tags['language']
|
2017-01-14 18:31:47 +01:00
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
id3.save()
|
2017-01-14 18:31:47 +01:00
|
|
|
|
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
def make_mp3_chapters(chapters, path):
|
|
|
|
audio = mutagen.File(path)
|
|
|
|
|
2017-01-14 18:31:47 +01:00
|
|
|
file_length = int(audio.info.length * 1000)
|
|
|
|
|
|
|
|
element_ids = [(u"ch%d" % i) for i in range(0, len(chapters))]
|
|
|
|
|
|
|
|
audio.tags.add(CTOC(element_id = u"toc",
|
|
|
|
flags = CTOCFlags.TOP_LEVEL | CTOCFlags.ORDERED,
|
|
|
|
child_element_ids = element_ids,
|
|
|
|
sub_frames = [
|
|
|
|
TIT2(text = [u"TOC"]),
|
|
|
|
]))
|
|
|
|
|
|
|
|
for i in range(0, len(chapters)):
|
|
|
|
start_time, name = chapters[i]
|
|
|
|
start_time = time_to_milliseconds(start_time)
|
|
|
|
|
|
|
|
end_time = file_length - 1
|
|
|
|
if i < len(chapters) - 1:
|
|
|
|
end_time = time_to_milliseconds(chapters[i+1][0])
|
|
|
|
|
|
|
|
audio.tags.add(CHAP(element_id = u"chp%d" % i, start_time = start_time, end_time = end_time,
|
|
|
|
sub_frames = [
|
|
|
|
TIT2(text = [name]),
|
|
|
|
]))
|
|
|
|
|
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
|
|
|
|
def make_mp4_tags(tags, path):
|
|
|
|
mp4 = EasyMP4(path)
|
|
|
|
|
|
|
|
mp4['title'] = tags['title']
|
|
|
|
mp4['artist'] = tags['artist']
|
|
|
|
mp4['date'] = tags['date']
|
|
|
|
|
|
|
|
mp4.save()
|
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
2017-01-14 18:31:47 +01:00
|
|
|
def make_mp4_chapters(chapters, path):
|
|
|
|
chap_path = "%s.chapters.txt" % os.path.splitext(path)[0]
|
2018-01-20 21:52:42 +01:00
|
|
|
chapter_file = codecs.open(chap_path, 'w', 'utf-8')
|
2017-01-14 18:31:47 +01:00
|
|
|
lines = [u"%s %s\n" % (start_time, name) for start_time, name in chapters]
|
|
|
|
chapter_file.writelines(lines)
|
|
|
|
chapter_file.close()
|
|
|
|
|
|
|
|
popen = subprocess.Popen(["mp4chaps", "-i", path])
|
|
|
|
popen.wait()
|
|
|
|
|
|
|
|
os.remove(chap_path)
|
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
|
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
def make_ogg_tags(tags, audio):
|
|
|
|
audio.tags['TITLE'] = tags['title']
|
|
|
|
audio.tags['ARTIST'] = tags['artist']
|
|
|
|
audio.tags['DATE'] = tags['date']
|
|
|
|
audio.tags['LANGUAGE'] = tags['language']
|
|
|
|
audio.tags['COMMENT'] = tags['comment']
|
|
|
|
|
|
|
|
audio.save()
|
2017-01-14 18:31:47 +01:00
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
def make_ogg_cover(cover, audio):
|
|
|
|
jpeg_data = open(cover, "rb").read()
|
|
|
|
|
|
|
|
audio['coverartmime'] = 'image/jpeg'
|
|
|
|
audio['coverartdescription'] = 'Cover'
|
|
|
|
audio['coverarttype'] = '3'
|
2018-01-20 21:52:42 +01:00
|
|
|
audio['coverart'] = base64.b64encode(jpeg_data).decode("utf-8")
|
2017-01-15 04:12:45 +01:00
|
|
|
|
|
|
|
audio.save()
|
|
|
|
|
|
|
|
|
|
|
|
def make_ogg_chapters(chapters, audio):
|
2017-01-14 18:31:47 +01:00
|
|
|
for i in range(0, len(chapters)):
|
|
|
|
num = str(i).zfill(3)
|
|
|
|
audio.tags['CHAPTER%s' % num] = chapters[i][0]
|
|
|
|
audio.tags['CHAPTER%sNAME' % num] = chapters[i][1]
|
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
audio.save()
|
2017-01-14 18:31:47 +01:00
|
|
|
|
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
|
2017-01-14 18:31:47 +01:00
|
|
|
def main():
|
|
|
|
|
|
|
|
if len(sys.argv) < 3:
|
2017-01-14 22:36:06 +01:00
|
|
|
print("Usage: %s tagfile mediafile1 mediafile2 mediafile3 ..." % sys.argv[0])
|
2017-01-14 18:31:47 +01:00
|
|
|
sys.exit(0)
|
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
tag_file_path = sys.argv[1]
|
|
|
|
media_files = sys.argv[2:]
|
2017-01-14 18:31:47 +01:00
|
|
|
|
2017-01-14 22:36:06 +01:00
|
|
|
tags = read_tag_file(tag_file_path)
|
2017-01-14 18:31:47 +01:00
|
|
|
|
2017-01-15 04:12:45 +01:00
|
|
|
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")
|
|
|
|
|
2017-01-14 18:31:47 +01:00
|
|
|
for path in media_files:
|
|
|
|
print("Adding chapters to: %s" % path)
|
2017-01-14 22:36:06 +01:00
|
|
|
|
2017-01-14 18:31:47 +01:00
|
|
|
audio = mutagen.File(path)
|
|
|
|
|
|
|
|
if isinstance(audio, mutagen.mp3.MP3):
|
2017-01-14 22:36:06 +01:00
|
|
|
make_mp3_tags(tags, path)
|
2017-01-15 04:12:45 +01:00
|
|
|
make_mp3_cover(tags['cover'], path)
|
2017-01-14 22:36:06 +01:00
|
|
|
make_mp3_chapters(tags['chapters'], path)
|
2017-01-14 18:31:47 +01:00
|
|
|
elif isinstance(audio, mutagen.mp4.MP4):
|
2017-01-14 22:36:06 +01:00
|
|
|
make_mp4_tags(tags, path)
|
2017-01-15 04:12:45 +01:00
|
|
|
make_mp4_cover(tags['cover'], path)
|
2017-01-14 22:36:06 +01:00
|
|
|
make_mp4_chapters(tags['chapters'], path)
|
2017-01-14 18:31:47 +01:00
|
|
|
elif isinstance(audio, mutagen.oggvorbis.OggVorbis) or isinstance(audio, mutagen.oggopus.OggOpus):
|
2017-01-14 22:36:06 +01:00
|
|
|
make_ogg_tags(tags, audio)
|
2017-01-15 04:12:45 +01:00
|
|
|
make_ogg_cover(tags['cover'], audio)
|
2017-01-14 22:36:06 +01:00
|
|
|
make_ogg_chapters(tags['chapters'], audio)
|
2017-01-14 18:31:47 +01:00
|
|
|
else:
|
|
|
|
print("Skipping unsupported file: %s" % path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|