Improved passing of multiple postprocessor-args

* Added `PP+exe:args` syntax
    If `PP+exe:args` is specifically given, only it used.
    Otherwise, `PP:args` and `exe:args` are combined.
    If none of the `PP`, `exe` or `PP+exe` args are given, `default` is used
    `Default` is purposely left undocumented since it exists only for backward compatibility

* Also added proper handling of args in `EmbedThumbnail`

Related: https://github.com/ytdl-org/youtube-dl/pull/27723
This commit is contained in:
pukkandan 2021-01-20 21:37:40 +05:30
parent 5c610515c9
commit 43820c0370
9 changed files with 89 additions and 44 deletions

View File

@ -551,18 +551,24 @@ Then simply type this
re-encoding is necessary (currently re-encoding is necessary (currently
supported: mp4|flv|ogg|webm|mkv|avi) supported: mp4|flv|ogg|webm|mkv|avi)
--postprocessor-args NAME:ARGS Give these arguments to the postprocessors. --postprocessor-args NAME:ARGS Give these arguments to the postprocessors.
Specify the postprocessor name and the Specify the postprocessor/executable name
arguments separated by a colon ':' to give and the arguments separated by a colon ':'
the argument to only the specified to give the argument to only the specified
postprocessor. Supported names are postprocessor/executable. Supported
postprocessors are: SponSkrub,
ExtractAudio, VideoRemuxer, VideoConvertor, ExtractAudio, VideoRemuxer, VideoConvertor,
EmbedSubtitle, Metadata, Merger, EmbedSubtitle, Metadata, Merger,
FixupStretched, FixupM4a, FixupM3u8, FixupStretched, FixupM4a, FixupM3u8,
SubtitlesConvertor, EmbedThumbnail, SubtitlesConvertor and EmbedThumbnail. The
XAttrMetadata, SponSkrub and Default. You supported executables are: SponSkrub,
can use this option multiple times to give FFmpeg, FFprobe, avconf, avprobe and
different arguments to different AtomicParsley. You can use this option
postprocessors multiple times to give different arguments
to different postprocessors. You can also
specify "PP+EXE:ARGS" to give the arguments
to the specified executable only when being
used by the specified postprocessor (Alias:
--ppa)
-k, --keep-video Keep the intermediate video file on disk -k, --keep-video Keep the intermediate video file on disk
after post-processing after post-processing
--no-keep-video Delete the intermediate video file after --no-keep-video Delete the intermediate video file after

View File

@ -343,10 +343,11 @@ class YoutubeDL(object):
otherwise prefer ffmpeg. otherwise prefer ffmpeg.
ffmpeg_location: Location of the ffmpeg/avconv binary; either the path ffmpeg_location: Location of the ffmpeg/avconv binary; either the path
to the binary or its containing directory. to the binary or its containing directory.
postprocessor_args: A dictionary of postprocessor names (in lower case) and a list postprocessor_args: A dictionary of postprocessor/executable keys (in lower case)
of additional command-line arguments for the postprocessor. and a list of additional command-line arguments for the
Use 'default' as the name for arguments to passed to all PP. postprocessor/executable. The dict can also have "PP+EXE" keys
which are used when the given exe is used by the given PP.
Use 'default' as the name for arguments to passed to all PP
The following options are used by the Youtube extractor: The following options are used by the Youtube extractor:
youtube_include_dash_manifest: If True (default), DASH manifests and related youtube_include_dash_manifest: If True (default), DASH manifests and related
data will be downloaded and processed by extractor. data will be downloaded and processed by extractor.

View File

@ -8,8 +8,8 @@ __license__ = 'Public Domain'
import codecs import codecs
import io import io
import os import os
import re
import random import random
import re
import sys import sys
@ -340,18 +340,18 @@ def _real_main(argv=None):
postprocessor_args = {} postprocessor_args = {}
if opts.postprocessor_args is not None: if opts.postprocessor_args is not None:
for string in opts.postprocessor_args: for string in opts.postprocessor_args:
mobj = re.match(r'(?P<pp>\w+):(?P<args>.*)$', string) mobj = re.match(r'(?P<pp>\w+(?:\+\w+)?):(?P<args>.*)$', string)
if mobj is None: if mobj is None:
if 'sponskrub' not in postprocessor_args: # for backward compatibility if 'sponskrub' not in postprocessor_args: # for backward compatibility
postprocessor_args['sponskrub'] = [] postprocessor_args['sponskrub'] = []
if opts.verbose: if opts.verbose:
write_string('[debug] Adding postprocessor args from command line option sponskrub:\n') write_string('[debug] Adding postprocessor args from command line option sponskrub: \n')
pp_name, pp_args = 'default', string pp_key, pp_args = 'default', string
else: else:
pp_name, pp_args = mobj.group('pp').lower(), mobj.group('args') pp_key, pp_args = mobj.group('pp').lower(), mobj.group('args')
if opts.verbose: if opts.verbose:
write_string('[debug] Adding postprocessor args from command line option %s:%s\n' % (pp_name, pp_args)) write_string('[debug] Adding postprocessor args from command line option %s: %s\n' % (pp_key, pp_args))
postprocessor_args[pp_name] = compat_shlex_split(pp_args) postprocessor_args[pp_key] = compat_shlex_split(pp_args)
match_filter = ( match_filter = (
None if opts.match_filter is None None if opts.match_filter is None

View File

@ -975,15 +975,18 @@ def parseOpts(overrideArguments=None):
metavar='FORMAT', dest='recodevideo', default=None, metavar='FORMAT', dest='recodevideo', default=None,
help='Re-encode the video into another format if re-encoding is necessary (currently supported: mp4|flv|ogg|webm|mkv|avi)') help='Re-encode the video into another format if re-encoding is necessary (currently supported: mp4|flv|ogg|webm|mkv|avi)')
postproc.add_option( postproc.add_option(
'--postprocessor-args', metavar='NAME:ARGS', '--postprocessor-args', '--ppa', metavar='NAME:ARGS',
dest='postprocessor_args', action='append', dest='postprocessor_args', action='append',
help=( help=(
'Give these arguments to the postprocessors. ' 'Give these arguments to the postprocessors. '
"Specify the postprocessor name and the arguments separated by a colon ':' " 'Specify the postprocessor/executable name and the arguments separated by a colon ":" '
'to give the argument to only the specified postprocessor. Supported names are ' 'to give the argument to only the specified postprocessor/executable. Supported postprocessors are: '
'ExtractAudio, VideoRemuxer, VideoConvertor, EmbedSubtitle, Metadata, Merger, FixupStretched, ' 'SponSkrub, ExtractAudio, VideoRemuxer, VideoConvertor, EmbedSubtitle, Metadata, Merger, '
'FixupM4a, FixupM3u8, SubtitlesConvertor, EmbedThumbnail, XAttrMetadata, SponSkrub and Default. ' 'FixupStretched, FixupM4a, FixupM3u8, SubtitlesConvertor and EmbedThumbnail. '
'You can use this option multiple times to give different arguments to different postprocessors')) 'The supported executables are: SponSkrub, FFmpeg, FFprobe, avconf, avprobe and AtomicParsley. '
'You can use this option multiple times to give different arguments to different postprocessors. '
'You can also specify "PP+EXE:ARGS" to give the arguments to the specified executable '
'only when being used by the specified postprocessor (Alias: --ppa)'))
postproc.add_option( postproc.add_option(
'-k', '--keep-video', '-k', '--keep-video',
action='store_true', dest='keepvideo', default=False, action='store_true', dest='keepvideo', default=False,

View File

@ -2,9 +2,9 @@ from __future__ import unicode_literals
import os import os
from ..compat import compat_str
from ..utils import ( from ..utils import (
PostProcessingError, PostProcessingError,
cli_configuration_args,
encodeFilename, encodeFilename,
) )
@ -33,8 +33,12 @@ class PostProcessor(object):
def __init__(self, downloader=None): def __init__(self, downloader=None):
self._downloader = downloader self._downloader = downloader
if not hasattr(self, 'PP_NAME'): self.PP_NAME = self.pp_key()
self.PP_NAME = self.__class__.__name__[:-2]
@classmethod
def pp_key(cls):
name = cls.__name__[:-2]
return compat_str(name[6:]) if name[:6].lower() == 'ffmpeg' else name
def to_screen(self, text, *args, **kwargs): def to_screen(self, text, *args, **kwargs):
if self._downloader: if self._downloader:
@ -84,11 +88,40 @@ class PostProcessor(object):
except Exception: except Exception:
self.report_warning(errnote) self.report_warning(errnote)
def _configuration_args(self, default=[]): def _configuration_args(self, default=[], exe=None):
args = self.get_param('postprocessor_args', {}) args = self.get_param('postprocessor_args', {})
if isinstance(args, list): # for backward compatibility pp_key = self.pp_key().lower()
args = {'default': args, 'sponskrub': []}
return cli_configuration_args(args, self.PP_NAME.lower(), args.get('default', [])) if isinstance(args, (list, tuple)): # for backward compatibility
return default if pp_key == 'sponskrub' else args
if args is None:
return default
assert isinstance(args, dict)
exe_args = None
if exe is not None:
assert isinstance(exe, compat_str)
exe = exe.lower()
specific_args = args.get('%s+%s' % (pp_key, exe))
if specific_args is not None:
assert isinstance(specific_args, (list, tuple))
return specific_args
exe_args = args.get(exe)
pp_args = args.get(pp_key) if pp_key != exe else None
if pp_args is None and exe_args is None:
default = args.get('default', default)
assert isinstance(default, (list, tuple))
return default
if pp_args is None:
pp_args = []
elif exe_args is None:
exe_args = []
assert isinstance(pp_args, (list, tuple))
assert isinstance(exe_args, (list, tuple))
return pp_args + exe_args
class AudioConversionError(PostProcessingError): class AudioConversionError(PostProcessingError):

View File

@ -24,7 +24,6 @@ class EmbedThumbnailPPError(PostProcessingError):
class EmbedThumbnailPP(FFmpegPostProcessor): class EmbedThumbnailPP(FFmpegPostProcessor):
PP_NAME = 'EmbedThumbnail'
def __init__(self, downloader=None, already_have_thumbnail=False): def __init__(self, downloader=None, already_have_thumbnail=False):
super(EmbedThumbnailPP, self).__init__(downloader) super(EmbedThumbnailPP, self).__init__(downloader)
@ -102,6 +101,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
encodeFilename(thumbnail_filename, True), encodeFilename(thumbnail_filename, True),
encodeArgument('-o'), encodeArgument('-o'),
encodeFilename(temp_filename, True)] encodeFilename(temp_filename, True)]
cmd += [encodeArgument(o) for o in self._configuration_args(exe='AtomicParsley')]
self.to_screen('Adding thumbnail to "%s"' % filename) self.to_screen('Adding thumbnail to "%s"' % filename)
self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd)) self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))

View File

@ -11,12 +11,15 @@ from ..utils import (
class ExecAfterDownloadPP(PostProcessor): class ExecAfterDownloadPP(PostProcessor):
PP_NAME = 'Exec'
def __init__(self, downloader, exec_cmd): def __init__(self, downloader, exec_cmd):
super(ExecAfterDownloadPP, self).__init__(downloader) super(ExecAfterDownloadPP, self).__init__(downloader)
self.exec_cmd = exec_cmd self.exec_cmd = exec_cmd
@classmethod
def pp_key(cls):
return 'Exec'
def run(self, information): def run(self, information):
cmd = self.exec_cmd cmd = self.exec_cmd
if '{}' not in cmd: if '{}' not in cmd:

View File

@ -54,8 +54,6 @@ class FFmpegPostProcessorError(PostProcessingError):
class FFmpegPostProcessor(PostProcessor): class FFmpegPostProcessor(PostProcessor):
def __init__(self, downloader=None): def __init__(self, downloader=None):
if not hasattr(self, 'PP_NAME'):
self.PP_NAME = self.__class__.__name__[6:-2] # Remove ffmpeg from the front
PostProcessor.__init__(self, downloader) PostProcessor.__init__(self, downloader)
self._determine_executables() self._determine_executables()
@ -209,7 +207,7 @@ class FFmpegPostProcessor(PostProcessor):
oldest_mtime = min( oldest_mtime = min(
os.stat(encodeFilename(path)).st_mtime for path in input_paths) os.stat(encodeFilename(path)).st_mtime for path in input_paths)
opts += self._configuration_args() opts += self._configuration_args(exe=self.basename)
files_cmd = [] files_cmd = []
for path in input_paths: for path in input_paths:

View File

@ -9,6 +9,7 @@ from ..utils import (
encodeArgument, encodeArgument,
encodeFilename, encodeFilename,
shell_quote, shell_quote,
str_or_none,
PostProcessingError, PostProcessingError,
prepend_extension, prepend_extension,
) )
@ -16,15 +17,13 @@ from ..utils import (
class SponSkrubPP(PostProcessor): class SponSkrubPP(PostProcessor):
_temp_ext = 'spons' _temp_ext = 'spons'
_def_args = []
_exe_name = 'sponskrub' _exe_name = 'sponskrub'
def __init__(self, downloader, path='', args=None, ignoreerror=False, cut=False, force=False): def __init__(self, downloader, path='', args=None, ignoreerror=False, cut=False, force=False):
PostProcessor.__init__(self, downloader) PostProcessor.__init__(self, downloader)
self.force = force self.force = force
self.cutout = cut self.cutout = cut
self.args = ['-chapter'] if not cut else [] self.args = str_or_none(args) or '' # For backward compatibility
self.args += self._configuration_args(self._def_args) if args is None else compat_shlex_split(args)
self.path = self.get_exe(path) self.path = self.get_exe(path)
if not ignoreerror and self.path is None: if not ignoreerror and self.path is None:
@ -64,9 +63,11 @@ class SponSkrubPP(PostProcessor):
if os.path.exists(encodeFilename(temp_filename)): if os.path.exists(encodeFilename(temp_filename)):
os.remove(encodeFilename(temp_filename)) os.remove(encodeFilename(temp_filename))
cmd = [self.path] cmd = [self.path]
if self.args: if not self.cutout:
cmd += self.args cmd += ['-chapter']
cmd += compat_shlex_split(self.args) # For backward compatibility
cmd += self._configuration_args(exe=self._exe_name)
cmd += ['--', information['id'], filename, temp_filename] cmd += ['--', information['id'], filename, temp_filename]
cmd = [encodeArgument(i) for i in cmd] cmd = [encodeArgument(i) for i in cmd]