diff --git a/README.md b/README.md index d3fbdd6e4..5641a868f 100644 --- a/README.md +++ b/README.md @@ -686,6 +686,12 @@ You can also fork the project on github and run your fork's [build workflow](.gi Implies --quiet and --simulate (unless --no-simulate is used). This option can be used multiple times + --print-to-file [WHEN:]TEMPLATE FILE + Append given template to the file. The + values of WHEN and TEMPLATE are same as + that of --print. FILE uses the same syntax + as the output template. This option can be + used multiple times -j, --dump-json Quiet, but print JSON information for each video. Simulate unless --no-simulate is used. See "OUTPUT TEMPLATE" for a diff --git a/yt_dlp/YoutubeDL.py b/yt_dlp/YoutubeDL.py index d1fbd9a87..97f9099ff 100644 --- a/yt_dlp/YoutubeDL.py +++ b/yt_dlp/YoutubeDL.py @@ -201,9 +201,12 @@ class YoutubeDL(object): verbose: Print additional info to stdout. quiet: Do not print messages to stdout. no_warnings: Do not print out anything for warnings. - forceprint: A dict with keys video/playlist mapped to - a list of templates to force print to stdout + forceprint: A dict with keys WHEN mapped to a list of templates to + print to stdout. The allowed keys are video or any of the + items in utils.POSTPROCESS_WHEN. For compatibility, a single list is also accepted + print_to_file: A dict with keys WHEN (same as forceprint) mapped to + a list of tuples with (template, filename) forceurl: Force printing final URL. (Deprecated) forcetitle: Force printing title. (Deprecated) forceid: Force printing ID. (Deprecated) @@ -349,8 +352,8 @@ class YoutubeDL(object): postprocessors: A list of dictionaries, each with an entry * key: The name of the postprocessor. See yt_dlp/postprocessor/__init__.py for a list. - * when: When to run the postprocessor. Can be one of - pre_process|before_dl|post_process|after_move. + * when: When to run the postprocessor. Allowed values are + the entries of utils.POSTPROCESS_WHEN Assumed to be 'post_process' if not given post_hooks: Deprecated - Register a custom postprocessor instead A list of functions that get called as the final step @@ -592,8 +595,10 @@ class YoutubeDL(object): else: self.params['nooverwrites'] = not self.params['overwrites'] - # Compatibility with older syntax params.setdefault('forceprint', {}) + params.setdefault('print_to_file', {}) + + # Compatibility with older syntax if not isinstance(params['forceprint'], dict): params['forceprint'] = {'video': params['forceprint']} @@ -2683,19 +2688,32 @@ class YoutubeDL(object): subs[lang] = f return subs - def _forceprint(self, tmpl, info_dict): - mobj = re.match(r'\w+(=?)$', tmpl) - if mobj and mobj.group(1): - tmpl = f'{tmpl[:-1]} = %({tmpl[:-1]})r' - elif mobj: - tmpl = '%({})s'.format(tmpl) + def _forceprint(self, key, info_dict): + if info_dict is None: + return + info_copy = info_dict.copy() + info_copy['formats_table'] = self.render_formats_table(info_dict) + info_copy['thumbnails_table'] = self.render_thumbnails_table(info_dict) + info_copy['subtitles_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('subtitles')) + info_copy['automatic_captions_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('automatic_captions')) + + def format_tmpl(tmpl): + mobj = re.match(r'\w+(=?)$', tmpl) + if mobj and mobj.group(1): + return f'{tmpl[:-1]} = %({tmpl[:-1]})r' + elif mobj: + return f'%({tmpl})s' + return tmpl - info_dict = info_dict.copy() - info_dict['formats_table'] = self.render_formats_table(info_dict) - info_dict['thumbnails_table'] = self.render_thumbnails_table(info_dict) - info_dict['subtitles_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('subtitles')) - info_dict['automatic_captions_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('automatic_captions')) - self.to_stdout(self.evaluate_outtmpl(tmpl, info_dict)) + for tmpl in self.params['forceprint'].get(key, []): + self.to_stdout(self.evaluate_outtmpl(format_tmpl(tmpl), info_copy)) + + for tmpl, file_tmpl in self.params['print_to_file'].get(key, []): + filename = self.evaluate_outtmpl(file_tmpl, info_dict) + tmpl = format_tmpl(tmpl) + self.to_screen(f'[info] Writing {tmpl!r} to: {filename}') + with io.open(filename, 'a', encoding='utf-8') as f: + f.write(self.evaluate_outtmpl(tmpl, info_copy) + '\n') def __forced_printings(self, info_dict, filename, incomplete): def print_mandatory(field, actual_field=None): @@ -2719,10 +2737,11 @@ class YoutubeDL(object): elif 'url' in info_dict: info_dict['urls'] = info_dict['url'] + info_dict.get('play_path', '') - if self.params['forceprint'].get('video') or self.params.get('forcejson'): + if (self.params.get('forcejson') + or self.params['forceprint'].get('video') + or self.params['print_to_file'].get('video')): self.post_extract(info_dict) - for tmpl in self.params['forceprint'].get('video', []): - self._forceprint(tmpl, info_dict) + self._forceprint('video', info_dict) print_mandatory('title') print_mandatory('id') @@ -3290,8 +3309,7 @@ class YoutubeDL(object): return infodict def run_all_pps(self, key, info, *, additional_pps=None): - for tmpl in self.params['forceprint'].get(key, []): - self._forceprint(tmpl, info) + self._forceprint(key, info) for pp in (additional_pps or []) + self._pps[key]: info = self.run_pp(pp, info) return info diff --git a/yt_dlp/__init__.py b/yt_dlp/__init__.py index c5e647c23..f9ebfa053 100644 --- a/yt_dlp/__init__.py +++ b/yt_dlp/__init__.py @@ -356,6 +356,10 @@ def _real_main(argv=None): for type_, tmpl_list in opts.forceprint.items(): for tmpl in tmpl_list: validate_outtmpl(tmpl, f'{type_} print template') + for type_, tmpl_list in opts.print_to_file.items(): + for tmpl, file in tmpl_list: + validate_outtmpl(tmpl, f'{type_} print-to-file template') + validate_outtmpl(file, f'{type_} print-to-file filename') validate_outtmpl(opts.sponsorblock_chapter_title, 'SponsorBlock chapter title') for k, tmpl in opts.progress_template.items(): k = f'{k[:-6]} console title' if '-title' in k else f'{k} progress' @@ -663,6 +667,7 @@ def _real_main(argv=None): 'forcefilename': opts.getfilename, 'forceformat': opts.getformat, 'forceprint': opts.forceprint, + 'print_to_file': opts.print_to_file, 'forcejson': opts.dumpjson or opts.print_json, 'dump_single_json': opts.dump_single_json, 'force_write_download_archive': opts.force_write_download_archive, diff --git a/yt_dlp/options.py b/yt_dlp/options.py index 289804945..df8fb6f63 100644 --- a/yt_dlp/options.py +++ b/yt_dlp/options.py @@ -173,11 +173,16 @@ def create_parser(): process_key=str.lower, append=False): out_dict = dict(getattr(parser.values, option.dest)) + multiple_args = not isinstance(value, str) if multiple_keys: allowed_keys = r'(%s)(,(%s))*' % (allowed_keys, allowed_keys) - mobj = re.match(r'(?i)(?P%s)%s(?P.*)$' % (allowed_keys, delimiter), value) + mobj = re.match( + r'(?i)(?P%s)%s(?P.*)$' % (allowed_keys, delimiter), + value[0] if multiple_args else value) if mobj is not None: keys, val = mobj.group('keys').split(','), mobj.group('val') + if multiple_args: + val = [val, *value[1:]] elif default_key is not None: keys, val = [default_key], value else: @@ -923,6 +928,18 @@ def create_parser(): 'Field name or output template to print to screen, optionally prefixed with when to print it, separated by a ":". ' 'Supported values of "WHEN" are the same as that of --use-postprocessor, and "video" (default). ' 'Implies --quiet and --simulate (unless --no-simulate is used). This option can be used multiple times')) + verbosity.add_option( + '--print-to-file', + metavar='[WHEN:]TEMPLATE FILE', dest='print_to_file', default={}, type='str', nargs=2, + action='callback', callback=_dict_from_options_callback, + callback_kwargs={ + 'allowed_keys': 'video|' + '|'.join(map(re.escape, POSTPROCESS_WHEN)), + 'default_key': 'video', + 'multiple_keys': False, + 'append': True, + }, help=( + 'Append given template to the file. The values of WHEN and TEMPLATE are same as that of --print. ' + 'FILE uses the same syntax as the output template. This option can be used multiple times')) verbosity.add_option( '-g', '--get-url', action='store_true', dest='geturl', default=False,