Skip to content

Commit bb66c24

Browse files
committed
Add option --print-to-file
Closes yt-dlp#2372
1 parent 2edb38e commit bb66c24

File tree

4 files changed

+69
-23
lines changed

4 files changed

+69
-23
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,12 @@ You can also fork the project on github and run your fork's [build workflow](.gi
686686
Implies --quiet and --simulate (unless
687687
--no-simulate is used). This option can be
688688
used multiple times
689+
--print-to-file [WHEN:]TEMPLATE FILE
690+
Append given template to the file. The
691+
values of WHEN and TEMPLATE are same as
692+
that of --print. FILE uses the same syntax
693+
as the output template. This option can be
694+
used multiple times
689695
-j, --dump-json Quiet, but print JSON information for each
690696
video. Simulate unless --no-simulate is
691697
used. See "OUTPUT TEMPLATE" for a

yt_dlp/YoutubeDL.py

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,12 @@ class YoutubeDL(object):
201201
verbose: Print additional info to stdout.
202202
quiet: Do not print messages to stdout.
203203
no_warnings: Do not print out anything for warnings.
204-
forceprint: A dict with keys video/playlist mapped to
205-
a list of templates to force print to stdout
204+
forceprint: A dict with keys WHEN mapped to a list of templates to
205+
print to stdout. The allowed keys are video or any of the
206+
items in utils.POSTPROCESS_WHEN.
206207
For compatibility, a single list is also accepted
208+
print_to_file: A dict with keys WHEN (same as forceprint) mapped to
209+
a list of tuples with (template, filename)
207210
forceurl: Force printing final URL. (Deprecated)
208211
forcetitle: Force printing title. (Deprecated)
209212
forceid: Force printing ID. (Deprecated)
@@ -349,8 +352,8 @@ class YoutubeDL(object):
349352
postprocessors: A list of dictionaries, each with an entry
350353
* key: The name of the postprocessor. See
351354
yt_dlp/postprocessor/__init__.py for a list.
352-
* when: When to run the postprocessor. Can be one of
353-
pre_process|before_dl|post_process|after_move.
355+
* when: When to run the postprocessor. Allowed values are
356+
the entries of utils.POSTPROCESS_WHEN
354357
Assumed to be 'post_process' if not given
355358
post_hooks: Deprecated - Register a custom postprocessor instead
356359
A list of functions that get called as the final step
@@ -592,8 +595,10 @@ def check_deprecated(param, option, suggestion):
592595
else:
593596
self.params['nooverwrites'] = not self.params['overwrites']
594597

595-
# Compatibility with older syntax
596598
params.setdefault('forceprint', {})
599+
params.setdefault('print_to_file', {})
600+
601+
# Compatibility with older syntax
597602
if not isinstance(params['forceprint'], dict):
598603
params['forceprint'] = {'video': params['forceprint']}
599604

@@ -2683,19 +2688,32 @@ def process_subtitles(self, video_id, normal_subtitles, automatic_captions):
26832688
subs[lang] = f
26842689
return subs
26852690

2686-
def _forceprint(self, tmpl, info_dict):
2687-
mobj = re.match(r'\w+(=?)$', tmpl)
2688-
if mobj and mobj.group(1):
2689-
tmpl = f'{tmpl[:-1]} = %({tmpl[:-1]})r'
2690-
elif mobj:
2691-
tmpl = '%({})s'.format(tmpl)
2691+
def _forceprint(self, key, info_dict):
2692+
if info_dict is None:
2693+
return
2694+
info_copy = info_dict.copy()
2695+
info_copy['formats_table'] = self.render_formats_table(info_dict)
2696+
info_copy['thumbnails_table'] = self.render_thumbnails_table(info_dict)
2697+
info_copy['subtitles_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('subtitles'))
2698+
info_copy['automatic_captions_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('automatic_captions'))
2699+
2700+
def format_tmpl(tmpl):
2701+
mobj = re.match(r'\w+(=?)$', tmpl)
2702+
if mobj and mobj.group(1):
2703+
return f'{tmpl[:-1]} = %({tmpl[:-1]})r'
2704+
elif mobj:
2705+
return f'%({tmpl})s'
2706+
return tmpl
26922707

2693-
info_dict = info_dict.copy()
2694-
info_dict['formats_table'] = self.render_formats_table(info_dict)
2695-
info_dict['thumbnails_table'] = self.render_thumbnails_table(info_dict)
2696-
info_dict['subtitles_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('subtitles'))
2697-
info_dict['automatic_captions_table'] = self.render_subtitles_table(info_dict.get('id'), info_dict.get('automatic_captions'))
2698-
self.to_stdout(self.evaluate_outtmpl(tmpl, info_dict))
2708+
for tmpl in self.params['forceprint'].get(key, []):
2709+
self.to_stdout(self.evaluate_outtmpl(format_tmpl(tmpl), info_copy))
2710+
2711+
for tmpl, file_tmpl in self.params['print_to_file'].get(key, []):
2712+
filename = self.evaluate_outtmpl(file_tmpl, info_dict)
2713+
tmpl = format_tmpl(tmpl)
2714+
self.to_screen(f'[info] Writing {tmpl!r} to: {filename}')
2715+
with io.open(filename, 'a', encoding='utf-8') as f:
2716+
f.write(self.evaluate_outtmpl(tmpl, info_copy) + '\n')
26992717

27002718
def __forced_printings(self, info_dict, filename, incomplete):
27012719
def print_mandatory(field, actual_field=None):
@@ -2719,10 +2737,11 @@ def print_optional(field):
27192737
elif 'url' in info_dict:
27202738
info_dict['urls'] = info_dict['url'] + info_dict.get('play_path', '')
27212739

2722-
if self.params['forceprint'].get('video') or self.params.get('forcejson'):
2740+
if (self.params.get('forcejson')
2741+
or self.params['forceprint'].get('video')
2742+
or self.params['print_to_file'].get('video')):
27232743
self.post_extract(info_dict)
2724-
for tmpl in self.params['forceprint'].get('video', []):
2725-
self._forceprint(tmpl, info_dict)
2744+
self._forceprint('video', info_dict)
27262745

27272746
print_mandatory('title')
27282747
print_mandatory('id')
@@ -3290,8 +3309,7 @@ def run_pp(self, pp, infodict):
32903309
return infodict
32913310

32923311
def run_all_pps(self, key, info, *, additional_pps=None):
3293-
for tmpl in self.params['forceprint'].get(key, []):
3294-
self._forceprint(tmpl, info)
3312+
self._forceprint(key, info)
32953313
for pp in (additional_pps or []) + self._pps[key]:
32963314
info = self.run_pp(pp, info)
32973315
return info

yt_dlp/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,10 @@ def validate_outtmpl(tmpl, msg):
356356
for type_, tmpl_list in opts.forceprint.items():
357357
for tmpl in tmpl_list:
358358
validate_outtmpl(tmpl, f'{type_} print template')
359+
for type_, tmpl_list in opts.print_to_file.items():
360+
for tmpl, file in tmpl_list:
361+
validate_outtmpl(tmpl, f'{type_} print-to-file template')
362+
validate_outtmpl(file, f'{type_} print-to-file filename')
359363
validate_outtmpl(opts.sponsorblock_chapter_title, 'SponsorBlock chapter title')
360364
for k, tmpl in opts.progress_template.items():
361365
k = f'{k[:-6]} console title' if '-title' in k else f'{k} progress'
@@ -663,6 +667,7 @@ def report_deprecation(val, old, new=None):
663667
'forcefilename': opts.getfilename,
664668
'forceformat': opts.getformat,
665669
'forceprint': opts.forceprint,
670+
'print_to_file': opts.print_to_file,
666671
'forcejson': opts.dumpjson or opts.print_json,
667672
'dump_single_json': opts.dump_single_json,
668673
'force_write_download_archive': opts.force_write_download_archive,

yt_dlp/options.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,16 @@ def _dict_from_options_callback(
173173
process_key=str.lower, append=False):
174174

175175
out_dict = dict(getattr(parser.values, option.dest))
176+
multiple_args = not isinstance(value, str)
176177
if multiple_keys:
177178
allowed_keys = r'(%s)(,(%s))*' % (allowed_keys, allowed_keys)
178-
mobj = re.match(r'(?i)(?P<keys>%s)%s(?P<val>.*)$' % (allowed_keys, delimiter), value)
179+
mobj = re.match(
180+
r'(?i)(?P<keys>%s)%s(?P<val>.*)$' % (allowed_keys, delimiter),
181+
value[0] if multiple_args else value)
179182
if mobj is not None:
180183
keys, val = mobj.group('keys').split(','), mobj.group('val')
184+
if multiple_args:
185+
val = [val, *value[1:]]
181186
elif default_key is not None:
182187
keys, val = [default_key], value
183188
else:
@@ -923,6 +928,18 @@ def _dict_from_options_callback(
923928
'Field name or output template to print to screen, optionally prefixed with when to print it, separated by a ":". '
924929
'Supported values of "WHEN" are the same as that of --use-postprocessor, and "video" (default). '
925930
'Implies --quiet and --simulate (unless --no-simulate is used). This option can be used multiple times'))
931+
verbosity.add_option(
932+
'--print-to-file',
933+
metavar='[WHEN:]TEMPLATE FILE', dest='print_to_file', default={}, type='str', nargs=2,
934+
action='callback', callback=_dict_from_options_callback,
935+
callback_kwargs={
936+
'allowed_keys': 'video|' + '|'.join(map(re.escape, POSTPROCESS_WHEN)),
937+
'default_key': 'video',
938+
'multiple_keys': False,
939+
'append': True,
940+
}, help=(
941+
'Append given template to the file. The values of WHEN and TEMPLATE are same as that of --print. '
942+
'FILE uses the same syntax as the output template. This option can be used multiple times'))
926943
verbosity.add_option(
927944
'-g', '--get-url',
928945
action='store_true', dest='geturl', default=False,

0 commit comments

Comments
 (0)