xconv: refactor main; adjust profiles

master
Taeyeon Mori 8 years ago
parent 8d5bbcbd03
commit 63cfaefe2b
  1. 86
      lib/python/xconv/app.py
  2. 12
      lib/python/xconv/cmdline.py
  3. 33
      lib/python/xconv/profile.py
  4. 42
      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 output_factory = OutputFile
class AdvancedTask(advancedav.Task):
output_factory = OutputFile
def __init__(self, aav, basename):
self.output_basename = basename
super().__init__(aav)
# == App == # == App ==
def make_outfile(args, profile, infile): def make_basename(path, infile):
if not args.output_filename: return build_path(path, splitext(basename(infile))[0])
if hasattr(profile, "ext"):
return build_path(args.output_directory, ".".join((splitext(basename(infile))[0], profile.ext if profile.ext else "bin")))
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: else:
return build_path(args.output_directory, basename(infile)) 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: else:
return args.output return "(anon task %p)" % id(task)
def main(argv): def main(argv):
@ -95,12 +138,7 @@ def main(argv):
print("\033[35mCollecting Tasks..\033[0m") print("\033[35mCollecting Tasks..\033[0m")
if args.merge: if args.merge:
task = SimpleTask(aav, make_outfile(args, profile, args.inputs[0]), profile.container) tasks.append(create_task(aav, profile, args.inputs, args))
for input in args.inputs:
task.add_input(input)
tasks.append(task)
elif args.concat: elif args.concat:
import tempfile, os import tempfile, os
@ -113,7 +151,7 @@ def main(argv):
print("\033[36m Concatenating %s\033[0m" % basename(f)) print("\033[36m Concatenating %s\033[0m" % basename(f))
tmp.write("file '%s'\n" % 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") task.add_input(tmp.name).set(f="concat", safe="0")
@ -121,12 +159,7 @@ def main(argv):
else: else:
for input in args.inputs: for input in args.inputs:
out = make_outfile(args, profile, input) tasks.append(create_task(aav, profile, (input,), args))
if args.update and exists(out):
continue
task = SimpleTask(aav, out, profile.container)
task.add_input(input)
tasks.append(task)
print("\033[35mPreparing Tasks..\033[0m") print("\033[35mPreparing Tasks..\033[0m")
@ -140,17 +173,26 @@ def main(argv):
# Apply profile # Apply profile
for task in tasks: 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) res = profile(task, **pkw)
if not res: 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 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") print("\033[35mExecuting Tasks..\033[0m\033[K")
# Commit # Commit
for task in tasks: 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() task.commit()
# Clean up # Clean up

@ -51,7 +51,7 @@ class ProfilesAction(TerminalAction):
def run(self, parser): def run(self, parser):
print("Available Profiles:") print("Available Profiles:")
for name, profile in sorted(load_all_profiles().items()): 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): class ProfileInfoAction(TerminalAction):
@ -62,11 +62,13 @@ class ProfileInfoAction(TerminalAction):
print("Profile '%s':" % profile_name) print("Profile '%s':" % profile_name)
if profile.description: if profile.description:
print(" Description: %s" % profile.description) print(" Description: %s" % profile.description)
if "output" in profile.features:
output = profile.features["output"]
output_info = [] output_info = []
if profile.container: if output[0]:
output_info.append("Format: %s" % profile.container) output_info.append("Format: %s" % output[0])
if profile.ext: if output[1]:
output_info.append("File extension: %s" % profile.ext) output_info.append("File extension: %s" % output[0])
if output_info: if output_info:
print(" Output: %s" % "; ".join(output_info)) print(" Output: %s" % "; ".join(output_info))
if profile.features: if profile.features:

@ -77,18 +77,6 @@ def description(desc):
return apply 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): def defines(**defs):
""" Document supported defines with description """ """ Document supported defines with description """
def apply(f): def apply(f):
@ -98,13 +86,32 @@ def defines(**defs):
def features(**features): 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): def apply(f):
__update(f, "features", features) __update(f, "features", features)
return f return f
return apply 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): def singleaudio(profile):
""" """
Operate on a single audio stream (The first one found) Operate on a single audio stream (The first one found)

@ -28,27 +28,37 @@ Opus Audiobook profile
from ..profile import * from ..profile import *
@profile abdefines = dict(
@description("Encode Opus Audiobook") stereo="Use two channels at 48k",
@output(container="ogg", ext="ogg")
@defines(stereo="Use two channels",
bitrate="Use custom target bitrate", bitrate="Use custom target bitrate",
fancy="Use 48kbps stereo (For dramatic audiobooks with a lot of music and effects)") fancy="Use 56kbps stereo (For dramatic audiobooks with a lot of music and effects)"
@singleaudio )
def audiobook(task, stream, defines):
out = (task.map_stream(stream) def apply(stream, defines):
.set(codec="libopus", stream.set(codec="libopus",
vbr="on", vbr="on",
b="32k", b="40k",
ac="1", ac="1",
application="voip")) application="voip")
if stream.source.channels > 1:
if "stereo" in defines: if "stereo" in defines:
out.set(ac="2", stream.set(ac="2",
b="36k") b="48k")
if "fancy" in defines: if "fancy" in defines:
out.set(ac="2", stream.set(ac="2",
b="48k", b="56k",
application="audio") application="audio")
if "bitrate" in defines: if "bitrate" in defines:
out.set(b=defines["bitrate"]) 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(**abdefines)
@singleaudio
def audiobook(task, stream, defines):
apply(task.map_stream(stream), defines)
return True 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