|
|
|
#!/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/>.
|
|
|
|
-----------------------------------------------------------
|
|
|
|
Commandline Parsing
|
|
|
|
"""
|
|
|
|
|
|
|
|
from .profileman import load_all_profiles, load_profile
|
|
|
|
from . import version_info
|
|
|
|
|
|
|
|
from advancedav import version_info as aav_version_info
|
|
|
|
|
|
|
|
from argparse import ArgumentParser, Action
|
|
|
|
from pathlib import Path
|
|
|
|
from os.path import basename
|
|
|
|
from multiprocessing import cpu_count
|
|
|
|
|
|
|
|
|
|
|
|
version = "%s (AdvancedAV %s)" % (".".join(map(str, version_info)), ".".join(map(str, aav_version_info)))
|
|
|
|
|
|
|
|
|
|
|
|
# == Support code ==
|
|
|
|
class TerminalAction(Action):
|
|
|
|
def __init__(self, option_strings, dest, nargs=0, default=None, **kwargs):
|
|
|
|
super().__init__(option_strings, dest, nargs=nargs, default=default or {}, **kwargs)
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
self.run(parser, *values)
|
|
|
|
parser.exit()
|
|
|
|
|
|
|
|
|
|
|
|
class ProfilesAction(TerminalAction):
|
|
|
|
def run(self, parser):
|
|
|
|
print("Available Profiles:")
|
|
|
|
for name, profile in sorted(load_all_profiles().items()):
|
|
|
|
print(" %-25s %s" % (name, profile.description if profile.description else ""))
|
|
|
|
|
|
|
|
|
|
|
|
class ProfileInfoAction(TerminalAction):
|
|
|
|
def __init__(self, option_strings, dest, nargs=1, default=None, **kwargs):
|
|
|
|
super().__init__(option_strings, dest, nargs=nargs, default=default or {}, **kwargs)
|
|
|
|
def run(self, parser, profile_name):
|
|
|
|
profile = load_profile(profile_name)
|
|
|
|
print("Profile '%s':" % profile_name)
|
|
|
|
if profile.description:
|
|
|
|
print(" Description: %s" % profile.description)
|
|
|
|
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:
|
|
|
|
print(" Supported defines:")
|
|
|
|
for define in sorted(profile.defines.items()):
|
|
|
|
print(" %s: %s" % define)
|
|
|
|
|
|
|
|
|
|
|
|
class DefineAction(Action):
|
|
|
|
def __init__(self, option_strings, dest, nargs=1, default=None, **kwargs):
|
|
|
|
super().__init__(option_strings, dest, nargs=nargs, default=default or {}, **kwargs)
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
value = values[0]
|
|
|
|
dest = getattr(namespace, self.dest)
|
|
|
|
if "=" in value:
|
|
|
|
k, v = value.split("=")
|
|
|
|
dest[k] = v
|
|
|
|
else:
|
|
|
|
dest[value] = True
|
|
|
|
|
|
|
|
|
|
|
|
class ExtendAction(Action):
|
|
|
|
def __init__(self, option_strings, dest, nargs="+", default=None, **kwargs):
|
|
|
|
super().__init__(option_strings, dest, nargs=nargs, default=default, **kwargs)
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
|
|
items = getattr(namespace, self.dest) or []
|
|
|
|
items.extend(values)
|
|
|
|
setattr(namespace, self.dest, items)
|
|
|
|
|
|
|
|
|
|
|
|
def parse_args(argv):
|
|
|
|
prog = basename(argv[0])
|
|
|
|
|
|
|
|
if prog == "__main__.py":
|
|
|
|
prog = "python -m xconv"
|
|
|
|
|
|
|
|
parser = ArgumentParser(prog=prog,
|
|
|
|
usage="""%(prog)s [-h | -l | -i PROFILE]
|
|
|
|
%(prog)s [option]... -p PROFILE [-DNAME[=VALUE]]... [-B] [-T] input output
|
|
|
|
%(prog)s [option]... -p PROFILE [-DNAME[=VALUE]]... -M [-T] inputs... output
|
|
|
|
%(prog)s [option]... -p PROFILE [-DNAME[=VALUE]]... -C [-T] inputs... output
|
|
|
|
%(prog)s [option]... -p PROFILE [-DNAME[=VALUE]]... [-B] inputs... directory
|
|
|
|
%(prog)s [option]... -p PROFILE [-DNAME[=VALUE]]... [-B] -t directory inputs...""",
|
|
|
|
description="""FFmpeg wrapper based on AdvancedAV""")
|
|
|
|
|
|
|
|
parser.add_argument("-V", "--version", help="Show version and quit", action="version",
|
|
|
|
version="""XConv %s""" % version)
|
|
|
|
|
|
|
|
# Available Options
|
|
|
|
parser.add_argument("-v", "--verbose", help="Enable verbose output", action="store_true")
|
|
|
|
parser.add_argument("-q", "--quiet", help="Be less verbose", action="store_true")
|
|
|
|
parser.add_argument("-j", "--concurrent", help="Run ffmpeg concurrently using at most N instances [%(default)s]", metavar="N", default=cpu_count())
|
|
|
|
profile = parser.add_argument_group("Profile")
|
|
|
|
profile.add_argument("-l", "--list-profiles", help="List profiles and quit", action=ProfilesAction)
|
|
|
|
profile.add_argument("-i", "--profile-info", help="Give info about a profile and quit", metavar="PROFILE", action=ProfileInfoAction)
|
|
|
|
profile.add_argument("-p", "--profile", help="Specify the profile", metavar="PROFILE", required=True)
|
|
|
|
profile.add_argument("-D", "--define", help="Define an option to be used by the profile", metavar="NAME[=VALUE]", action=DefineAction)
|
|
|
|
mode = parser.add_argument_group("Mode").add_mutually_exclusive_group()
|
|
|
|
mode.add_argument("-B", "--batch", help="Batch process every input file into an output file (default)", action="store_true")
|
|
|
|
mode.add_argument("-M", "--merge", help="Merge streams from all inputs", action="store_true")
|
|
|
|
mode.add_argument("-C", "--concat", help="Concatenate streams from inputs", action="store_true")
|
|
|
|
files = parser.add_argument_group("Files")
|
|
|
|
files.add_argument("inputs", help="The input file(s)", nargs="+")
|
|
|
|
files.add_argument("output", help="The output filename or directory (unless -t is given)", nargs="?") # always empty
|
|
|
|
files.add_argument("-u", "--update", help="Only work on files that don't already exist", action="store_true")
|
|
|
|
files.add_argument("-c", "--create-directory", help="Create directories if they don't exist", action="store_true")
|
|
|
|
target = files.add_mutually_exclusive_group()
|
|
|
|
target.add_argument("-t", "--target-directory", help="Output into a directory", metavar="DIRECTORY", type=Path)
|
|
|
|
target.add_argument("-T", "--no-target-directory", help="Treat output as a normal file", action="store_true")
|
|
|
|
files.add_argument("-S", "--subdirectory", help="Work in a subdirectory of here and -t (use glob patterns for inputs)")
|
|
|
|
files.add_argument("-K", "--copy-files", help="Copy all following files unmodified", metavar="FILE", action=ExtendAction)
|
|
|
|
progs = parser.add_argument_group("Programs")
|
|
|
|
progs.add_argument("--ffmpeg", help="Path to the ffmpeg executable", default="ffmpeg")
|
|
|
|
progs.add_argument("--ffprobe", help="Path to the ffprobe executable", default="ffprobe")
|
|
|
|
|
|
|
|
# Parse arguments
|
|
|
|
args = parser.parse_args(argv[1:])
|
|
|
|
|
|
|
|
# Figure out output path
|
|
|
|
# ----------------------
|
|
|
|
# Fill in args.output
|
|
|
|
# args.output will never be filled in by argparse, since inputs consumes everything
|
|
|
|
if args.target_directory:
|
|
|
|
args.output = args.target_directory
|
|
|
|
elif len(args.inputs) < 2:
|
|
|
|
parser.error("Neither --target-directory nor output is given")
|
|
|
|
else:
|
|
|
|
args.output = Path(args.inputs.pop(-1))
|
|
|
|
|
|
|
|
if args.subdirectory:
|
|
|
|
subdir = Path(args.subdirectory)#.resolve()
|
|
|
|
outdir = Path(args.output, args.subdirectory)#.resolve()
|
|
|
|
|
|
|
|
if outdir.exists() and not outdir.is_dir():
|
|
|
|
parser.error("--subdirectory only works with output directories. '%s' exists and isn't a directory")
|
|
|
|
|
|
|
|
inputs = args.inputs
|
|
|
|
args.inputs = []
|
|
|
|
for pattern in inputs:
|
|
|
|
args.inputs.extend(subdir.glob(pattern))
|
|
|
|
|
|
|
|
files = args.copy_files
|
|
|
|
args.copy_files = []
|
|
|
|
for pattern in files:
|
|
|
|
args.copy_files.extend(subdir.glob(pattern))
|
|
|
|
|
|
|
|
args.output_directory = args.output = outdir
|
|
|
|
args.output_filename = None
|
|
|
|
|
|
|
|
else:
|
|
|
|
# Check if we're outputting to a directory
|
|
|
|
multiple_outputs = args.copy_files or not (args.merge or args.concat) and len(args.inputs) > 1
|
|
|
|
|
|
|
|
if args.target_directory or args.output.is_dir() or multiple_outputs:
|
|
|
|
if args.no_target_directory:
|
|
|
|
if multiple_outputs:
|
|
|
|
parser.error("Passed --no-target-directory, but operation would have multiple outputs. (See --merge or --concat)")
|
|
|
|
else:
|
|
|
|
parser.error("Passed --no-target-directory, but '%s' is an existing directory." % args.output)
|
|
|
|
args.output_filename = None
|
|
|
|
args.output_directory = args.output
|
|
|
|
else:
|
|
|
|
args.output_filename = args.output.name
|
|
|
|
args.output_directory = args.output.parent
|
|
|
|
|
|
|
|
return args
|