Allow passing different arguments to different postprocessors

* Also deprecated --sponskrub-args

Closes: https://github.com/ytdl-org/youtube-dl/issues/27593
Eg: `--postprocessor-args "VideoConvertor:-c:v h264_nvenc -preset slow"`
Eg: `--postprocessor-args "SponsKrub:-include-selfpromo"`

For backward compatibility, `--postprocessor-args args` is equivalent to:
`--post-processor-args "sponskrub:" --post-processor-args "default:args"`
This commit is contained in:
pukkandan 2021-01-08 00:58:41 +05:30
parent 6c40e33c9e
commit 1b77b347d4
11 changed files with 96 additions and 58 deletions

View file

@ -550,7 +550,18 @@ ## Post-Processing Options:
--recode-video FORMAT Re-encode the video into another format if
re-encoding is necessary (currently
supported: mp4|flv|ogg|webm|mkv|avi)
--postprocessor-args ARGS Give these arguments to the postprocessor
--postprocessor-args NAME:ARGS Give these arguments to the postprocessors.
Specify the postprocessor name and the
arguments separated by a colon ':' to give
the argument to only the specified
postprocessor. Supported names are
ExtractAudio, VideoRemuxer, VideoConvertor,
EmbedSubtitle, Metadata, Merger,
FixupStretched, FixupM4a, FixupM3u8,
SubtitlesConvertor, SponSkrub and Default.
You can use this option multiple times to
give different arguments to different
postprocessors
-k, --keep-video Keep the intermediate video file on disk
after post-processing
--no-keep-video Delete the intermediate video file after

View file

@ -333,8 +333,9 @@ class YoutubeDL(object):
otherwise prefer ffmpeg.
ffmpeg_location: Location of the ffmpeg/avconv binary; either the path
to the binary or its containing directory.
postprocessor_args: A list of additional command-line arguments for the
postprocessor.
postprocessor_args: A dictionary of postprocessor names (in lower case) and a list
of additional command-line arguments for the postprocessor.
Use 'default' as the name for arguments to passed to all PP.
The following options are used by the Youtube extractor:
youtube_include_dash_manifest: If True (default), DASH manifests and related

View file

@ -331,9 +331,23 @@ def parse_retries(retries):
external_downloader_args = None
if opts.external_downloader_args:
external_downloader_args = compat_shlex_split(opts.external_downloader_args)
postprocessor_args = None
if opts.postprocessor_args:
postprocessor_args = compat_shlex_split(opts.postprocessor_args)
postprocessor_args = {}
if opts.postprocessor_args is not None:
for string in opts.postprocessor_args:
mobj = re.match(r'(?P<pp>\w+):(?P<args>.*)$', string)
if mobj is None:
if 'sponskrub' not in postprocessor_args: # for backward compatibility
postprocessor_args['sponskrub'] = []
if opts.verbose:
write_string('[debug] Adding postprocessor args from command line option sponskrub:\n')
pp_name, pp_args = 'default', string
else:
pp_name, pp_args = mobj.group('pp').lower(), mobj.group('args')
if opts.verbose:
write_string('[debug] Adding postprocessor args from command line option %s:%s\n' % (pp_name, pp_args))
postprocessor_args[pp_name] = compat_shlex_split(pp_args)
match_filter = (
None if opts.match_filter is None
else match_filter_func(opts.match_filter))

View file

@ -970,9 +970,14 @@ def _comma_separated_values_options_callback(option, opt_str, value, parser):
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)')
postproc.add_option(
'--postprocessor-args',
dest='postprocessor_args', metavar='ARGS',
help='Give these arguments to the postprocessor')
'--postprocessor-args', metavar='NAME:ARGS',
dest='postprocessor_args', action='append',
help=(
'Give these arguments to the postprocessors. '
"Specify the postprocessor name and the arguments separated by a colon ':' "
'to give the argument to only the specified postprocessor. Supported names are '
'ExtractAudio, VideoRemuxer, VideoConvertor, EmbedSubtitle, Metadata, Merger, FixupStretched, FixupM4a, FixupM3u8, SubtitlesConvertor, SponSkrub and Default'
'. You can use this option multiple times to give different arguments to different postprocessors'))
postproc.add_option(
'-k', '--keep-video',
action='store_true', dest='keepvideo', default=False,
@ -1089,7 +1094,7 @@ def _comma_separated_values_options_callback(option, opt_str, value, parser):
help='Location of the sponskrub binary; either the path to the binary or its containing directory.')
sponskrub.add_option(
'--sponskrub-args', dest='sponskrub_args', metavar='ARGS',
help='Give these arguments to sponskrub')
help=optparse.SUPPRESS_HELP)
extractor = optparse.OptionGroup(parser, 'Extractor Options')
extractor.add_option(

View file

@ -33,6 +33,11 @@ class PostProcessor(object):
def __init__(self, downloader=None):
self._downloader = downloader
if not hasattr(self, 'PP_NAME'):
self.PP_NAME = self.__class__.__name__[:-2]
def to_screen(self, text, *args, **kwargs):
return self._downloader.to_screen('[%s] %s' % (self.PP_NAME, text), *args, **kwargs)
def set_downloader(self, downloader):
"""Sets the downloader for this PP."""
@ -62,7 +67,10 @@ def try_utime(self, path, atime, mtime, errnote='Cannot update utime of file'):
self._downloader.report_warning(errnote)
def _configuration_args(self, default=[]):
return cli_configuration_args(self._downloader.params, 'postprocessor_args', default)
args = self._downloader.params.get('postprocessor_args', {})
if isinstance(args, list): # for backward compatibility
args = {'default': args, 'sponskrub': []}
return cli_configuration_args(args, self.PP_NAME.lower(), args.get('default', []))
class AudioConversionError(PostProcessingError):

View file

@ -23,6 +23,8 @@ class EmbedThumbnailPPError(PostProcessingError):
class EmbedThumbnailPP(FFmpegPostProcessor):
PP_NAME = 'EmbedThumbnail'
def __init__(self, downloader=None, already_have_thumbnail=False):
super(EmbedThumbnailPP, self).__init__(downloader)
self._already_have_thumbnail = already_have_thumbnail
@ -32,7 +34,7 @@ def run(self, info):
temp_filename = prepend_extension(filename, 'temp')
if not info.get('thumbnails'):
self._downloader.to_screen('[embedthumbnail] There aren\'t any thumbnails to embed')
self.to_screen('There aren\'t any thumbnails to embed')
return [], info
thumbnail_filename = info['thumbnails'][-1]['filename']
@ -52,8 +54,7 @@ def is_webp(path):
if thumbnail_ext:
thumbnail_ext = thumbnail_ext[1:].lower()
if thumbnail_ext != 'webp' and is_webp(thumbnail_filename):
self._downloader.to_screen(
'[ffmpeg] Correcting extension to webp and escaping path for thumbnail "%s"' % thumbnail_filename)
self.to_screen('Correcting extension to webp and escaping path for thumbnail "%s"' % thumbnail_filename)
thumbnail_webp_filename = replace_extension(thumbnail_filename, 'webp')
os.rename(encodeFilename(thumbnail_filename), encodeFilename(thumbnail_webp_filename))
thumbnail_filename = thumbnail_webp_filename
@ -66,7 +67,7 @@ def is_webp(path):
escaped_thumbnail_filename = thumbnail_filename.replace('%', '#')
os.rename(encodeFilename(thumbnail_filename), encodeFilename(escaped_thumbnail_filename))
escaped_thumbnail_jpg_filename = replace_extension(escaped_thumbnail_filename, 'jpg')
self._downloader.to_screen('[ffmpeg] Converting thumbnail "%s" to JPEG' % escaped_thumbnail_filename)
self.to_screen('Converting thumbnail "%s" to JPEG' % escaped_thumbnail_filename)
self.run_ffmpeg(escaped_thumbnail_filename, escaped_thumbnail_jpg_filename, ['-bsf:v', 'mjpeg2jpeg'])
os.remove(encodeFilename(escaped_thumbnail_filename))
thumbnail_jpg_filename = replace_extension(thumbnail_filename, 'jpg')
@ -79,7 +80,7 @@ def is_webp(path):
'-c', 'copy', '-map', '0:0', '-map', '1:0', '-id3v2_version', '3',
'-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment="Cover (front)"']
self._downloader.to_screen('[ffmpeg] Adding thumbnail to "%s"' % filename)
self.to_screen('Adding thumbnail to "%s"' % filename)
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
@ -99,7 +100,7 @@ def is_webp(path):
'-c', 'copy', '-map', '0', '-dn',
'-attach', thumbnail_filename, '-metadata:s:t', 'mimetype=image/jpeg']
self._downloader.to_screen('[ffmpeg] Adding thumbnail to "%s"' % filename)
self.to_screen('Adding thumbnail to "%s"' % filename)
self.run_ffmpeg_multiple_files([filename], temp_filename, options)
@ -121,7 +122,7 @@ def is_webp(path):
encodeArgument('-o'),
encodeFilename(temp_filename, True)]
self._downloader.to_screen('[atomicparsley] Adding thumbnail to "%s"' % filename)
self.to_screen('Adding thumbnail to "%s"' % filename)
if self._downloader.params.get('verbose', False):
self._downloader.to_screen('[debug] AtomicParsley command line: %s' % shell_quote(cmd))

View file

@ -11,6 +11,8 @@
class ExecAfterDownloadPP(PostProcessor):
PP_NAME = 'Exec'
def __init__(self, downloader, exec_cmd):
super(ExecAfterDownloadPP, self).__init__(downloader)
self.exec_cmd = exec_cmd
@ -22,7 +24,7 @@ def run(self, information):
cmd = cmd.replace('{}', compat_shlex_quote(information['filepath']))
self._downloader.to_screen('[exec] Executing command: %s' % cmd)
self.to_screen('Executing command: %s' % cmd)
retCode = subprocess.call(encodeArgument(cmd), shell=True)
if retCode != 0:
raise PostProcessingError(

View file

@ -53,6 +53,8 @@ class FFmpegPostProcessorError(PostProcessingError):
class FFmpegPostProcessor(PostProcessor):
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)
self._determine_executables()
@ -328,11 +330,11 @@ def run(self, information):
# If we download foo.mp3 and convert it to... foo.mp3, then don't delete foo.mp3, silly.
if (new_path == path
or (self._nopostoverwrites and os.path.exists(encodeFilename(new_path)))):
self._downloader.to_screen('[ffmpeg] Post-process file %s exists, skipping' % new_path)
self.to_screen('Post-process file %s exists, skipping' % new_path)
return [], information
try:
self._downloader.to_screen('[ffmpeg] Destination: ' + new_path)
self.to_screen('Destination: ' + new_path)
self.run_ffmpeg(path, new_path, acodec, more_opts)
except AudioConversionError as e:
raise PostProcessingError(
@ -357,12 +359,12 @@ def __init__(self, downloader=None, preferedformat=None):
def run(self, information):
path = information['filepath']
if information['ext'] == self._preferedformat:
self._downloader.to_screen('[ffmpeg] Not remuxing video file %s - already is in target format %s' % (path, self._preferedformat))
self.to_screen('Not remuxing video file %s - already is in target format %s' % (path, self._preferedformat))
return [], information
options = ['-c', 'copy', '-map', '0', '-dn']
prefix, sep, ext = path.rpartition('.')
outpath = prefix + sep + self._preferedformat
self._downloader.to_screen('[' + 'ffmpeg' + '] Remuxing video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath)
self.to_screen('Remuxing video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath)
self.run_ffmpeg(path, outpath, options)
information['filepath'] = outpath
information['format'] = self._preferedformat
@ -378,14 +380,14 @@ def __init__(self, downloader=None, preferedformat=None):
def run(self, information):
path = information['filepath']
if information['ext'] == self._preferedformat:
self._downloader.to_screen('[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
self.to_screen('Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
return [], information
options = []
if self._preferedformat == 'avi':
options.extend(['-c:v', 'libxvid', '-vtag', 'XVID'])
prefix, sep, ext = path.rpartition('.')
outpath = prefix + sep + self._preferedformat
self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath)
self.to_screen('Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath)
self.run_ffmpeg(path, outpath, options)
information['filepath'] = outpath
information['format'] = self._preferedformat
@ -396,11 +398,11 @@ def run(self, information):
class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
def run(self, information):
if information['ext'] not in ('mp4', 'webm', 'mkv'):
self._downloader.to_screen('[ffmpeg] Subtitles can only be embedded in mp4, webm or mkv files')
self.to_screen('Subtitles can only be embedded in mp4, webm or mkv files')
return [], information
subtitles = information.get('requested_subtitles')
if not subtitles:
self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to embed')
self.to_screen('There aren\'t any subtitles to embed')
return [], information
filename = information['filepath']
@ -413,14 +415,14 @@ def run(self, information):
for lang, sub_info in subtitles.items():
sub_ext = sub_info['ext']
if sub_ext == 'json':
self._downloader.to_screen('[ffmpeg] JSON subtitles cannot be embedded')
self.to_screen('JSON subtitles cannot be embedded')
elif ext != 'webm' or ext == 'webm' and sub_ext == 'vtt':
sub_langs.append(lang)
sub_filenames.append(subtitles_filename(filename, lang, sub_ext, ext))
else:
if not webm_vtt_warn and ext == 'webm' and sub_ext != 'vtt':
webm_vtt_warn = True
self._downloader.to_screen('[ffmpeg] Only WebVTT subtitles can be embedded in webm files')
self.to_screen('Only WebVTT subtitles can be embedded in webm files')
if not sub_langs:
return [], information
@ -444,7 +446,7 @@ def run(self, information):
opts.extend(['-metadata:s:s:%d' % i, 'language=%s' % lang_code])
temp_filename = prepend_extension(filename, 'temp')
self._downloader.to_screen('[ffmpeg] Embedding subtitles in \'%s\'' % filename)
self.to_screen('Embedding subtitles in \'%s\'' % filename)
self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
os.remove(encodeFilename(filename))
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
@ -492,7 +494,7 @@ def add(meta_list, info_list=None):
add('episode_sort', 'episode_number')
if not metadata:
self._downloader.to_screen('[ffmpeg] There isn\'t any metadata to add')
self.to_screen('There isn\'t any metadata to add')
return [], info
filename = info['filepath']
@ -527,7 +529,7 @@ def ffmpeg_escape(text):
in_filenames.append(metadata_filename)
options.extend(['-map_metadata', '1'])
self._downloader.to_screen('[ffmpeg] Adding metadata to \'%s\'' % filename)
self.to_screen('Adding metadata to \'%s\'' % filename)
self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options)
if chapters:
os.remove(metadata_filename)
@ -546,7 +548,7 @@ def run(self, info):
args.extend(['-map', '%u:a:0' % (i)])
if fmt.get('vcodec') != 'none':
args.extend(['-map', '%u:v:0' % (i)])
self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename)
self.to_screen('Merging formats into "%s"' % filename)
self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args)
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
return info['__files_to_merge'], info
@ -579,7 +581,7 @@ def run(self, info):
temp_filename = prepend_extension(filename, 'temp')
options = ['-c', 'copy', '-map', '0', '-dn', '-aspect', '%f' % stretched_ratio]
self._downloader.to_screen('[ffmpeg] Fixing aspect ratio in "%s"' % filename)
self.to_screen('Fixing aspect ratio in "%s"' % filename)
self.run_ffmpeg(filename, temp_filename, options)
os.remove(encodeFilename(filename))
@ -597,7 +599,7 @@ def run(self, info):
temp_filename = prepend_extension(filename, 'temp')
options = ['-c', 'copy', '-map', '0', '-dn', '-f', 'mp4']
self._downloader.to_screen('[ffmpeg] Correcting container in "%s"' % filename)
self.to_screen('Correcting container in "%s"' % filename)
self.run_ffmpeg(filename, temp_filename, options)
os.remove(encodeFilename(filename))
@ -613,7 +615,7 @@ def run(self, info):
temp_filename = prepend_extension(filename, 'temp')
options = ['-c', 'copy', '-map', '0', '-dn', '-f', 'mp4', '-bsf:a', 'aac_adtstoasc']
self._downloader.to_screen('[ffmpeg] Fixing malformed AAC bitstream in "%s"' % filename)
self.to_screen('Fixing malformed AAC bitstream in "%s"' % filename)
self.run_ffmpeg(filename, temp_filename, options)
os.remove(encodeFilename(filename))
@ -634,19 +636,18 @@ def run(self, info):
if new_format == 'vtt':
new_format = 'webvtt'
if subs is None:
self._downloader.to_screen('[ffmpeg] There aren\'t any subtitles to convert')
self.to_screen('There aren\'t any subtitles to convert')
return [], info
self._downloader.to_screen('[ffmpeg] Converting subtitles')
self.to_screen('Converting subtitles')
sub_filenames = []
for lang, sub in subs.items():
ext = sub['ext']
if ext == new_ext:
self._downloader.to_screen(
'[ffmpeg] Subtitle file for %s is already in the requested format' % new_ext)
self.to_screen('Subtitle file for %s is already in the requested format' % new_ext)
continue
elif ext == 'json':
self._downloader.to_screen(
'[ffmpeg] You have requested to convert json subtitles into another format, '
self.to_screen(
'You have requested to convert json subtitles into another format, '
'which is currently not possible')
continue
old_file = subtitles_filename(filename, lang, ext, info.get('ext'))
@ -655,7 +656,7 @@ def run(self, info):
if ext in ('dfxp', 'ttml', 'tt'):
self._downloader.report_warning(
'[ffmpeg] You have requested to convert dfxp (TTML) subtitles into another format, '
'You have requested to convert dfxp (TTML) subtitles into another format, '
'which results in style information loss')
dfxp_file = old_file

View file

@ -35,14 +35,10 @@ def run(self, info):
title = info['title']
match = re.match(self._titleregex, title)
if match is None:
self._downloader.to_screen(
'[fromtitle] Could not interpret title of video as "%s"'
% self._titleformat)
self.to_screen('Could not interpret title of video as "%s"' % self._titleformat)
return [], info
for attribute, value in match.groupdict().items():
info[attribute] = value
self._downloader.to_screen(
'[fromtitle] parsed %s: %s'
% (attribute, value if value is not None else 'NA'))
self.to_screen('parsed %s: %s' % (attribute, value if value is not None else 'NA'))
return [], info

View file

@ -22,7 +22,7 @@ def __init__(self, downloader, path='', args=None, ignoreerror=False, cut=False,
self.force = force
self.cutout = cut
self.args = ['-chapter'] if not cut else []
self.args += self._def_args if args is None else compat_shlex_split(args)
self.args += self._configuration_args(self._def_args) if args is None else compat_shlex_split(args)
self.path = self.get_exe(path)
if not ignoreerror and self.path is None:
@ -43,7 +43,7 @@ def run(self, information):
return [], information
if information['extractor_key'].lower() != 'youtube':
self._downloader.to_screen('[sponskrub] Skipping sponskrub since it is not a YouTube video')
self.to_screen('Skipping sponskrub since it is not a YouTube video')
return [], information
if self.cutout and not self.force and not information.get('__real_download', False):
self._downloader.to_screen(
@ -51,7 +51,7 @@ def run(self, information):
'Use --sponskrub-force to run sponskrub anyway')
return [], information
self._downloader.to_screen('[sponskrub] Trying to %s sponsor sections' % ('remove' if self.cutout else 'mark'))
self.to_screen('Trying to %s sponsor sections' % ('remove' if self.cutout else 'mark'))
if self.cutout:
self._downloader.to_screen('WARNING: Cutting out sponsor segments will cause the subtitles to go out of sync.')
if not information.get('__real_download', False):
@ -76,11 +76,11 @@ def run(self, information):
if p.returncode == 0:
os.remove(filename)
os.rename(temp_filename, filename)
self._downloader.to_screen('[sponskrub] Sponsor sections have been %s' % ('removed' if self.cutout else 'marked'))
elif p.returncode != 3: # error code 3 means there was no info about the video
self.to_screen('Sponsor sections have been %s' % ('removed' if self.cutout else 'marked'))
elif p.returncode == 3:
self.to_screen('No segments in the SponsorBlock database')
else:
stderr = stderr.decode('utf-8', 'replace')
msg = stderr.strip().split('\n')[-1]
raise PostProcessingError(msg if msg else 'sponskrub failed with error code %s!' % p.returncode)
else:
self._downloader.to_screen('[sponskrub] No segments in the SponsorBlock database')
return [], information

View file

@ -11,7 +11,6 @@
class XAttrMetadataPP(PostProcessor):
#
# More info about extended attributes for media:
# http://freedesktop.org/wiki/CommonExtendedAttributes/
@ -27,7 +26,7 @@ def run(self, info):
""" Set extended attributes on downloaded file (if xattr support is found). """
# Write the metadata to the file's xattrs
self._downloader.to_screen('[metadata] Writing metadata to file\'s xattrs')
self.to_screen('Writing metadata to file\'s xattrs')
filename = info['filepath']