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 @@ Then simply type this
--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 _real_main(argv=None):
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 parseOpts(overrideArguments=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)')
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 parseOpts(overrideArguments=None):
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 @@ class PostProcessor(object):
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 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
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 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
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 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
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 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
'-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 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
'-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 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
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 @@ from ..utils import (
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 @@ class ExecAfterDownloadPP(PostProcessor):
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 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
# 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 @@ class FFmpegVideoRemuxerPP(FFmpegPostProcessor):
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 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor):
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 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor):
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 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
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 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
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 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
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 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
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 @@ class FFmpegMergerPP(FFmpegPostProcessor):
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 @@ class FFmpegFixupStretchedPP(FFmpegPostProcessor):
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 @@ class FFmpegFixupM4aPP(FFmpegPostProcessor):
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 @@ class FFmpegFixupM3u8PP(FFmpegPostProcessor):
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 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
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 @@ class FFmpegSubtitlesConvertorPP(FFmpegPostProcessor):
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 @@ class MetadataFromTitlePP(PostProcessor):
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 @@ class SponSkrubPP(PostProcessor):
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 @@ class SponSkrubPP(PostProcessor):
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 @@ class SponSkrubPP(PostProcessor):
'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 @@ class SponSkrubPP(PostProcessor):
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 @@ from ..utils import (
class XAttrMetadataPP(PostProcessor):
#
# More info about extended attributes for media:
# http://freedesktop.org/wiki/CommonExtendedAttributes/
@ -27,7 +26,7 @@ class XAttrMetadataPP(PostProcessor):
""" 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']