You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
10 KiB
200 lines
10 KiB
#!/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", type=int, 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
|
|
|