parent
04c609c152
commit
a1fe0e7401
11 changed files with 739 additions and 0 deletions
@ -0,0 +1,26 @@ |
||||
#!/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/>. |
||||
""" |
||||
|
||||
version_info = 0, 2, 0 |
@ -0,0 +1,30 @@ |
||||
#!/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 .app import main |
||||
|
||||
from sys import argv, exit |
||||
|
||||
exit(main(argv)) |
@ -0,0 +1,171 @@ |
||||
#!/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 |
||||
|
||||
|
||||
# == 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 SimpleTask(advancedav.SimpleTask): |
||||
output_factory = OutputFile |
||||
|
||||
|
||||
# == 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)) |
||||
else: |
||||
return args.output |
||||
|
||||
|
||||
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) |
||||
|
||||
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 = advancedav.SimpleAV(ffmpeg=args.ffmpeg, ffprobe=args.ffprobe) |
||||
|
||||
if args.quiet: |
||||
aav.global_conv_args = "-loglevel", "warning" |
||||
|
||||
aav.global_args += "-hide_banner", |
||||
|
||||
# Collect Tasks |
||||
tasks = [] |
||||
|
||||
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) |
||||
|
||||
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 = SimpleTask(aav, make_outfile(args, profile, args.inputs[0]), profile.container) |
||||
|
||||
options(task.add_input(tmp.name), |
||||
f="concat", |
||||
safe="0") |
||||
|
||||
tasks.append(task) |
||||
|
||||
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) |
||||
|
||||
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" % basename(task.name), 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)) |
||||
return 1 |
||||
|
||||
print("\033[35mExecuting Tasks..\033[0m\033[K") |
||||
|
||||
# Commit |
||||
for task in tasks: |
||||
print("\033[32m Processing '%s'\033[0m" % basename(task.name)) |
||||
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 |
@ -0,0 +1,196 @@ |
||||
#!/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 |
||||
|
||||
|
||||
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(" %-20s %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) |
||||
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 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") |
||||
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 |
@ -0,0 +1,123 @@ |
||||
#!/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/>. |
||||
----------------------------------------------------------- |
||||
Decorators for defining xconv profiles |
||||
""" |
||||
|
||||
from .profileman import index |
||||
|
||||
from functools import wraps |
||||
|
||||
|
||||
__all__ = [ |
||||
"profile", |
||||
"description", |
||||
"output", |
||||
"defines", |
||||
"features", |
||||
"singleaudio" |
||||
] |
||||
|
||||
# == Misc. helpers == |
||||
def __update(f, name, update): |
||||
if hasattr(f, name): |
||||
getattr(f, name).update(update); |
||||
else: |
||||
setattr(f, name, update) |
||||
|
||||
def __defaults(obj, **defs): |
||||
for k, v in defs.items(): |
||||
if not hasattr(obj, k): |
||||
setattr(obj, k, v) |
||||
|
||||
|
||||
# == Profile Decorators == |
||||
def profile(f): |
||||
""" |
||||
Define a XConv Profile |
||||
|
||||
Note: Should be outermost decorator |
||||
""" |
||||
__defaults(f, |
||||
description=None, |
||||
container=None, |
||||
ext=None, |
||||
defines={}, |
||||
features={}) |
||||
index[f.__name__] = f |
||||
return f |
||||
|
||||
|
||||
def description(desc): |
||||
""" Add a profile description """ |
||||
def apply(f): |
||||
f.description = desc |
||||
return f |
||||
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): |
||||
__update(f, "defines", defs) |
||||
return f |
||||
return apply |
||||
|
||||
|
||||
def features(**features): |
||||
""" Set opaque feature flags """ |
||||
def apply(f): |
||||
__update(f, "features", features) |
||||
return f |
||||
return apply |
||||
|
||||
|
||||
def singleaudio(profile): |
||||
""" |
||||
Operate on a single audio stream (The first one found) |
||||
|
||||
The stream will be passed to the decorated function in the "stream" keyword |
||||
""" |
||||
@wraps(profile) |
||||
def wrapper(task, **kwds): |
||||
try: |
||||
audio_stream = next(task.iter_audio_streams()) |
||||
except StopIteration: |
||||
print("No audio track in '%s'" % "', '".join(map(lambda x: x.name, task.inputs))) |
||||
return False |
||||
return profile(task, stream=audio_stream, **kwds) |
||||
__update(wrapper, "features", {"singleaudio": None}) |
||||
return wrapper |
@ -0,0 +1,65 @@ |
||||
#!/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 .profiles import __path__ as profilepaths |
||||
|
||||
from importlib import import_module |
||||
from pathlib import Path |
||||
|
||||
|
||||
# == Profile Index == |
||||
index = {} |
||||
|
||||
|
||||
def load_profile(name): |
||||
if name in index: |
||||
return index[name] |
||||
|
||||
try: |
||||
import_module(".profiles.%s" % name, __package__) |
||||
except ImportError as e: |
||||
print(e) |
||||
pass |
||||
|
||||
if name in index: |
||||
return index[name] |
||||
|
||||
load_all_profiles() |
||||
|
||||
if name in index: |
||||
return index[name] |
||||
|
||||
raise ImportError("Could not find XConv profile '%s'" % name) |
||||
|
||||
|
||||
def load_all_profiles(): |
||||
for location in profilepaths: |
||||
for mod in (x for x in Path(location).iterdir() if x.is_file() and x.suffix == ".py"): |
||||
try: |
||||
import_module(".profiles.%s" % mod.stem, __package__) |
||||
except ImportError: |
||||
pass |
||||
|
||||
return index |
@ -0,0 +1,55 @@ |
||||
#!/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 ..profile import * |
||||
|
||||
|
||||
@profile |
||||
@description("Encode Opus Audiobook") |
||||
@output(container="ogg", ext="ogg") |
||||
@defines(stereo="Use two channels", |
||||
bitrate="Use custom target bitrate", |
||||
voip="Use voice optimization", |
||||
fancy="Use 48kbps stereo (For dramatic audiobooks with a lot of music and effects)") |
||||
@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"]) |
||||
return True |
@ -0,0 +1,14 @@ |
||||
from ..profile import * |
||||
|
||||
|
||||
@profile |
||||
@description("Save the first audio track as FLAC.") |
||||
@output(ext="flac") |
||||
@singleaudio |
||||
def flac(task, stream): |
||||
if stream.codec == "ape": |
||||
stream.file.set(max_samples="all") # Monkey's insane preset is insane. |
||||
(task.map_stream(stream) |
||||
.set(codec="flac", |
||||
compression_level="10")) |
||||
return True |
@ -0,0 +1,26 @@ |
||||
from ..profile import * |
||||
|
||||
|
||||
@profile |
||||
@description("First Video H.264 Main fastdecode animation, max 1280x800; Audio AAC; Keep subtitles") |
||||
@output(container="matroska", ext="mkv") |
||||
def laptop(task): |
||||
# add first video stream |
||||
for s in task.iter_video_streams(): |
||||
(task.map_stream(s) |
||||
.set(codec="libx264", |
||||
tune=("fastdecode", "animation"), |
||||
profile="main", |
||||
preset="fast") |
||||
.downscale(1280, 800)) |
||||
break |
||||
# Add all audio streams (reencode to aac if necessary) |
||||
for s in task.iter_audio_streams(): |
||||
os = task.map_stream(s) |
||||
if s.codec != "aac": |
||||
os.set(codec="aac") |
||||
# add all subtitle and attachment streams |
||||
for s in chain(task.iter_subtitle_streams(), task.iter_attachment_streams()): |
||||
task.map_stream(s) |
||||
# go |
||||
return True |
@ -0,0 +1,20 @@ |
||||
from ..profile import * |
||||
|
||||
|
||||
@profile |
||||
@description("Encode Opus Audio") |
||||
@output(ext="mka", container="matroska") |
||||
@defines(ogg="Use Ogg/Opus output container", |
||||
bitrate="Target bitrate (Default 96k)") |
||||
@features(argshax=None) |
||||
@singleaudio |
||||
def opus(task, stream, defines, args): |
||||
os = (task.map_stream(stream) |
||||
.set(codec="libopus", |
||||
vbr="on") |
||||
# Defines |
||||
.apply(defines, bitrate="b")) |
||||
# Output format |
||||
if "ogg" in defines: |
||||
task.change_format("ogg", "opus" if args.genout else None) |
||||
return True |
@ -0,0 +1,13 @@ |
||||
from ..profile import * |
||||
|
||||
|
||||
@profile |
||||
@description("Copy all streams to a new container") |
||||
@defines(format="Container format", |
||||
fext="File extension") |
||||
@features(argshax=None) |
||||
def remux(task, defines, args): |
||||
task.change_format( |
||||
defines["format"] if "format" in defines else None, |
||||
defines["fext"] if "fext" in defines and args.genout else None) |
||||
return all(task.map_stream(s) for s in task.iter_streams()) |
Loading…
Reference in new issue