]> jfr.im git - yt-dlp.git/blame - yt_dlp/__init__.py
Fix bug in 52efa4b31200119adaa8acf33e50b84fcb6948f0
[yt-dlp.git] / yt_dlp / __init__.py
CommitLineData
cc52de43 1#!/usr/bin/env python3
dcdb292f 2# coding: utf-8
235b3ba4 3
347182a0 4f'You are using an unsupported version of Python. Only Python versions 3.6 and above are supported by yt-dlp' # noqa: F541
a4bc4336 5
235b3ba4 6__license__ = 'Public Domain'
235b3ba4 7
0d94f247 8import codecs
8f563f32 9import io
e9f4ccd1 10import itertools
235b3ba4 11import os
0f818663 12import random
43820c03 13import re
235b3ba4 14import sys
235b3ba4 15
d1b5f70b 16from .options import parseOpts
8c25f81b 17from .compat import (
e68301af 18 compat_getpass,
b69fd25c 19 compat_os_name,
e9f4ccd1 20 compat_shlex_quote,
e07e9313 21 workaround_optparse_bug9161,
8c25f81b 22)
f59f5ef8 23from .cookies import SUPPORTED_BROWSERS, SUPPORTED_KEYRINGS
8c25f81b 24from .utils import (
a4fd0415
PH
25 DateRange,
26 decodeOption,
f304da8a 27 DownloadCancelled,
a4fd0415 28 DownloadError,
590bc6f6 29 expand_path,
31c49255 30 float_or_none,
d1b5f70b 31 GeoUtils,
31c49255 32 int_or_none,
347de493 33 match_filter_func,
d1b5f70b 34 NO_DEFAULT,
2d9ec704 35 parse_duration,
a4fd0415 36 preferredencoding,
62e609ab 37 read_batch_urls,
df692c5a 38 render_table,
a4fd0415 39 SameFileError,
e3946f98 40 setproctitle,
a4fd0415 41 std_headers,
8b7539d2 42 traverse_obj,
a4fd0415 43 write_string,
a4fd0415 44)
c19bc311 45from .update import run_update
d1b5f70b 46from .downloader import FileDownloader
2bad0e5d 47from .extractor import gen_extractors, list_extractors
eb8a4433 48from .extractor.common import InfoExtractor
1b6712ab 49from .extractor.adobepass import MSO_INFO
e9f4ccd1 50from .postprocessor import (
81a23040 51 FFmpegExtractAudioPP,
52 FFmpegSubtitlesConvertorPP,
53 FFmpegThumbnailsConvertorPP,
54 FFmpegVideoConvertorPP,
55 FFmpegVideoRemuxerPP,
e9f4ccd1 56 MetadataFromFieldPP,
57 MetadataParserPP,
81a23040 58)
8222d8de 59from .YoutubeDL import YoutubeDL
a4fd0415 60
235b3ba4 61
d1b5f70b 62def get_urls(urls, batchfile, verbose):
59ae15a5 63 # Batch file verification
62e609ab 64 batch_urls = []
d1b5f70b 65 if batchfile is not None:
59ae15a5 66 try:
d1b5f70b 67 if batchfile == '-':
b69fd25c 68 write_string('Reading URLs from stdin - EOF (%s) to end:\n' % (
69 'Ctrl+Z' if compat_os_name == 'nt' else 'Ctrl+D'))
59ae15a5
PH
70 batchfd = sys.stdin
71 else:
e2eca6f6 72 batchfd = io.open(
d1b5f70b 73 expand_path(batchfile),
e2eca6f6 74 'r', encoding='utf-8', errors='ignore')
62e609ab 75 batch_urls = read_batch_urls(batchfd)
d1b5f70b 76 if verbose:
a4bc4336 77 write_string('[debug] Batch file urls: ' + repr(batch_urls) + '\n')
59ae15a5 78 except IOError:
d1b5f70b 79 sys.exit('ERROR: batch file %s could not be read' % batchfile)
c774b3c6 80 _enc = preferredencoding()
d1b5f70b 81 return [
82 url.strip().decode(_enc, 'ignore') if isinstance(url, bytes) else url.strip()
83 for url in batch_urls + urls]
59ae15a5 84
d1b5f70b 85
86def print_extractor_information(opts, urls):
59ae15a5 87 if opts.list_extractors:
05900629 88 for ie in list_extractors(opts.age_limit):
251ae04e 89 write_string(ie.IE_NAME + (' (CURRENTLY BROKEN)' if not ie.working() else '') + '\n', out=sys.stdout)
d1b5f70b 90 matchedUrls = [url for url in urls if ie.suitable(url)]
59ae15a5 91 for mu in matchedUrls:
7b0d1c28 92 write_string(' ' + mu + '\n', out=sys.stdout)
d1b5f70b 93 elif opts.list_extractor_descriptions:
05900629 94 for ie in list_extractors(opts.age_limit):
251ae04e 95 if not ie.working():
0f818663 96 continue
231025c4 97 if ie.IE_DESC is False:
15870e90 98 continue
231025c4 99 desc = ie.IE_DESC or ie.IE_NAME
96565c7e 100 if getattr(ie, 'SEARCH_KEY', None) is not None:
50a0f6df 101 _SEARCHES = ('cute kittens', 'slithering pythons', 'falling cat', 'angry poodle', 'purple fish', 'running tortoise', 'sleeping bunny', 'burping cow')
a4bc4336 102 _COUNTS = ('', '5', '10', 'all')
96565c7e 103 desc += f'; "{ie.SEARCH_KEY}:" prefix (Example: "{ie.SEARCH_KEY}{random.choice(_COUNTS)}:{random.choice(_SEARCHES)}")'
7b0d1c28 104 write_string(desc + '\n', out=sys.stdout)
d1b5f70b 105 elif opts.ap_list_mso:
1b6712ab 106 table = [[mso_id, mso_info['name']] for mso_id, mso_info in MSO_INFO.items()]
797c636b 107 write_string('Supported TV Providers:\n' + render_table(['mso', 'mso name'], table) + '\n', out=sys.stdout)
bd558525 108 else:
d1b5f70b 109 return False
110 return True
53ed7066 111
19b824f6 112
d1b5f70b 113def set_compat_opts(opts):
53ed7066 114 def _unused_compat_opt(name):
d1b5f70b 115 if name not in opts.compat_opts:
53ed7066 116 return False
d1b5f70b 117 opts.compat_opts.discard(name)
118 opts.compat_opts.update(['*%s' % name])
53ed7066 119 return True
120
e4f02757 121 def set_default_compat(compat_name, opt_name, default=True, remove_compat=True):
53ed7066 122 attr = getattr(opts, opt_name)
d1b5f70b 123 if compat_name in opts.compat_opts:
53ed7066 124 if attr is None:
125 setattr(opts, opt_name, not default)
126 return True
127 else:
128 if remove_compat:
129 _unused_compat_opt(compat_name)
130 return False
131 elif attr is None:
132 setattr(opts, opt_name, default)
133 return None
134
b1940459 135 set_default_compat('abort-on-error', 'ignoreerrors', 'only_download')
53ed7066 136 set_default_compat('no-playlist-metafiles', 'allow_playlist_files')
e4f02757 137 set_default_compat('no-clean-infojson', 'clean_infojson')
d1b5f70b 138 if 'no-attach-info-json' in opts.compat_opts:
dac5df5a 139 if opts.embed_infojson:
140 _unused_compat_opt('no-attach-info-json')
141 else:
142 opts.embed_infojson = False
d1b5f70b 143 if 'format-sort' in opts.compat_opts:
53ed7066 144 opts.format_sort.extend(InfoExtractor.FormatSort.ytdl_default)
145 _video_multistreams_set = set_default_compat('multistreams', 'allow_multiple_video_streams', False, remove_compat=False)
146 _audio_multistreams_set = set_default_compat('multistreams', 'allow_multiple_audio_streams', False, remove_compat=False)
147 if _video_multistreams_set is False and _audio_multistreams_set is False:
148 _unused_compat_opt('multistreams')
d1b5f70b 149 if 'filename' in opts.compat_opts:
150 if opts.outtmpl.get('default') is None:
151 opts.outtmpl.update({'default': '%(title)s-%(id)s.%(ext)s'})
53ed7066 152 else:
153 _unused_compat_opt('filename')
154
d1b5f70b 155
156def validate_options(opts):
157 def validate(cndn, name, value=None, msg=None):
158 if cndn:
159 return True
160 raise ValueError((msg or 'invalid {name} "{value}" given').format(name=name, value=value))
161
162 def validate_in(name, value, items, msg=None):
163 return validate(value is None or value in items, name, value, msg)
164
165 def validate_regex(name, value, regex):
166 return validate(value is None or re.match(regex, value), name, value)
167
168 def validate_positive(name, value, strict=False):
169 return validate(value is None or value > 0 or (not strict and value == 0),
170 name, value, '{name} "{value}" must be positive' + ('' if strict else ' or 0'))
171
172 def validate_minmax(min_val, max_val, min_name, max_name=None):
173 if max_val is None or min_val is None or max_val >= min_val:
174 return
175 if not max_name:
176 min_name, max_name = f'min {min_name}', f'max {min_name}'
177 raise ValueError(f'{max_name} "{max_val}" must be must be greater than or equal to {min_name} "{min_val}"')
178
179 # Usernames and passwords
180 validate(not opts.usenetrc or (opts.username is None and opts.password is None),
181 '.netrc', msg='using {name} conflicts with giving username/password')
182 validate(opts.password is None or opts.username is not None, 'account username', msg='{name} missing')
183 validate(opts.ap_password is None or opts.ap_username is not None,
184 'TV Provider account username', msg='{name} missing')
185 validate_in('TV Provider', opts.ap_mso, MSO_INFO,
186 'Unsupported {name} "{value}", use --ap-list-mso to get a list of supported TV Providers')
187
188 # Numbers
189 validate_positive('autonumber start', opts.autonumber_start)
190 validate_positive('autonumber size', opts.autonumber_size, True)
191 validate_positive('concurrent fragments', opts.concurrent_fragment_downloads, True)
192 validate_positive('playlist start', opts.playliststart, True)
193 if opts.playlistend != -1:
194 validate_minmax(opts.playliststart, opts.playlistend, 'playlist start', 'playlist end')
195
196 # Time ranges
197 validate_positive('subtitles sleep interval', opts.sleep_interval_subtitles)
198 validate_positive('requests sleep interval', opts.sleep_interval_requests)
199 validate_positive('sleep interval', opts.sleep_interval)
200 validate_positive('max sleep interval', opts.max_sleep_interval)
07ff290d 201 if opts.sleep_interval is None:
d1b5f70b 202 validate(
07ff290d 203 opts.max_sleep_interval is None, 'min sleep interval',
d1b5f70b 204 msg='{name} must be specified; use --min-sleep-interval')
07ff290d 205 elif opts.max_sleep_interval is None:
206 opts.max_sleep_interval = opts.sleep_interval
207 else:
d1b5f70b 208 validate_minmax(opts.sleep_interval, opts.max_sleep_interval, 'sleep interval')
209
210 if opts.wait_for_video is not None:
211 min_wait, max_wait, *_ = map(parse_duration, opts.wait_for_video.split('-', 1) + [None])
212 validate(min_wait is not None and not (max_wait is None and '-' in opts.wait_for_video),
213 'time range to wait for video', opts.wait_for_video)
214 validate_minmax(min_wait, max_wait, 'time range to wait for video')
215 opts.wait_for_video = (min_wait, max_wait)
216
217 # Format sort
218 for f in opts.format_sort:
219 validate_regex('format sorting', f, InfoExtractor.FormatSort.regex)
220
221 # Postprocessor formats
222 validate_in('audio format', opts.audioformat, ['best'] + list(FFmpegExtractAudioPP.SUPPORTED_EXTS))
223 validate_in('subtitle format', opts.convertsubtitles, FFmpegSubtitlesConvertorPP.SUPPORTED_EXTS)
224 validate_in('thumbnail format', opts.convertthumbnails, FFmpegThumbnailsConvertorPP.SUPPORTED_EXTS)
225 if opts.recodevideo is not None:
226 opts.recodevideo = opts.recodevideo.replace(' ', '')
227 validate_regex('video recode format', opts.recodevideo, FFmpegVideoConvertorPP.FORMAT_RE)
228 if opts.remuxvideo is not None:
229 opts.remuxvideo = opts.remuxvideo.replace(' ', '')
230 validate_regex('video remux format', opts.remuxvideo, FFmpegVideoRemuxerPP.FORMAT_RE)
231 if opts.audioquality:
232 opts.audioquality = opts.audioquality.strip('k').strip('K')
233 # int_or_none prevents inf, nan
234 validate_positive('audio quality', int_or_none(float_or_none(opts.audioquality), default=0))
235
236 # Retries
237 def parse_retries(name, value):
238 if value is None:
239 return None
240 elif value in ('inf', 'infinite'):
241 return float('inf')
242 try:
243 return int(value)
244 except (TypeError, ValueError):
245 validate(False, f'{name} retry count', value)
246
247 opts.retries = parse_retries('download', opts.retries)
248 opts.fragment_retries = parse_retries('fragment', opts.fragment_retries)
249 opts.extractor_retries = parse_retries('extractor', opts.extractor_retries)
250 opts.file_access_retries = parse_retries('file access', opts.file_access_retries)
251
252 # Bytes
253 def parse_bytes(name, value):
254 if value is None:
255 return None
256 numeric_limit = FileDownloader.parse_bytes(value)
257 validate(numeric_limit is not None, 'rate limit', value)
258 return numeric_limit
259
260 opts.ratelimit = parse_bytes('rate limit', opts.ratelimit)
51c22ef4 261 opts.throttledratelimit = parse_bytes('throttled rate limit', opts.throttledratelimit)
d1b5f70b 262 opts.min_filesize = parse_bytes('min filesize', opts.min_filesize)
263 opts.max_filesize = parse_bytes('max filesize', opts.max_filesize)
264 opts.buffersize = parse_bytes('buffer size', opts.buffersize)
265 opts.http_chunk_size = parse_bytes('http chunk size', opts.http_chunk_size)
266
267 # Output templates
76a264ac 268 def validate_outtmpl(tmpl, msg):
269 err = YoutubeDL.validate_outtmpl(tmpl)
270 if err:
d1b5f70b 271 raise ValueError(f'invalid {msg} "{tmpl}": {err}')
76a264ac 272
273 for k, tmpl in opts.outtmpl.items():
819e0531 274 validate_outtmpl(tmpl, f'{k} output template')
ca30f449 275 for type_, tmpl_list in opts.forceprint.items():
276 for tmpl in tmpl_list:
277 validate_outtmpl(tmpl, f'{type_} print template')
bb66c247 278 for type_, tmpl_list in opts.print_to_file.items():
279 for tmpl, file in tmpl_list:
d1b5f70b 280 validate_outtmpl(tmpl, f'{type_} print to file template')
281 validate_outtmpl(file, f'{type_} print to file filename')
7a340e0d 282 validate_outtmpl(opts.sponsorblock_chapter_title, 'SponsorBlock chapter title')
819e0531 283 for k, tmpl in opts.progress_template.items():
284 k = f'{k[:-6]} console title' if '-title' in k else f'{k} progress'
285 validate_outtmpl(tmpl, f'{k} template')
76a264ac 286
d1b5f70b 287 outtmpl_default = opts.outtmpl.get('default')
288 if outtmpl_default == '':
289 opts.skip_download = None
290 del opts.outtmpl['default']
291 if outtmpl_default and not os.path.splitext(outtmpl_default)[1] and opts.extractaudio:
292 raise ValueError(
293 'Cannot download a video and extract audio into the same file! '
294 f'Use "{outtmpl_default}.%(ext)s" instead of "{outtmpl_default}" as the output template')
295
296 # Remove chapters
297 remove_chapters_patterns, opts.remove_ranges = [], []
298 for regex in opts.remove_chapters or []:
299 if regex.startswith('*'):
300 dur = list(map(parse_duration, regex[1:].split('-')))
301 if len(dur) == 2 and all(t is not None for t in dur):
302 opts.remove_ranges.append(tuple(dur))
303 continue
304 raise ValueError(f'invalid --remove-chapters time range "{regex}". Must be of the form *start-end')
305 try:
306 remove_chapters_patterns.append(re.compile(regex))
307 except re.error as err:
308 raise ValueError(f'invalid --remove-chapters regex "{regex}" - {err}')
309 opts.remove_chapters = remove_chapters_patterns
0202b52a 310
d1b5f70b 311 # Cookies from browser
312 if opts.cookiesfrombrowser:
313 mobj = re.match(r'(?P<name>[^+:]+)(\s*\+\s*(?P<keyring>[^:]+))?(\s*:(?P<profile>.+))?', opts.cookiesfrombrowser)
314 if mobj is None:
315 raise ValueError(f'invalid cookies from browser arguments: {opts.cookiesfrombrowser}')
316 browser_name, keyring, profile = mobj.group('name', 'keyring', 'profile')
317 browser_name = browser_name.lower()
318 if browser_name not in SUPPORTED_BROWSERS:
319 raise ValueError(f'unsupported browser specified for cookies: "{browser_name}". '
320 f'Supported browsers are: {", ".join(sorted(SUPPORTED_BROWSERS))}')
321 if keyring is not None:
322 keyring = keyring.upper()
323 if keyring not in SUPPORTED_KEYRINGS:
324 raise ValueError(f'unsupported keyring specified for cookies: "{keyring}". '
325 f'Supported keyrings are: {", ".join(sorted(SUPPORTED_KEYRINGS))}')
326 opts.cookiesfrombrowser = (browser_name, profile, keyring)
29c7a63d 327
d1b5f70b 328 # MetadataParser
e9f4ccd1 329 def metadataparser_actions(f):
330 if isinstance(f, str):
331 cmd = '--parse-metadata %s' % compat_shlex_quote(f)
332 try:
333 actions = [MetadataFromFieldPP.to_action(f)]
334 except Exception as err:
d1b5f70b 335 raise ValueError(f'{cmd} is invalid; {err}')
e9f4ccd1 336 else:
337 cmd = '--replace-in-metadata %s' % ' '.join(map(compat_shlex_quote, f))
338 actions = ((MetadataParserPP.Actions.REPLACE, x, *f[1:]) for x in f[0].split(','))
339
340 for action in actions:
341 try:
342 MetadataParserPP.validate_action(*action)
343 except Exception as err:
d1b5f70b 344 raise ValueError(f'{cmd} is invalid; {err}')
e9f4ccd1 345 yield action
346
d1b5f70b 347 parse_metadata = opts.parse_metadata or []
5bfa4862 348 if opts.metafromtitle is not None:
d1b5f70b 349 parse_metadata.append('title:%s' % opts.metafromtitle)
350 opts.parse_metadata = list(itertools.chain(*map(metadataparser_actions, parse_metadata)))
5bfa4862 351
d1b5f70b 352 # Other options
353 geo_bypass_code = opts.geo_bypass_ip_block or opts.geo_bypass_country
354 if geo_bypass_code is not None:
355 try:
356 GeoUtils.random_ipv4(geo_bypass_code)
357 except Exception:
358 raise ValueError('unsupported geo-bypass country or ip-block')
ca30f449 359
d1b5f70b 360 opts.match_filter = match_filter_func(opts.match_filter)
525ef922 361
d1b5f70b 362 if opts.download_archive is not None:
363 opts.download_archive = expand_path(opts.download_archive)
364
365 if opts.user_agent is not None:
366 opts.headers.setdefault('User-Agent', opts.user_agent)
367 if opts.referer is not None:
368 opts.headers.setdefault('Referer', opts.referer)
f0884c8b 369
7a340e0d 370 if opts.no_sponsorblock:
d1b5f70b 371 opts.sponsorblock_mark = opts.sponsorblock_remove = set()
372
373 warnings, deprecation_warnings = [], []
7a340e0d 374
d1b5f70b 375 # Common mistake: -f best
376 if opts.format == 'best':
377 warnings.append('.\n '.join((
378 '"-f best" selects the best pre-merged format which is often not the best option',
379 'To let yt-dlp download and merge the best available formats, simply do not pass any format selection',
380 'If you know what you are doing and want only the best pre-merged format, use "-f b" instead to suppress this warning')))
381
382 # --(post-processor/downloader)-args without name
383 def report_args_compat(name, value, key1, key2=None):
384 if key1 in value and key2 not in value:
385 warnings.append(f'{name} arguments given without specifying name. The arguments will be given to all {name}s')
386 return True
387 return False
388
389 report_args_compat('external downloader', opts.external_downloader_args, 'default')
390 if report_args_compat('post-processor', opts.postprocessor_args, 'default-compat', 'default'):
391 opts.postprocessor_args['default'] = opts.postprocessor_args.pop('default-compat')
392 opts.postprocessor_args.setdefault('sponskrub', [])
393
394 def report_conflict(arg1, opt1, arg2='--allow-unplayable-formats', opt2='allow_unplayable_formats',
395 val1=NO_DEFAULT, val2=NO_DEFAULT, default=False):
396 if val2 is NO_DEFAULT:
397 val2 = getattr(opts, opt2)
398 if not val2:
399 return
400
401 if val1 is NO_DEFAULT:
402 val1 = getattr(opts, opt1)
403 if val1:
404 warnings.append(f'{arg1} is ignored since {arg2} was given')
405 setattr(opts, opt1, default)
406
407 # Conflicting options
da1d734f 408 report_conflict('--dateafter', 'dateafter', '--date', 'date', default=None)
409 report_conflict('--datebefore', 'datebefore', '--date', 'date', default=None)
d1b5f70b 410 report_conflict('--exec-before-download', 'exec_before_dl_cmd', '"--exec before_dl:"', 'exec_cmd', opts.exec_cmd.get('before_dl'))
411 report_conflict('--id', 'useid', '--output', 'outtmpl', val2=opts.outtmpl.get('default'))
412 report_conflict('--remux-video', 'remuxvideo', '--recode-video', 'recodevideo')
413 report_conflict('--sponskrub', 'sponskrub', '--remove-chapters', 'remove_chapters')
414 report_conflict('--sponskrub', 'sponskrub', '--sponsorblock-mark', 'sponsorblock_mark')
415 report_conflict('--sponskrub', 'sponskrub', '--sponsorblock-remove', 'sponsorblock_remove')
416 report_conflict('--sponskrub-cut', 'sponskrub_cut', '--split-chapter', 'split_chapters', val1=opts.sponskrub and opts.sponskrub_cut)
417
418 # Conflicts with --allow-unplayable-formats
419 report_conflict('--add-metadata', 'addmetadata')
420 report_conflict('--embed-chapters', 'addchapters')
421 report_conflict('--embed-info-json', 'embed_infojson')
422 report_conflict('--embed-subs', 'embedsubtitles')
423 report_conflict('--embed-thumbnail', 'embedthumbnail')
424 report_conflict('--extract-audio', 'extractaudio')
425 report_conflict('--fixup', 'fixup', val1=(opts.fixup or '').lower() in ('', 'never', 'ignore'), default='never')
426 report_conflict('--recode-video', 'recodevideo')
427 report_conflict('--remove-chapters', 'remove_chapters', default=[])
428 report_conflict('--remux-video', 'remuxvideo')
429 report_conflict('--sponskrub', 'sponskrub')
430 report_conflict('--sponsorblock-remove', 'sponsorblock_remove', default=set())
431 report_conflict('--xattrs', 'xattrs')
432
433 # Fully deprecated options
434 def report_deprecation(val, old, new=None):
435 if not val:
436 return
437 deprecation_warnings.append(
438 f'{old} is deprecated and may be removed in a future version. Use {new} instead' if new
439 else f'{old} is deprecated and may not work as expected')
440
441 report_deprecation(opts.sponskrub, '--sponskrub', '--sponsorblock-mark or --sponsorblock-remove')
442 report_deprecation(not opts.prefer_ffmpeg, '--prefer-avconv', 'ffmpeg')
443 # report_deprecation(opts.include_ads, '--include-ads') # We may re-implement this in future
444 # report_deprecation(opts.call_home, '--call-home') # We may re-implement this in future
445 # report_deprecation(opts.writeannotations, '--write-annotations') # It's just that no website has it
446
447 # Dependent options
da1d734f 448 opts.date = DateRange.day(opts.date) if opts.date else DateRange(opts.dateafter, opts.datebefore)
449
d1b5f70b 450 if opts.exec_before_dl_cmd:
451 opts.exec_cmd['before_dl'] = opts.exec_before_dl_cmd
452
453 if opts.useid: # --id is not deprecated in youtube-dl
454 opts.outtmpl['default'] = '%(id)s.%(ext)s'
455
456 if opts.overwrites: # --force-overwrites implies --no-continue
457 opts.continue_dl = False
63ad4d43 458
9222c381 459 if (opts.addmetadata or opts.sponsorblock_mark) and opts.addchapters is None:
d1b5f70b 460 # Add chapters when adding metadata or marking sponsors
9222c381 461 opts.addchapters = True
462
d1b5f70b 463 if opts.extractaudio and not opts.keepvideo and opts.format is None:
464 # Do not unnecessarily download audio
465 opts.format = 'bestaudio/best'
466
467 if opts.getcomments and opts.writeinfojson is None:
468 # If JSON is not printed anywhere, but comments are requested, save it to file
469 if not opts.dumpjson or opts.print_json or opts.dump_single_json:
470 opts.writeinfojson = True
471
472 if opts.allsubtitles and not (opts.embedsubtitles or opts.writeautomaticsub):
473 # --all-sub automatically sets --write-sub if --write-auto-sub is not given
474 opts.writesubtitles = True
475
476 if opts.addmetadata and opts.embed_infojson is None:
477 # If embedding metadata and infojson is present, embed it
478 opts.embed_infojson = 'if_exists'
479
480 # Ask for passwords
481 if opts.username is not None and opts.password is None:
482 opts.password = compat_getpass('Type account password and press [Return]: ')
483 if opts.ap_username is not None and opts.ap_password is None:
484 opts.ap_password = compat_getpass('Type TV provider account password and press [Return]: ')
485
486 return warnings, deprecation_warnings
487
488
489def get_postprocessors(opts):
490 yield from opts.add_postprocessors
491
492 if opts.parse_metadata:
493 yield {
494 'key': 'MetadataParser',
495 'actions': opts.parse_metadata,
496 'when': 'pre_process'
497 }
498 sponsorblock_query = opts.sponsorblock_mark | opts.sponsorblock_remove
7a340e0d 499 if sponsorblock_query:
d1b5f70b 500 yield {
7a340e0d
NA
501 'key': 'SponsorBlock',
502 'categories': sponsorblock_query,
503 'api': opts.sponsorblock_api,
09b49e1f 504 'when': 'after_filter'
d1b5f70b 505 }
56d868db 506 if opts.convertsubtitles:
d1b5f70b 507 yield {
56d868db 508 'key': 'FFmpegSubtitlesConvertor',
509 'format': opts.convertsubtitles,
56d868db 510 'when': 'before_dl'
d1b5f70b 511 }
8fa43c73 512 if opts.convertthumbnails:
d1b5f70b 513 yield {
8fa43c73 514 'key': 'FFmpegThumbnailsConvertor',
515 'format': opts.convertthumbnails,
8fa43c73 516 'when': 'before_dl'
d1b5f70b 517 }
4f026faf 518 if opts.extractaudio:
d1b5f70b 519 yield {
4f026faf
PH
520 'key': 'FFmpegExtractAudio',
521 'preferredcodec': opts.audioformat,
522 'preferredquality': opts.audioquality,
523 'nopostoverwrites': opts.nopostoverwrites,
d1b5f70b 524 }
efe87a10 525 if opts.remuxvideo:
d1b5f70b 526 yield {
efe87a10
FS
527 'key': 'FFmpegVideoRemuxer',
528 'preferedformat': opts.remuxvideo,
d1b5f70b 529 }
4f026faf 530 if opts.recodevideo:
d1b5f70b 531 yield {
4f026faf
PH
532 'key': 'FFmpegVideoConvertor',
533 'preferedformat': opts.recodevideo,
d1b5f70b 534 }
7a340e0d 535 # If ModifyChapters is going to remove chapters, subtitles must already be in the container.
4f026faf 536 if opts.embedsubtitles:
d1b5f70b 537 keep_subs = 'no-keep-subs' not in opts.compat_opts
538 yield {
4f026faf 539 'key': 'FFmpegEmbedSubtitle',
56d868db 540 # already_have_subtitle = True prevents the file from being deleted after embedding
d1b5f70b 541 'already_have_subtitle': opts.writesubtitles and keep_subs
542 }
543 if not opts.writeautomaticsub and keep_subs:
cffab0ee 544 opts.writesubtitles = True
d1b5f70b 545
7a340e0d 546 # ModifyChapters must run before FFmpegMetadataPP
7a340e0d 547 if opts.remove_chapters or sponsorblock_query:
d1b5f70b 548 yield {
7a340e0d 549 'key': 'ModifyChapters',
d1b5f70b 550 'remove_chapters_patterns': opts.remove_chapters,
7a340e0d 551 'remove_sponsor_segments': opts.sponsorblock_remove,
d1b5f70b 552 'remove_ranges': opts.remove_ranges,
7a340e0d
NA
553 'sponsorblock_chapter_title': opts.sponsorblock_chapter_title,
554 'force_keyframes': opts.force_keyframes_at_cuts
d1b5f70b 555 }
7a340e0d
NA
556 # FFmpegMetadataPP should be run after FFmpegVideoConvertorPP and
557 # FFmpegExtractAudioPP as containers before conversion may not support
558 # metadata (3gp, webm, etc.)
559 # By default ffmpeg preserves metadata applicable for both
560 # source and target containers. From this point the container won't change,
561 # so metadata can be added here.
dac5df5a 562 if opts.addmetadata or opts.addchapters or opts.embed_infojson:
d1b5f70b 563 yield {
7a340e0d
NA
564 'key': 'FFmpegMetadata',
565 'add_chapters': opts.addchapters,
566 'add_metadata': opts.addmetadata,
dac5df5a 567 'add_infojson': opts.embed_infojson,
d1b5f70b 568 }
ee8dd27a 569 # Deprecated
f4e4be19 570 # This should be above EmbedThumbnail since sponskrub removes the thumbnail attachment
571 # but must be below EmbedSubtitle and FFmpegMetadata
572 # See https://github.com/yt-dlp/yt-dlp/issues/204 , https://github.com/faissaloo/SponSkrub/issues/29
a9e7f546 573 # If opts.sponskrub is None, sponskrub is used, but it silently fails if the executable can't be found
574 if opts.sponskrub is not False:
d1b5f70b 575 yield {
a9e7f546 576 'key': 'SponSkrub',
577 'path': opts.sponskrub_path,
578 'args': opts.sponskrub_args,
579 'cut': opts.sponskrub_cut,
580 'force': opts.sponskrub_force,
581 'ignoreerror': opts.sponskrub is None,
ee8dd27a 582 '_from_cli': True,
d1b5f70b 583 }
f4e4be19 584 if opts.embedthumbnail:
d1b5f70b 585 yield {
f4e4be19 586 'key': 'EmbedThumbnail',
56d868db 587 # already_have_thumbnail = True prevents the file from being deleted after embedding
acc0d6a4 588 'already_have_thumbnail': opts.writethumbnail
d1b5f70b 589 }
acc0d6a4 590 if not opts.writethumbnail:
f4e4be19 591 opts.writethumbnail = True
80c03fa9 592 opts.outtmpl['pl_thumbnail'] = ''
72755351 593 if opts.split_chapters:
d1b5f70b 594 yield {
7a340e0d
NA
595 'key': 'FFmpegSplitChapters',
596 'force_keyframes': opts.force_keyframes_at_cuts,
d1b5f70b 597 }
72755351 598 # XAttrMetadataPP should be run after post-processors that may change file contents
599 if opts.xattrs:
d1b5f70b 600 yield {'key': 'XAttrMetadata'}
3b603dbd 601 if opts.concat_playlist != 'never':
d1b5f70b 602 yield {
3b603dbd 603 'key': 'FFmpegConcat',
604 'only_multi_video': opts.concat_playlist != 'always',
605 'when': 'playlist',
d1b5f70b 606 }
1e43a6f7 607 # Exec must be the last PP of each category
1e43a6f7 608 for when, exec_cmd in opts.exec_cmd.items():
d1b5f70b 609 yield {
ad3dc496 610 'key': 'Exec',
1e43a6f7 611 'exec_cmd': exec_cmd,
1e43a6f7 612 'when': when,
d1b5f70b 613 }
1b77b347 614
0d1bb027 615
d1b5f70b 616def parse_options(argv=None):
617 """ @returns (parser, opts, urls, ydl_opts) """
618 parser, opts, urls = parseOpts(argv)
619 urls = get_urls(urls, opts.batchfile, opts.verbose)
b8f6bbe6 620
d1b5f70b 621 set_compat_opts(opts)
622 try:
623 warnings, deprecation_warnings = validate_options(opts)
624 except ValueError as err:
625 parser.error(f'{err}\n')
1b77b347 626
d1b5f70b 627 postprocessors = list(get_postprocessors(opts))
ee8dd27a 628
d1b5f70b 629 any_getting = (any(opts.forceprint.values()) or opts.dumpjson or opts.dump_single_json
630 or opts.geturl or opts.gettitle or opts.getid or opts.getthumbnail
631 or opts.getdescription or opts.getfilename or opts.getformat or opts.getduration)
632
633 any_printing = opts.print_json
ee8dd27a 634
df692c5a 635 final_ext = (
81a23040 636 opts.recodevideo if opts.recodevideo in FFmpegVideoConvertorPP.SUPPORTED_EXTS
637 else opts.remuxvideo if opts.remuxvideo in FFmpegVideoRemuxerPP.SUPPORTED_EXTS
638 else opts.audioformat if (opts.extractaudio and opts.audioformat != 'best')
639 else None)
f6d7624f 640
d1b5f70b 641 return parser, opts, urls, {
59ae15a5 642 'usenetrc': opts.usenetrc,
0001fcb5 643 'netrc_location': opts.netrc_location,
59ae15a5
PH
644 'username': opts.username,
645 'password': opts.password,
83317f69 646 'twofactor': opts.twofactor,
c6c19746 647 'videopassword': opts.videopassword,
797c636b 648 'ap_mso': opts.ap_mso,
1b6712ab
RA
649 'ap_username': opts.ap_username,
650 'ap_password': opts.ap_password,
c0bdf32a 651 'quiet': (opts.quiet or any_getting or any_printing),
ad8915b7 652 'no_warnings': opts.no_warnings,
59ae15a5
PH
653 'forceurl': opts.geturl,
654 'forcetitle': opts.gettitle,
1a2adf3f 655 'forceid': opts.getid,
59ae15a5
PH
656 'forcethumbnail': opts.getthumbnail,
657 'forcedescription': opts.getdescription,
525ef922 658 'forceduration': opts.getduration,
59ae15a5
PH
659 'forcefilename': opts.getfilename,
660 'forceformat': opts.getformat,
d2a1fad9 661 'forceprint': opts.forceprint,
bb66c247 662 'print_to_file': opts.print_to_file,
c0bdf32a 663 'forcejson': opts.dumpjson or opts.print_json,
63e0be34 664 'dump_single_json': opts.dump_single_json,
2d30509f 665 'force_write_download_archive': opts.force_write_download_archive,
b7b04c78 666 'simulate': (any_getting or None) if opts.simulate is None else opts.simulate,
1bdeb7be 667 'skip_download': opts.skip_download,
59ae15a5 668 'format': opts.format,
63ad4d43 669 'allow_unplayable_formats': opts.allow_unplayable_formats,
b7da73eb 670 'ignore_no_formats_error': opts.ignore_no_formats_error,
eb8a4433 671 'format_sort': opts.format_sort,
672 'format_sort_force': opts.format_sort_force,
909d24dd 673 'allow_multiple_video_streams': opts.allow_multiple_video_streams,
674 'allow_multiple_audio_streams': opts.allow_multiple_audio_streams,
e8e73840 675 'check_formats': opts.check_formats,
59ae15a5 676 'listformats': opts.listformats,
76d321f6 677 'listformats_table': opts.listformats_table,
486fb179 678 'outtmpl': opts.outtmpl,
a820dc72 679 'outtmpl_na_placeholder': opts.outtmpl_na_placeholder,
0202b52a 680 'paths': opts.paths,
213c31ae 681 'autonumber_size': opts.autonumber_size,
acbb2374 682 'autonumber_start': opts.autonumber_start,
59ae15a5 683 'restrictfilenames': opts.restrictfilenames,
c2934512 684 'windowsfilenames': opts.windowsfilenames,
59ae15a5 685 'ignoreerrors': opts.ignoreerrors,
d22dec74 686 'force_generic_extractor': opts.force_generic_extractor,
59ae15a5 687 'ratelimit': opts.ratelimit,
51d9739f 688 'throttledratelimit': opts.throttledratelimit,
0c3d0f51 689 'overwrites': opts.overwrites,
52bb437e 690 'retries': opts.retries,
205a0654 691 'file_access_retries': opts.file_access_retries,
52bb437e 692 'fragment_retries': opts.fragment_retries,
62bff2c1 693 'extractor_retries': opts.extractor_retries,
9603b660 694 'skip_unavailable_fragments': opts.skip_unavailable_fragments,
0eee52f3 695 'keep_fragments': opts.keep_fragments,
4cf1e5d2 696 'concurrent_fragment_downloads': opts.concurrent_fragment_downloads,
59ae15a5
PH
697 'buffersize': opts.buffersize,
698 'noresizebuffer': opts.noresizebuffer,
ba515388 699 'http_chunk_size': opts.http_chunk_size,
59ae15a5 700 'continuedl': opts.continue_dl,
819e0531 701 'noprogress': opts.quiet if opts.noprogress is None else opts.noprogress,
5717d91a 702 'progress_with_newline': opts.progress_with_newline,
819e0531 703 'progress_template': opts.progress_template,
59ae15a5
PH
704 'playliststart': opts.playliststart,
705 'playlistend': opts.playlistend,
ff815fe6 706 'playlistreverse': opts.playlist_reverse,
75822ca7 707 'playlistrandom': opts.playlist_random,
47192f92 708 'noplaylist': opts.noplaylist,
d1b5f70b 709 'logtostderr': opts.outtmpl.get('default') == '-',
59ae15a5
PH
710 'consoletitle': opts.consoletitle,
711 'nopart': opts.nopart,
712 'updatetime': opts.updatetime,
713 'writedescription': opts.writedescription,
1fb07d10 714 'writeannotations': opts.writeannotations,
f0884c8b 715 'writeinfojson': opts.writeinfojson,
1ea24129 716 'allow_playlist_files': opts.allow_playlist_files,
75d43ca0 717 'clean_infojson': opts.clean_infojson,
06167fbb 718 'getcomments': opts.getcomments,
acc0d6a4 719 'writethumbnail': opts.writethumbnail is True,
720 'write_all_thumbnails': opts.writethumbnail == 'all',
732044af 721 'writelink': opts.writelink,
722 'writeurllink': opts.writeurllink,
723 'writewebloclink': opts.writewebloclink,
724 'writedesktoplink': opts.writedesktoplink,
59ae15a5 725 'writesubtitles': opts.writesubtitles,
b004821f 726 'writeautomaticsub': opts.writeautomaticsub,
ae608b80 727 'allsubtitles': opts.allsubtitles,
2a4093ea 728 'listsubtitles': opts.listsubtitles,
9e62bc44 729 'subtitlesformat': opts.subtitlesformat,
d6e203b3 730 'subtitleslangs': opts.subtitleslangs,
8271226a
PH
731 'matchtitle': decodeOption(opts.matchtitle),
732 'rejecttitle': decodeOption(opts.rejecttitle),
59ae15a5
PH
733 'max_downloads': opts.max_downloads,
734 'prefer_free_formats': opts.prefer_free_formats,
bdc3fd2f 735 'trim_file_name': opts.trim_file_name,
59ae15a5 736 'verbose': opts.verbose,
855703e5 737 'dump_intermediate_pages': opts.dump_intermediate_pages,
d41e6efc 738 'write_pages': opts.write_pages,
8d5d3a5d 739 'test': opts.test,
7851b379 740 'keepvideo': opts.keepvideo,
9e982f9e 741 'min_filesize': opts.min_filesize,
bd558525 742 'max_filesize': opts.max_filesize,
5fe18bdb
PH
743 'min_views': opts.min_views,
744 'max_views': opts.max_views,
d1b5f70b 745 'daterange': opts.date,
7f747732 746 'cachedir': opts.cachedir,
f8061589 747 'youtube_print_sig_code': opts.youtube_print_sig_code,
8dbe9899 748 'age_limit': opts.age_limit,
d1b5f70b 749 'download_archive': opts.download_archive,
ea6e0c2b 750 'break_on_existing': opts.break_on_existing,
8b0d7497 751 'break_on_reject': opts.break_on_reject,
b222c271 752 'break_per_url': opts.break_per_url,
26e2805c 753 'skip_playlist_after_errors': opts.skip_playlist_after_errors,
dca08720 754 'cookiefile': opts.cookiefile,
982ee69a 755 'cookiesfrombrowser': opts.cookiesfrombrowser,
f81c62a6 756 'legacyserverconnect': opts.legacy_server_connect,
dca08720 757 'nocheckcertificate': opts.no_check_certificate,
7e8c0af0 758 'prefer_insecure': opts.prefer_insecure,
8b7539d2 759 'http_headers': opts.headers,
c2e52508 760 'proxy': opts.proxy,
6ad14cab 761 'socket_timeout': opts.socket_timeout,
0783b09b 762 'bidi_workaround': opts.bidi_workaround,
a0ddb8a2 763 'debug_printtraffic': opts.debug_printtraffic,
76b1bd67 764 'prefer_ffmpeg': opts.prefer_ffmpeg,
7b0817e8 765 'include_ads': opts.include_ads,
04b4d394 766 'default_search': opts.default_search,
78895bd3 767 'dynamic_mpd': opts.dynamic_mpd,
5d3a0e79 768 'extractor_args': opts.extractor_args,
4919603f 769 'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
78895bd3 770 'youtube_include_hls_manifest': opts.youtube_include_hls_manifest,
62fec3b2 771 'encoding': opts.encoding,
057a5206 772 'extract_flat': opts.extract_flat,
adbc4ec4 773 'live_from_start': opts.live_from_start,
f2ebc5c7 774 'wait_for_video': opts.wait_for_video,
d77ab8e2 775 'mark_watched': opts.mark_watched,
34c781a2 776 'merge_output_format': opts.merge_output_format,
df692c5a 777 'final_ext': final_ext,
4f026faf 778 'postprocessors': postprocessors,
6271f1ca 779 'fixup': opts.fixup,
be4a824d 780 'source_address': opts.source_address,
58b1f00d 781 'call_home': opts.call_home,
1cf376f5 782 'sleep_interval_requests': opts.sleep_interval_requests,
5f0d813d 783 'sleep_interval': opts.sleep_interval,
065bc354 784 'max_sleep_interval': opts.max_sleep_interval,
0c9df79e 785 'sleep_interval_subtitles': opts.sleep_interval_subtitles,
222516d9 786 'external_downloader': opts.external_downloader,
cfb56d1a 787 'list_thumbnails': opts.list_thumbnails,
c14e88f0 788 'playlist_items': opts.playlist_items,
881e6a1f 789 'xattr_set_filesize': opts.xattr_set_filesize,
d1b5f70b 790 'match_filter': opts.match_filter,
7e5db8c9 791 'no_color': opts.no_color,
73fac4e9 792 'ffmpeg_location': opts.ffmpeg_location,
85729c51 793 'hls_prefer_native': opts.hls_prefer_native,
7d106a65 794 'hls_use_mpegts': opts.hls_use_mpegts,
310c2ed2 795 'hls_split_discontinuity': opts.hls_split_discontinuity,
46ee996e 796 'external_downloader_args': opts.external_downloader_args,
45016689 797 'postprocessor_args': opts.postprocessor_args,
91410c9b 798 'cn_verification_proxy': opts.cn_verification_proxy,
38cce791 799 'geo_verification_proxy': opts.geo_verification_proxy,
0a840f58
S
800 'geo_bypass': opts.geo_bypass,
801 'geo_bypass_country': opts.geo_bypass_country,
5f95927a 802 'geo_bypass_ip_block': opts.geo_bypass_ip_block,
49a57e70 803 '_warnings': warnings,
ee8dd27a 804 '_deprecation_warnings': deprecation_warnings,
d1b5f70b 805 'compat_opts': opts.compat_opts,
bdde425c 806 }
59ae15a5 807
d1b5f70b 808
809def _real_main(argv=None):
810 # Compatibility fixes for Windows
811 if sys.platform == 'win32':
812 # https://github.com/ytdl-org/youtube-dl/issues/820
813 codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)
814
815 workaround_optparse_bug9161()
816
817 setproctitle('yt-dlp')
818
819 parser, opts, all_urls, ydl_opts = parse_options(argv)
820
821 # Dump user agent
822 if opts.dump_user_agent:
823 ua = traverse_obj(opts.headers, 'User-Agent', casesense=False, default=std_headers['User-Agent'])
824 write_string(f'{ua}\n', out=sys.stdout)
e880c92c 825 sys.exit(0)
d1b5f70b 826
827 if print_extractor_information(opts, all_urls):
828 sys.exit(0)
829
bdde425c 830 with YoutubeDL(ydl_opts) as ydl:
f304da8a 831 actual_use = all_urls or opts.load_info_filename
bdde425c 832
052421ff
PH
833 # Remove cache dir
834 if opts.rm_cachedir:
a0e07d31 835 ydl.cache.remove()
052421ff 836
e5813e53 837 # Update version
838 if opts.update_self:
839 # If updater returns True, exit. Required for windows
c19bc311 840 if run_update(ydl):
e5813e53 841 if actual_use:
6b027907 842 sys.exit('ERROR: The program must exit for the update to complete')
e5813e53 843 sys.exit()
844
bdde425c 845 # Maybe do nothing
e5813e53 846 if not actual_use:
7d4111ed 847 if opts.update_self or opts.rm_cachedir:
bdde425c 848 sys.exit()
59ae15a5 849
7d4111ed 850 ydl.warn_if_short_id(sys.argv[1:] if argv is None else argv)
adc0ae3c
PH
851 parser.error(
852 'You must provide at least one URL.\n'
7a5c1cfe 853 'Type yt-dlp --help to see a list of all options.')
7d4111ed 854
bdde425c 855 try:
1dcc4c0c 856 if opts.load_info_filename is not None:
590bc6f6 857 retcode = ydl.download_with_info_file(expand_path(opts.load_info_filename))
1dcc4c0c
JMF
858 else:
859 retcode = ydl.download(all_urls)
f304da8a 860 except DownloadCancelled:
8b0d7497 861 ydl.to_screen('Aborting remaining downloads')
bdde425c 862 retcode = 101
59ae15a5 863
59ae15a5 864 sys.exit(retcode)
235b3ba4 865
a27b9e8b 866
b8ad4f02 867def main(argv=None):
59ae15a5 868 try:
b8ad4f02 869 _real_main(argv)
59ae15a5
PH
870 except DownloadError:
871 sys.exit(1)
aa9369a2 872 except SameFileError as e:
873 sys.exit(f'ERROR: {e}')
59ae15a5 874 except KeyboardInterrupt:
a4bc4336 875 sys.exit('\nERROR: Interrupted by user')
aa9369a2 876 except BrokenPipeError as e:
cc3fa8d3 877 # https://docs.python.org/3/library/signal.html#note-on-sigpipe
878 devnull = os.open(os.devnull, os.O_WRONLY)
879 os.dup2(devnull, sys.stdout.fileno())
aa9369a2 880 sys.exit(f'\nERROR: {e}')
2bad0e5d 881
582be358 882
d1b5f70b 883__all__ = [
884 'main',
885 'YoutubeDL',
886 'parse_options',
887 'gen_extractors',
888 'list_extractors',
889]