xconv: refactor main; adjust profiles

master
Taeyeon Mori 7 years ago
parent 8d5bbcbd03
commit 63cfaefe2b
  1. 88
      lib/python/xconv/app.py
  2. 18
      lib/python/xconv/cmdline.py
  3. 33
      lib/python/xconv/profile.py
  4. 46
      lib/python/xconv/profiles/audiobook.py
  5. 45
      lib/python/xconv/profiles/audiobook_from_chapters.py
  6. 41
      lib/python/xconv/profiles/getsubs.py

@ -50,15 +50,58 @@ class SimpleTask(advancedav.SimpleTask):
output_factory = OutputFile
class AdvancedTask(advancedav.Task):
output_factory = OutputFile
def __init__(self, aav, basename):
self.output_basename = basename
super().__init__(aav)
# == App ==
def make_outfile(args, profile, infile):
if not args.output_filename:
if hasattr(profile, "ext"):
return build_path(args.output_directory, ".".join((splitext(basename(infile))[0], profile.ext if profile.ext else "bin")))
else:
return build_path(args.output_directory, basename(infile))
def make_basename(path, infile):
return build_path(path, splitext(basename(infile))[0])
def make_outfile(path, infile, ext=None):
name, oldext = splitext(basename(infile))
return build_path(path, ".".join((name, ext if ext else oldext)))
def create_task(aav, profile, inputs, args, filename_from=None):
is_advanced_task_profile = any(("advanced_task" in profile.features,
"no_single_output" in profile.features))
filename_from = filename_from or inputs[0]
if not is_advanced_task_profile:
fmt = advancedav.DEFAULT_CONTAINER
ext = None
if "output" in profile.features:
fmt, ext = profile.features["output"]
outfile = args.output if args.output_filename else make_outfile(args.output_directory, filename_from, ext)
task = SimpleTask(aav, outfile, fmt)
else:
return args.output
basename = args.output if args.output_filename else make_basename(args.output_directory, filename_from)
task = AdvancedTask(aav, basename)
for input in inputs:
task.add_input(input)
return task
def task_name(task):
if hasattr(task, "name"):
return basename(task.name)
elif task.inputs:
return "<%s" % task.inputs[0].name
elif task.outputs:
return ">%s" % task.outputs[0].name
else:
return "(anon task %p)" % id(task)
def main(argv):
@ -95,12 +138,7 @@ def main(argv):
print("\033[35mCollecting Tasks..\033[0m")
if args.merge:
task = SimpleTask(aav, make_outfile(args, profile, args.inputs[0]), profile.container)
for input in args.inputs:
task.add_input(input)
tasks.append(task)
tasks.append(create_task(aav, profile, args.inputs, args))
elif args.concat:
import tempfile, os
@ -113,7 +151,7 @@ def main(argv):
print("\033[36m Concatenating %s\033[0m" % basename(f))
tmp.write("file '%s'\n" % f)
task = SimpleTask(aav, make_outfile(args, profile, args.inputs[0]), profile.container)
task = create_task(aav, profile, (), args, filename_from=args.inputs[0])
task.add_input(tmp.name).set(f="concat", safe="0")
@ -121,12 +159,7 @@ def main(argv):
else:
for input in args.inputs:
out = make_outfile(args, profile, input)
if args.update and exists(out):
continue
task = SimpleTask(aav, out, profile.container)
task.add_input(input)
tasks.append(task)
tasks.append(create_task(aav, profile, (input,), args))
print("\033[35mPreparing Tasks..\033[0m")
@ -140,17 +173,26 @@ def main(argv):
# Apply profile
for task in tasks:
print("\033[32m Applying profile for '%s'\033[0m" % basename(task.name), end="\033[K\r")
print("\033[32m Applying profile for '%s'\033[0m" % task_name(task), end="\033[K\r")
res = profile(task, **pkw)
if not res:
print("\033[31m Failed to apply profile for '%s'\033[0m\033[K" % basename(task.name))
print("\033[31m Failed to apply profile for '%s'\033[0m\033[K" % task_name(task))
return 1
if args.update:
for task in tasks[:]:
for output in [o for o in task.outputs if exists(o.name)]:
print("\033[33m Skipping existing '%s' (--update)\033[0m\033[K" % basename(skip.name))
task.outputs.remove(skip)
if not tasks.outputs:
print("\033[33m Skipping task '%s' because no output files are left\033[0m\033[K" % task_name(task))
tasks.remove[task]
print("\033[35mExecuting Tasks..\033[0m\033[K")
# Commit
for task in tasks:
print("\033[32m Processing '%s'\033[0m" % basename(task.name))
print("\033[32m Processing '%s'\033[0m" % task_name(task))
task.commit()
# Clean up

@ -51,7 +51,7 @@ class ProfilesAction(TerminalAction):
def run(self, parser):
print("Available Profiles:")
for name, profile in sorted(load_all_profiles().items()):
print(" %-20s %s" % (name, profile.description if profile.description else ""))
print(" %-25s %s" % (name, profile.description if profile.description else ""))
class ProfileInfoAction(TerminalAction):
@ -62,13 +62,15 @@ class ProfileInfoAction(TerminalAction):
print("Profile '%s':" % profile_name)
if profile.description:
print(" Description: %s" % profile.description)
output_info = []
if profile.container:
output_info.append("Format: %s" % profile.container)
if profile.ext:
output_info.append("File extension: %s" % profile.ext)
if output_info:
print(" Output: %s" % "; ".join(output_info))
if "output" in profile.features:
output = profile.features["output"]
output_info = []
if output[0]:
output_info.append("Format: %s" % output[0])
if output[1]:
output_info.append("File extension: %s" % output[0])
if output_info:
print(" Output: %s" % "; ".join(output_info))
if profile.features:
print(" Flags: %s" % ", ".join("%s(%r)" % (k, v) if v is not None else k for k, v in profile.features.items()))
if profile.defines:

@ -77,18 +77,6 @@ def description(desc):
return apply
def output(container=None, ext=None):
""" Add output file information """
def apply(f):
if container:
f.container = container
f.ext = "mkv" if container == "matroska" else container
if ext:
f.ext = ext
return f
return apply
def defines(**defs):
""" Document supported defines with description """
def apply(f):
@ -98,13 +86,32 @@ def defines(**defs):
def features(**features):
""" Set opaque feature flags """
"""
Set opaque feature flags
Boolean flags are checked for existance, not actual value.
Known flags:
- output: Specifies type of the output file [(format, file-extension)]
- argshax: Pass parsed cmdline arguments in 'args' kwd
- singleaudio: Indicates that it operates on a single audio stream. No effects
- no_single_output: Profile doesn't constitute a 1:1 file conversion. Don't use SimpleTask
- advanced_task: reserved
"""
def apply(f):
__update(f, "features", features)
return f
return apply
def output(container=None, ext=None):
""" Add output file information """
return features(output=(
container,
ext if ext else "mkv" if container == "matroska" else container
))
def singleaudio(profile):
"""
Operate on a single audio stream (The first one found)

@ -28,27 +28,37 @@ Opus Audiobook profile
from ..profile import *
abdefines = dict(
stereo="Use two channels at 48k",
bitrate="Use custom target bitrate",
fancy="Use 56kbps stereo (For dramatic audiobooks with a lot of music and effects)"
)
def apply(stream, defines):
stream.set(codec="libopus",
vbr="on",
b="40k",
ac="1",
application="voip")
if stream.source.channels > 1:
if "stereo" in defines:
stream.set(ac="2",
b="48k")
if "fancy" in defines:
stream.set(ac="2",
b="56k",
application="audio")
if "bitrate" in defines:
stream.bitrate = defines["bitrate"]
# At most input bitrate; we wouldn't gain anything since opus should be same or better compression
stream.bitrate = min(stream.bitrate, stream.source.bitrate)
@profile
@description("Encode Opus Audiobook")
@output(container="ogg", ext="ogg")
@defines(stereo="Use two channels",
bitrate="Use custom target bitrate",
fancy="Use 48kbps stereo (For dramatic audiobooks with a lot of music and effects)")
@defines(**abdefines)
@singleaudio
def audiobook(task, stream, defines):
out = (task.map_stream(stream)
.set(codec="libopus",
vbr="on",
b="32k",
ac="1",
application="voip"))
if "stereo" in defines:
out.set(ac="2",
b="36k")
if "fancy" in defines:
out.set(ac="2",
b="48k",
application="audio")
if "bitrate" in defines:
out.set(b=defines["bitrate"])
apply(task.map_stream(stream), defines)
return True

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
xconv ffmpeg wrapper based on AdvancedAV
-----------------------------------------------------------
AdvancedAV helps with constructing FFmpeg commandline arguments.
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 (c) 2015-2017 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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-----------------------------------------------------------
Opus Audiobook profile from m4b chapters
"""
from ..profile import *
from .audiobook import apply, abdefines
@profile
@description("Split & Encode Opus Audiobook from M4B chapters")
@output(container="ogg", ext="ogg")
@features(no_single_output=True)
@defines(**abdefines)
@singleaudio
def audiobook_from_chapters(task, stream, defines):
for chapter in task.iter_chapters():
apply(
task.add_output(chapter.title + ".ogg", "ogg")
.set(ss=chapter.start_time,
to=chapter.end_time)
.map_stream(stream), defines)
return True

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""
xconv ffmpeg wrapper based on AdvancedAV
-----------------------------------------------------------
AdvancedAV helps with constructing FFmpeg commandline arguments.
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 (c) 2015-2017 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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-----------------------------------------------------------
Extract subtitle streams
"""
from ..profile import *
@profile
@description("Extract subtitle tracks")
@defines(format="Convert all subtitles to a specific format")
@features(no_single_output=True)
def getsubs(task, defines):
for stream in task.iter_subtitle_streams():
of = task.add_output("%s.%s.%s" % (task.output_basename, task.inputs.index(stream.file), stream.stream_spec), None) # TODO get real file extension
os = of.map_stream(stream)
if "format" in defines:
os.codec = defines["format"]
return True
Loading…
Cancel
Save