diff --git a/lib/python/advancedav.py b/lib/python/advancedav.py index cec576e..20790db 100644 --- a/lib/python/advancedav.py +++ b/lib/python/advancedav.py @@ -6,7 +6,7 @@ AdvancedAV FFmpeg commandline generator v3.0 [Library Edition] It can automatically parse input files with the help of FFmpeg's ffprobe tool (WiP) and allows programatically mapping streams to output files and setting metadata on them. ----------------------------------------------------------- - Copyright 2014-2017 Taeyeon Mori + Copyright 2014-2019 Taeyeon Mori This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -37,11 +37,9 @@ from pathlib import Path, PurePath __all__ = "AdvancedAVError", "AdvancedAV", "SimpleAV", "MultiAV" -version_info = 2, 99, 7 +version_info = 2, 99, 8 # Constants -DEFAULT_CONTAINER = "matroska" - S_AUDIO = "a" S_VIDEO = "v" S_SUBTITLE = "s" @@ -784,16 +782,16 @@ class OutputFile(File, ObjectWithMetadata): Holds information about an output file """ __slots__ = "task", "container", "_mapped_sources", "metadata" - + local_option_names = ("reorder_streams",) + File.local_option_names stream_factory = staticmethod(output_stream_factory) - def __init__(self, task: "Task", name: str, container=DEFAULT_CONTAINER, + def __init__(self, task: "Task", name: str, container=None, options: Mapping=None, metadata: Mapping=None): super().__init__(name, options=options, metadata=metadata) - self.options.setdefault("c", "copy") + #self.options.setdefault("c", "copy") self.options.setdefault("reorder_streams", True) self.task = task @@ -1153,7 +1151,7 @@ class Task(BaseTask): return file # -- Manage Outputs - def add_output(self, filename: str, container: str=DEFAULT_CONTAINER, options: Mapping=None) -> OutputFile: + def add_output(self, filename: str, container: str=None, options: Mapping=None) -> OutputFile: """ Add an output file NOTE: Contrary to add_input this will NOT take an OutputFile instance and return it. @@ -1187,7 +1185,7 @@ class SimpleTask(Task): All members of the OutputFile can be accessed on the SimpleTask directly, as well as the usual Task methods. Usage of add_output should be avoided however, because it would lead to confusion. """ - def __init__(self, pp: "AdvancedAV", filename: str, container: str=DEFAULT_CONTAINER, options: Mapping=None): + def __init__(self, pp: "AdvancedAV", filename: str, container: str=None, options: Mapping=None): super().__init__(pp) self.output = self.add_output(filename, container, options) @@ -1238,7 +1236,7 @@ class AdvancedAV(metaclass=ABCMeta): """ return Task(self) - def create_job(self, filename: str, container: str=DEFAULT_CONTAINER, options: Mapping=None) -> SimpleTask: + def create_job(self, filename: str, container: str=None, options: Mapping=None) -> SimpleTask: """ Create a simple AdvandecAV task :param filename: str The resulting filename @@ -1349,9 +1347,9 @@ class SimpleAV(AdvancedAV): return out.decode("utf-8", "replace") def probe_file(self, file, *, ffprobe_args_hint=None): - probe = self.call_probe(tuple(FFmpeg.argv_options(file.options)) - + ffprobe_args_hint - + ("-i", file.filename)) + probe = self.call_probe(ffprobe_args_hint + + tuple(FFmpeg.argv_options(file.options)) + + ("-i", file.filename)) return json.loads(probe) @@ -1383,7 +1381,7 @@ class MultiAV(SimpleAV): while self.queue: self.manage_workers() sleep(.250) - + def manage_workers(self): """ Make a single run over available workers and see to it that they have work if available diff --git a/lib/python/xconv/app.py b/lib/python/xconv/app.py index 1f6bec9..c81b665 100644 --- a/lib/python/xconv/app.py +++ b/lib/python/xconv/app.py @@ -99,7 +99,7 @@ def create_task(aav, profile, inputs, args, filename_from=None): filename_from = filename_from or inputs[0] if not is_advanced_task_profile: - fmt = advancedav.DEFAULT_CONTAINER + fmt = None ext = None if "output" in profile.features: fmt, ext = profile.features["output"] @@ -166,7 +166,8 @@ def main(argv): if args.quiet: aav.global_conv_args = "-loglevel", "warning" - aav.global_args += "-hide_banner", "-stats" + aav.global_args += "-hide_banner", + aav.global_conv_args += "-stats", # Collect Tasks tasks = [] diff --git a/lib/python/xconv/profiles/audiobook.py b/lib/python/xconv/profiles/audiobook.py index f3b686f..36d84e9 100644 --- a/lib/python/xconv/profiles/audiobook.py +++ b/lib/python/xconv/profiles/audiobook.py @@ -26,6 +26,7 @@ Opus Audiobook profile """ import os +import math from ..profile import * @@ -34,7 +35,7 @@ abdefines = dict( bitrate = "Use custom target bitrate", stereo = "Use 2 channels (Ignored for mono source streams)", fancy = "Use higher bitrates (48k mono/64k stereo)", - ogg = "Use the .ogg file extension (Currently required on Android)" + ogg = "Use the .ogg file extension (Currently required on Android)", ) def apply_stream(stream, defines): @@ -66,10 +67,26 @@ def apply_stream(stream, defines): stream.bitrate = min(stream.bitrate, stream.source.bitrate) +metadefines = dict( + title="Name of the book", + series="Series the book belongs to", + author="Name of the book's author", + performer="Name of the audiobook's reader/narrator", + genre="Name of the genre. Default is Audiobook", + publisher="Name of the recording company", + language="Language", +) + + +def apply_metadata(ob, defines): + ob.apply_meta(defines, "language", "author", "performer", publisher="organization", title="album") + ob.meta(genre=defines.get("genre", "Audiobook")) + + @profile @description("Encode Opus Audiobook") @output(container="ogg", ext="opus") -@defines(**abdefines) +@defines(**abdefines, **metadefines) @singleaudio def audiobook(task, stream, defines): if "ogg" in defines: @@ -77,6 +94,8 @@ def audiobook(task, stream, defines): apply_stream(task.map_stream(stream), defines) + apply_metadata(task.output, defines) + return True @@ -87,7 +106,7 @@ def audiobook(task, stream, defines): @features(no_single_output=True) @defines(ignore_ends="Ignore chapter end marks and continue until next chapter starts", chapter_only_names="Don't include the input filename in the output filename", - **abdefines) + **abdefines, **metadefines) @singleaudio def from_chapters(task, stream, defines): # Read chapters from input @@ -126,9 +145,77 @@ def from_chapters(task, stream, defines): apply_stream(out.map_stream(stream), defines) + apply_metadata(out, defines) + + return True + + +@profile +@description("Split & Encode Opus Audiobook from Audible AAX") +@output(container="ogg", ext="opus") +@features(no_single_output=True) +@defines(key="Audible activation_bytes (required)", + cover_file="Filename for the extracted cover (Default: .jpg)", + #dont_embed_cover="Don't try to embed the cover", + artist_tag="Specify the tag to store the artist name (Default: author)", + performer="Add a performer tag", + #album="Override book title", + **abdefines) +def audible(task, defines): + if len(task.inputs) != 1: + print("audiobook.audible profile must be applied to a single AAX file!") + return False + + input = task.inputs[0] + if "key" not in defines: + if input.metadata["major_brand"].lower() == "aax": + print("Audible activation_bytes must be specified in the 'key' define!") + return False + else: + input.set(activation_bytes=defines["key"]) + + audio = input.audio_streams[0] + + # Extract cover + cover = input.video_streams[0] + cover_file = defines.get("cover_file", True) + if cover_file != "": + if cover_file is True: + cover_file = input.album + ".jpg" + elif "." not in cover_file: + cover_file += ".jpg" + cof = task.add_output(os.path.join(task.output_directory, cover_file)) + cof.map_stream(cover).set(c="copy") + + ext = "ogg" if "ogg" in defines else "opus" + chaps = len(input.chapters) + ct_fmt = "%%s %%0%dd - %%s.%s" % (math.ceil(math.log10(chaps)), ext) + add_meta = {defines.get("artist_tag", "author"): input.artist} + #album = defines.get("album", input.album) + + for chapter in input.chapters: + no = chapter.index + 1 + title = " - ".join((input.album, chapter.title)) + filename = os.path.join(task.output_directory, ct_fmt % (input.album, no, chapter.title)) + out = task.add_output(filename) + out.set(ss = chapter.start_time, + to = chapter.end_time, + map_metadata = "-1", + reorder_streams = False) + out.meta(title = title, + album = input.title, + tracknumber = "%d/%d" % (no, chaps), + **add_meta) + out.apply_meta(input.metadata, "copyright", "genre", "date", comment="description") + out.apply_meta(defines, "performer", publisher="organization") + apply_stream(out.map_stream(audio), defines) + #if not "dont_embed_cover" in defines: + # out.map_stream(cover) # Not sure how to make ffmpeg add covers to ogg + return True + @profile @description("Split into uniform pieces & Encode Opus Audiobook") @output(container="ogg", ext="opus")