xconv: Add support for Audible AAX

master
Taeyeon Mori 5 years ago
parent a64c02f870
commit 9c346bbcf0
  1. 26
      lib/python/advancedav.py
  2. 5
      lib/python/xconv/app.py
  3. 93
      lib/python/xconv/profiles/audiobook.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

@ -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 = []

@ -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: <album>.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")

Loading…
Cancel
Save