From 1e43a6f7336f4d9691dc52a1bc7cfe14ba7a936d Mon Sep 17 00:00:00 2001 From: pukkandan Date: Mon, 3 Jan 2022 16:43:54 +0530 Subject: [PATCH] Allow `--exec` to be run at any post-processing stage Deprecates `--exec-before-download` --- README.md | 29 ++++++++++++++--------------- yt_dlp/YoutubeDL.py | 5 +++-- yt_dlp/__init__.py | 19 ++++++++----------- yt_dlp/options.py | 29 +++++++++++++++++------------ yt_dlp/postprocessor/exec.py | 12 +++++++----- yt_dlp/utils.py | 3 +++ 6 files changed, 52 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index e032ea6e6f..1b8680e339 100644 --- a/README.md +++ b/README.md @@ -896,23 +896,20 @@ You can also fork the project on github and run your fork's [build workflow](.gi --ffmpeg-location PATH Location of the ffmpeg binary; either the path to the binary or its containing directory - --exec CMD Execute a command on the file after - downloading and post-processing. Same - syntax as the output template can be used - to pass any field as arguments to the - command. An additional field "filepath" + --exec [WHEN:]CMD Execute a command, optionally prefixed with + when to execute it (after_move if + unspecified), separated by a ":". Supported + values of "WHEN" are the same as that of + --use-postprocessor. Same syntax as the + output template can be used to pass any + field as arguments to the command. After + download, an additional field "filepath" that contains the final path of the - downloaded file is also available. If no - fields are passed, %(filepath)q is appended - to the end of the command. This option can - be used multiple times - --no-exec Remove any previously defined --exec - --exec-before-download CMD Execute a command before the actual - download. The syntax is the same as --exec - but "filepath" is not available. This + downloaded file is also available, and if + no fields are passed, %(filepath)q is + appended to the end of the command. This option can be used multiple times - --no-exec-before-download Remove any previously defined - --exec-before-download + --no-exec Remove any previously defined --exec --convert-subs FORMAT Convert the subtitles to another format (currently supported: srt|vtt|ass|lrc) (Alias: --convert-subtitles) @@ -1800,6 +1797,8 @@ While these options are redundant, they are still expected to be used due to the #### Not recommended While these options still work, their use is not recommended since there are other alternatives to achieve the same + --exec-before-download CMD --exec "before_dl:CMD" + --no-exec-before-download --no-exec --all-formats -f all --all-subs --sub-langs all --write-subs --print-json -j --no-simulate diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index faea854855..5b285e1a16 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -91,6 +91,7 @@ from .utils import ( PerRequestProxyHandler, platform_name, Popen, + POSTPROCESS_WHEN, PostProcessingError, preferredencoding, prepend_extension, @@ -507,7 +508,7 @@ class YoutubeDL(object): params = None _ies = {} - _pps = {'pre_process': [], 'before_dl': [], 'after_move': [], 'post_process': []} + _pps = {k: [] for k in POSTPROCESS_WHEN} _printed_messages = set() _first_webpage_request = True _download_retcode = None @@ -525,7 +526,7 @@ class YoutubeDL(object): params = {} self._ies = {} self._ies_instances = {} - self._pps = {'pre_process': [], 'before_dl': [], 'after_move': [], 'post_process': []} + self._pps = {k: [] for k in POSTPROCESS_WHEN} self._printed_messages = set() self._first_webpage_request = True self._post_hooks = [] diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index af7a4e195c..85f000df4f 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -143,6 +143,8 @@ def _real_main(argv=None): '"-f best" selects the best pre-merged format which is often not the best option', 'To let yt-dlp download and merge the best available formats, simply do not pass any format selection', 'If you know what you are doing and want only the best pre-merged format, use "-f b" instead to suppress this warning'))) + if opts.exec_cmd.get('before_dl') and opts.exec_before_dl_cmd: + parser.error('using "--exec-before-download" conflicts with "--exec before_dl:"') if opts.usenetrc and (opts.username is not None or opts.password is not None): parser.error('using .netrc conflicts with giving username/password') if opts.password is not None and opts.username is None: @@ -489,13 +491,6 @@ def _real_main(argv=None): # Run this before the actual video download 'when': 'before_dl' }) - # Must be after all other before_dl - if opts.exec_before_dl_cmd: - postprocessors.append({ - 'key': 'Exec', - 'exec_cmd': opts.exec_before_dl_cmd, - 'when': 'before_dl' - }) if opts.extractaudio: postprocessors.append({ 'key': 'FFmpegExtractAudio', @@ -596,13 +591,15 @@ def _real_main(argv=None): # XAttrMetadataPP should be run after post-processors that may change file contents if opts.xattrs: postprocessors.append({'key': 'XAttrMetadata'}) - # Exec must be the last PP - if opts.exec_cmd: + # Exec must be the last PP of each category + if opts.exec_before_dl_cmd: + opts.exec_cmd.setdefault('before_dl', opts.exec_before_dl_cmd) + for when, exec_cmd in opts.exec_cmd.items(): postprocessors.append({ 'key': 'Exec', - 'exec_cmd': opts.exec_cmd, + 'exec_cmd': exec_cmd, # Run this only after the files have been moved to their final locations - 'when': 'after_move' + 'when': when, }) def report_args_compat(arg, name): diff --git a/yt_dlp/options.py b/yt_dlp/options.py index d48cd1457a..f4e5d14df5 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -16,6 +16,7 @@ from .utils import ( expand_path, get_executable_path, OUTTMPL_TYPES, + POSTPROCESS_WHEN, preferredencoding, remove_end, write_string, @@ -1393,29 +1394,33 @@ def parseOpts(overrideArguments=None): dest='ffmpeg_location', help='Location of the ffmpeg binary; either the path to the binary or its containing directory') postproc.add_option( - '--exec', metavar='CMD', - action='append', dest='exec_cmd', - help=( - 'Execute a command on the file after downloading and post-processing. ' + '--exec', + metavar='[WHEN:]CMD', dest='exec_cmd', default={}, type='str', + action='callback', callback=_dict_from_options_callback, + callback_kwargs={ + 'allowed_keys': '|'.join(map(re.escape, POSTPROCESS_WHEN)), + 'default_key': 'after_move', + 'multiple_keys': False, + 'append': True, + }, help=( + 'Execute a command, optionally prefixed with when to execute it (after_move if unspecified), separated by a ":". ' + 'Supported values of "WHEN" are the same as that of --use-postprocessor. ' 'Same syntax as the output template can be used to pass any field as arguments to the command. ' - 'An additional field "filepath" that contains the final path of the downloaded file is also available. ' - 'If no fields are passed, %(filepath)q is appended to the end of the command. ' + 'After download, an additional field "filepath" that contains the final path of the downloaded file ' + 'is also available, and if no fields are passed, %(filepath)q is appended to the end of the command. ' 'This option can be used multiple times')) postproc.add_option( '--no-exec', - action='store_const', dest='exec_cmd', const=[], + action='store_const', dest='exec_cmd', const={}, help='Remove any previously defined --exec') postproc.add_option( '--exec-before-download', metavar='CMD', action='append', dest='exec_before_dl_cmd', - help=( - 'Execute a command before the actual download. ' - 'The syntax is the same as --exec but "filepath" is not available. ' - 'This option can be used multiple times')) + help=optparse.SUPPRESS_HELP) postproc.add_option( '--no-exec-before-download', action='store_const', dest='exec_before_dl_cmd', const=[], - help='Remove any previously defined --exec-before-download') + help=optparse.SUPPRESS_HELP) postproc.add_option( '--convert-subs', '--convert-sub', '--convert-subtitles', metavar='FORMAT', dest='convertsubtitles', default=None, diff --git a/yt_dlp/postprocessor/exec.py b/yt_dlp/postprocessor/exec.py index 28a7c3d704..63f4d23f26 100644 --- a/yt_dlp/postprocessor/exec.py +++ b/yt_dlp/postprocessor/exec.py @@ -22,11 +22,13 @@ class ExecPP(PostProcessor): if tmpl_dict: # if there are no replacements, tmpl_dict = {} return self._downloader.escape_outtmpl(tmpl) % tmpl_dict - # If no replacements are found, replace {} for backard compatibility - if '{}' not in cmd: - cmd += ' {}' - return cmd.replace('{}', compat_shlex_quote( - info.get('filepath') or info['_filename'])) + filepath = info.get('filepath', info.get('_filename')) + # If video, and no replacements are found, replace {} for backard compatibility + if filepath: + if '{}' not in cmd: + cmd += ' {}' + cmd = cmd.replace('{}', compat_shlex_quote(filepath)) + return cmd def run(self, info): for tmpl in self.exec_cmd: diff --git a/yt_dlp/utils.py b/yt_dlp/utils.py index ae23ec2a36..f56129aa5f 100644 --- a/yt_dlp/utils.py +++ b/yt_dlp/utils.py @@ -3036,6 +3036,9 @@ def qualities(quality_ids): return q +POSTPROCESS_WHEN = {'pre_process', 'before_dl', 'after_move', 'post_process'} + + DEFAULT_OUTTMPL = { 'default': '%(title)s [%(id)s].%(ext)s', 'chapter': '%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s',