|
|
|
#!/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/>.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
from .profileman import load_profile
|
|
|
|
from .cmdline import parse_args, version
|
|
|
|
|
|
|
|
import advancedav
|
|
|
|
|
|
|
|
from os.path import isdir, join as build_path, basename, dirname, splitext, exists, abspath
|
|
|
|
from os import environ, makedirs, mkdir
|
|
|
|
from shutil import copyfile
|
|
|
|
from pathlib import Path
|
|
|
|
from functools import partial
|
|
|
|
|
|
|
|
|
|
|
|
# == Extend AAV ==
|
|
|
|
class OutputFile(advancedav.OutputFile):
|
|
|
|
def change_format(self, format=None, ext=None):
|
|
|
|
# Diverge from decorated format.
|
|
|
|
# Watch out for args.genout!!
|
|
|
|
if format:
|
|
|
|
self.container = format
|
|
|
|
if ext:
|
|
|
|
self.name = splitext(self.name)[0] + "." + ext
|
|
|
|
|
|
|
|
|
|
|
|
class XconvMixin:
|
|
|
|
output_factory = OutputFile
|
|
|
|
|
|
|
|
|
|
|
|
class SimpleTask(XconvMixin, advancedav.SimpleTask):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class AdvancedTask(XconvMixin, advancedav.Task):
|
|
|
|
def __init__(self, aav, output_prefix):
|
|
|
|
self.output_prefix = output_prefix
|
|
|
|
self.output_directory = dirname(output_prefix)
|
|
|
|
self.output_basename = basename(output_prefix)
|
|
|
|
super().__init__(aav)
|
|
|
|
|
|
|
|
|
|
|
|
class Manager(advancedav.MultiAV):
|
|
|
|
def _spawn_next(self, **b):
|
|
|
|
task = self.queue[0][1]
|
|
|
|
|
|
|
|
print("\033[32m Processing '%s'\033[0m" % task_name(task))
|
|
|
|
|
|
|
|
proc, f = super()._spawn_next(**b)
|
|
|
|
|
|
|
|
f.then(partial(task_done, task)).catch(partial(task_fail, task))
|
|
|
|
|
|
|
|
return proc, f
|
|
|
|
|
|
|
|
def task_done(task, res):
|
|
|
|
print("\033[32m Finished '%s'\033[0m" % task_name(task))
|
|
|
|
|
|
|
|
def task_fail(task, exc):
|
|
|
|
print("\033[31m Failed '%s': %s\033[0m" % (task_name(task), exc))
|
|
|
|
|
|
|
|
|
|
|
|
# == App ==
|
|
|
|
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 = None
|
|
|
|
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:
|
|
|
|
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 or task.outputs:
|
|
|
|
name = "ffT"
|
|
|
|
if task.inputs:
|
|
|
|
name += " <`%s`" % task.inputs[0].name
|
|
|
|
if len(task.inputs) > 1:
|
|
|
|
name += "..."
|
|
|
|
if task.outputs:
|
|
|
|
name += " >`%s`" % task.outputs[0].name
|
|
|
|
if len(task.outputs) > 1:
|
|
|
|
name += "..."
|
|
|
|
return name
|
|
|
|
else:
|
|
|
|
return "(anon task %p)" % id(task)
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv):
|
|
|
|
import logging
|
|
|
|
|
|
|
|
# Parse commandline
|
|
|
|
args = parse_args(argv)
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO)
|
|
|
|
|
|
|
|
profile = load_profile(args.profile)
|
|
|
|
|
|
|
|
print("\033[36mXConv %s (c) Taeyeon Mori\033[0m" % version)
|
|
|
|
print("\033[34mProfile: %s\033[0m" % args.profile)
|
|
|
|
|
|
|
|
unknown_defines = [n for n in args.define.keys() if n not in profile.defines]
|
|
|
|
if unknown_defines:
|
|
|
|
print("\033[33mWarning: Unknown defines %s; see '%s -i %s' for avaliable defines in this profile\033[0m" %
|
|
|
|
(", ".join(unknown_defines), argv[0], args.profile))
|
|
|
|
|
|
|
|
if args.create_directory:
|
|
|
|
makedirs(args.output_directory, exist_ok=True)
|
|
|
|
|
|
|
|
if not args.output_filename and not isdir(args.output_directory):
|
|
|
|
print("\033[31mOutput location '%s' is not a directory.\033[0m" % args.output_directory)
|
|
|
|
return -1
|
|
|
|
|
|
|
|
# Initialize AAV
|
|
|
|
aav = Manager(ffmpeg=args.ffmpeg, ffprobe=args.ffprobe, workers=args.concurrent)
|
|
|
|
|
|
|
|
if args.quiet:
|
|
|
|
aav.global_conv_args = "-loglevel", "warning"
|
|
|
|
|
|
|
|
aav.global_args += "-hide_banner",
|
|
|
|
aav.global_conv_args += "-stats",
|
|
|
|
|
|
|
|
# Collect Tasks
|
|
|
|
tasks = []
|
|
|
|
|
|
|
|
print("\033[35mCollecting Tasks..\033[0m")
|
|
|
|
|
|
|
|
if args.merge:
|
|
|
|
tasks.append(create_task(aav, profile, args.inputs, args))
|
|
|
|
|
|
|
|
elif args.concat:
|
|
|
|
import tempfile, os
|
|
|
|
tmp = tempfile.NamedTemporaryFile(mode="w", delete=False)
|
|
|
|
|
|
|
|
with tmp:
|
|
|
|
tmp.write("ffconcat version 1.0\n")
|
|
|
|
tmp.write("# XConv concat file\n")
|
|
|
|
for f in map(abspath, args.inputs):
|
|
|
|
print("\033[36m Concatenating %s\033[0m" % basename(f))
|
|
|
|
tmp.write("file '%s'\n" % f)
|
|
|
|
|
|
|
|
task = create_task(aav, profile, (), args, filename_from=args.inputs[0])
|
|
|
|
|
|
|
|
task.add_input(tmp.name).set(f="concat", safe="0")
|
|
|
|
|
|
|
|
tasks.append(task)
|
|
|
|
|
|
|
|
else:
|
|
|
|
for input in args.inputs:
|
|
|
|
tasks.append(create_task(aav, profile, (input,), args))
|
|
|
|
|
|
|
|
print("\033[35mPreparing Tasks..\033[0m")
|
|
|
|
|
|
|
|
# Prepare profile parameters
|
|
|
|
pkw = {}
|
|
|
|
if profile.defines:
|
|
|
|
pkw["defines"] = args.define
|
|
|
|
if profile.features:
|
|
|
|
if "argshax" in profile.features:
|
|
|
|
pkw["args"] = args
|
|
|
|
|
|
|
|
# Apply profile
|
|
|
|
for task in tasks:
|
|
|
|
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" % task_name(task))
|
|
|
|
return 1
|
|
|
|
|
|
|
|
if args.update:
|
|
|
|
for task in tasks[:]:
|
|
|
|
for output in [o for o in task.outputs if exists(o.filename)]:
|
|
|
|
print("\033[33m Skipping existing '%s' (--update)\033[0m\033[K" % output.name)
|
|
|
|
task.outputs.remove(output)
|
|
|
|
if not task.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")
|
|
|
|
|
|
|
|
# Paralellize
|
|
|
|
if args.concurrent > 1 and not args.merge and not args.concat:
|
|
|
|
tasks = sum([task.split(args.concurrent) for task in tasks], [])
|
|
|
|
|
|
|
|
# Commit
|
|
|
|
[t.commit2() for t in tasks]
|
|
|
|
aav.process_queue()
|
|
|
|
aav.wait()
|
|
|
|
|
|
|
|
else:
|
|
|
|
for task in tasks:
|
|
|
|
print("\033[32m Processing '%s'\033[0m" % task_name(task))
|
|
|
|
task.commit()
|
|
|
|
|
|
|
|
# Clean up
|
|
|
|
if args.concat:
|
|
|
|
os.unlink(tmp.name)
|
|
|
|
|
|
|
|
# Copy files
|
|
|
|
if args.copy_files:
|
|
|
|
print("\033[35mCopying Files..\033[0m\033[K")
|
|
|
|
for file in args.copy_files:
|
|
|
|
print("\033[32m Copying '%s'\033[0m\033[K" % basename(file))
|
|
|
|
copyfile(file, build_path(args.output_directory, basename(file)))
|
|
|
|
|
|
|
|
print("\033[35mDone.\033[0m\033[K")
|
|
|
|
|
|
|
|
return 0
|