]> asedeno.scripts.mit.edu Git - youtube-dl.git/blob - youtube_dl/YoutubeDL.py
Update options.py
[youtube-dl.git] / youtube_dl / YoutubeDL.py
1 #!/usr/bin/env python
2 # coding: utf-8
3
4 from __future__ import absolute_import, unicode_literals
5
6 import collections
7 import contextlib
8 import copy
9 import datetime
10 import errno
11 import fileinput
12 import io
13 import itertools
14 import json
15 import locale
16 import operator
17 import os
18 import platform
19 import re
20 import shutil
21 import subprocess
22 import socket
23 import sys
24 import time
25 import tokenize
26 import traceback
27 import random
28
29 from string import ascii_letters
30
31 from .compat import (
32     compat_basestring,
33     compat_cookiejar,
34     compat_get_terminal_size,
35     compat_http_client,
36     compat_kwargs,
37     compat_numeric_types,
38     compat_os_name,
39     compat_str,
40     compat_tokenize_tokenize,
41     compat_urllib_error,
42     compat_urllib_request,
43     compat_urllib_request_DataHandler,
44 )
45 from .utils import (
46     age_restricted,
47     args_to_str,
48     ContentTooShortError,
49     date_from_str,
50     DateRange,
51     DEFAULT_OUTTMPL,
52     determine_ext,
53     determine_protocol,
54     DownloadError,
55     encode_compat_str,
56     encodeFilename,
57     error_to_compat_str,
58     expand_path,
59     ExtractorError,
60     format_bytes,
61     formatSeconds,
62     GeoRestrictedError,
63     int_or_none,
64     ISO3166Utils,
65     locked_file,
66     make_HTTPS_handler,
67     MaxDownloadsReached,
68     orderedSet,
69     PagedList,
70     parse_filesize,
71     PerRequestProxyHandler,
72     platform_name,
73     PostProcessingError,
74     preferredencoding,
75     prepend_extension,
76     register_socks_protocols,
77     render_table,
78     replace_extension,
79     SameFileError,
80     sanitize_filename,
81     sanitize_path,
82     sanitize_url,
83     sanitized_Request,
84     std_headers,
85     str_or_none,
86     subtitles_filename,
87     UnavailableVideoError,
88     url_basename,
89     version_tuple,
90     write_json_file,
91     write_string,
92     YoutubeDLCookieJar,
93     YoutubeDLCookieProcessor,
94     YoutubeDLHandler,
95     YoutubeDLRedirectHandler,
96 )
97 from .cache import Cache
98 from .extractor import get_info_extractor, gen_extractor_classes, _LAZY_LOADER
99 from .extractor.openload import PhantomJSwrapper
100 from .downloader import get_suitable_downloader
101 from .downloader.rtmp import rtmpdump_version
102 from .postprocessor import (
103     FFmpegFixupM3u8PP,
104     FFmpegFixupM4aPP,
105     FFmpegFixupStretchedPP,
106     FFmpegMergerPP,
107     FFmpegPostProcessor,
108     get_postprocessor,
109 )
110 from .version import __version__
111
112 if compat_os_name == 'nt':
113     import ctypes
114
115
116 class YoutubeDL(object):
117     """YoutubeDL class.
118
119     YoutubeDL objects are the ones responsible of downloading the
120     actual video file and writing it to disk if the user has requested
121     it, among some other tasks. In most cases there should be one per
122     program. As, given a video URL, the downloader doesn't know how to
123     extract all the needed information, task that InfoExtractors do, it
124     has to pass the URL to one of them.
125
126     For this, YoutubeDL objects have a method that allows
127     InfoExtractors to be registered in a given order. When it is passed
128     a URL, the YoutubeDL object handles it to the first InfoExtractor it
129     finds that reports being able to handle it. The InfoExtractor extracts
130     all the information about the video or videos the URL refers to, and
131     YoutubeDL process the extracted information, possibly using a File
132     Downloader to download the video.
133
134     YoutubeDL objects accept a lot of parameters. In order not to saturate
135     the object constructor with arguments, it receives a dictionary of
136     options instead. These options are available through the params
137     attribute for the InfoExtractors to use. The YoutubeDL also
138     registers itself as the downloader in charge for the InfoExtractors
139     that are added to it, so this is a "mutual registration".
140
141     Available options:
142
143     username:          Username for authentication purposes.
144     password:          Password for authentication purposes.
145     videopassword:     Password for accessing a video.
146     ap_mso:            Adobe Pass multiple-system operator identifier.
147     ap_username:       Multiple-system operator account username.
148     ap_password:       Multiple-system operator account password.
149     usenetrc:          Use netrc for authentication instead.
150     verbose:           Print additional info to stdout.
151     quiet:             Do not print messages to stdout.
152     no_warnings:       Do not print out anything for warnings.
153     forceurl:          Force printing final URL.
154     forcetitle:        Force printing title.
155     forceid:           Force printing ID.
156     forcethumbnail:    Force printing thumbnail URL.
157     forcedescription:  Force printing description.
158     forcefilename:     Force printing final filename.
159     forceduration:     Force printing duration.
160     forcejson:         Force printing info_dict as JSON.
161     dump_single_json:  Force printing the info_dict of the whole playlist
162                        (or video) as a single JSON line.
163     simulate:          Do not download the video files.
164     format:            Video format code. See options.py for more information.
165     outtmpl:           Template for output names.
166     outtmpl_na_placeholder: Placeholder for unavailable meta fields.
167     restrictfilenames: Do not allow "&" and spaces in file names
168     ignoreerrors:      Do not stop on download errors.
169     force_generic_extractor: Force downloader to use the generic extractor
170     nooverwrites:      Prevent overwriting files.
171     playliststart:     Playlist item to start at.
172     playlistend:       Playlist item to end at.
173     playlist_items:    Specific indices of playlist to download.
174     playlistreverse:   Download playlist items in reverse order.
175     playlistrandom:    Download playlist items in random order.
176     matchtitle:        Download only matching titles.
177     rejecttitle:       Reject downloads for matching titles.
178     logger:            Log messages to a logging.Logger instance.
179     logtostderr:       Log messages to stderr instead of stdout.
180     writedescription:  Write the video description to a .description file
181     writeinfojson:     Write the video description to a .info.json file
182     writeannotations:  Write the video annotations to a .annotations.xml file
183     writethumbnail:    Write the thumbnail image to a file
184     write_all_thumbnails:  Write all thumbnail formats to files
185     writesubtitles:    Write the video subtitles to a file
186     writeautomaticsub: Write the automatically generated subtitles to a file
187     allsubtitles:      Downloads all the subtitles of the video
188                        (requires writesubtitles or writeautomaticsub)
189     listsubtitles:     Lists all available subtitles for the video
190     subtitlesformat:   The format code for subtitles
191     subtitleslangs:    List of languages of the subtitles to download
192     keepvideo:         Keep the video file after post-processing
193     daterange:         A DateRange object, download only if the upload_date is in the range.
194     skip_download:     Skip the actual download of the video file
195     cachedir:          Location of the cache files in the filesystem.
196                        False to disable filesystem cache.
197     noplaylist:        Download single video instead of a playlist if in doubt.
198     age_limit:         An integer representing the user's age in years.
199                        Unsuitable videos for the given age are skipped.
200     min_views:         An integer representing the minimum view count the video
201                        must have in order to not be skipped.
202                        Videos without view count information are always
203                        downloaded. None for no limit.
204     max_views:         An integer representing the maximum view count.
205                        Videos that are more popular than that are not
206                        downloaded.
207                        Videos without view count information are always
208                        downloaded. None for no limit.
209     download_archive:  File name of a file where all downloads are recorded.
210                        Videos already present in the file are not downloaded
211                        again.
212     cookiefile:        File name where cookies should be read from and dumped to.
213     nocheckcertificate:Do not verify SSL certificates
214     prefer_insecure:   Use HTTP instead of HTTPS to retrieve information.
215                        At the moment, this is only supported by YouTube.
216     proxy:             URL of the proxy server to use
217     geo_verification_proxy:  URL of the proxy to use for IP address verification
218                        on geo-restricted sites.
219     socket_timeout:    Time to wait for unresponsive hosts, in seconds
220     bidi_workaround:   Work around buggy terminals without bidirectional text
221                        support, using fridibi
222     debug_printtraffic:Print out sent and received HTTP traffic
223     include_ads:       Download ads as well
224     default_search:    Prepend this string if an input url is not valid.
225                        'auto' for elaborate guessing
226     encoding:          Use this encoding instead of the system-specified.
227     extract_flat:      Do not resolve URLs, return the immediate result.
228                        Pass in 'in_playlist' to only show this behavior for
229                        playlist items.
230     postprocessors:    A list of dictionaries, each with an entry
231                        * key:  The name of the postprocessor. See
232                                youtube_dl/postprocessor/__init__.py for a list.
233                        as well as any further keyword arguments for the
234                        postprocessor.
235     progress_hooks:    A list of functions that get called on download
236                        progress, with a dictionary with the entries
237                        * status: One of "downloading", "error", or "finished".
238                                  Check this first and ignore unknown values.
239
240                        If status is one of "downloading", or "finished", the
241                        following properties may also be present:
242                        * filename: The final filename (always present)
243                        * tmpfilename: The filename we're currently writing to
244                        * downloaded_bytes: Bytes on disk
245                        * total_bytes: Size of the whole file, None if unknown
246                        * total_bytes_estimate: Guess of the eventual file size,
247                                                None if unavailable.
248                        * elapsed: The number of seconds since download started.
249                        * eta: The estimated time in seconds, None if unknown
250                        * speed: The download speed in bytes/second, None if
251                                 unknown
252                        * fragment_index: The counter of the currently
253                                          downloaded video fragment.
254                        * fragment_count: The number of fragments (= individual
255                                          files that will be merged)
256
257                        Progress hooks are guaranteed to be called at least once
258                        (with status "finished") if the download is successful.
259     merge_output_format: Extension to use when merging formats.
260     fixup:             Automatically correct known faults of the file.
261                        One of:
262                        - "never": do nothing
263                        - "warn": only emit a warning
264                        - "detect_or_warn": check whether we can do anything
265                                            about it, warn otherwise (default)
266     source_address:    Client-side IP address to bind to.
267     call_home:         Boolean, true iff we are allowed to contact the
268                        youtube-dl servers for debugging.
269     sleep_interval:    Number of seconds to sleep before each download when
270                        used alone or a lower bound of a range for randomized
271                        sleep before each download (minimum possible number
272                        of seconds to sleep) when used along with
273                        max_sleep_interval.
274     max_sleep_interval:Upper bound of a range for randomized sleep before each
275                        download (maximum possible number of seconds to sleep).
276                        Must only be used along with sleep_interval.
277                        Actual sleep time will be a random float from range
278                        [sleep_interval; max_sleep_interval].
279     listformats:       Print an overview of available video formats and exit.
280     list_thumbnails:   Print a table of all thumbnails and exit.
281     match_filter:      A function that gets called with the info_dict of
282                        every video.
283                        If it returns a message, the video is ignored.
284                        If it returns None, the video is downloaded.
285                        match_filter_func in utils.py is one example for this.
286     no_color:          Do not emit color codes in output.
287     geo_bypass:        Bypass geographic restriction via faking X-Forwarded-For
288                        HTTP header
289     geo_bypass_country:
290                        Two-letter ISO 3166-2 country code that will be used for
291                        explicit geographic restriction bypassing via faking
292                        X-Forwarded-For HTTP header
293     geo_bypass_ip_block:
294                        IP range in CIDR notation that will be used similarly to
295                        geo_bypass_country
296
297     The following options determine which downloader is picked:
298     external_downloader: Executable of the external downloader to call.
299                        None or unset for standard (built-in) downloader.
300     hls_prefer_native: Use the native HLS downloader instead of ffmpeg/avconv
301                        if True, otherwise use ffmpeg/avconv if False, otherwise
302                        use downloader suggested by extractor if None.
303
304     The following parameters are not used by YoutubeDL itself, they are used by
305     the downloader (see youtube_dl/downloader/common.py):
306     nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
307     noresizebuffer, retries, continuedl, noprogress, consoletitle,
308     xattr_set_filesize, external_downloader_args, hls_use_mpegts,
309     http_chunk_size.
310
311     The following options are used by the post processors:
312     prefer_ffmpeg:     If False, use avconv instead of ffmpeg if both are available,
313                        otherwise prefer ffmpeg.
314     ffmpeg_location:   Location of the ffmpeg/avconv binary; either the path
315                        to the binary or its containing directory.
316     postprocessor_args: A list of additional command-line arguments for the
317                         postprocessor.
318
319     The following options are used by the Youtube extractor:
320     youtube_include_dash_manifest: If True (default), DASH manifests and related
321                         data will be downloaded and processed by extractor.
322                         You can reduce network I/O by disabling it if you don't
323                         care about DASH.
324     """
325
326     _NUMERIC_FIELDS = set((
327         'width', 'height', 'tbr', 'abr', 'asr', 'vbr', 'fps', 'filesize', 'filesize_approx',
328         'timestamp', 'upload_year', 'upload_month', 'upload_day',
329         'duration', 'view_count', 'like_count', 'dislike_count', 'repost_count',
330         'average_rating', 'comment_count', 'age_limit',
331         'start_time', 'end_time',
332         'chapter_number', 'season_number', 'episode_number',
333         'track_number', 'disc_number', 'release_year',
334         'playlist_index',
335     ))
336
337     params = None
338     _ies = []
339     _pps = []
340     _download_retcode = None
341     _num_downloads = None
342     _playlist_level = 0
343     _playlist_urls = set()
344     _screen_file = None
345
346     def __init__(self, params=None, auto_init=True):
347         """Create a FileDownloader object with the given options."""
348         if params is None:
349             params = {}
350         self._ies = []
351         self._ies_instances = {}
352         self._pps = []
353         self._progress_hooks = []
354         self._download_retcode = 0
355         self._num_downloads = 0
356         self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
357         self._err_file = sys.stderr
358         self.params = {
359             # Default parameters
360             'nocheckcertificate': False,
361         }
362         self.params.update(params)
363         self.cache = Cache(self)
364
365         def check_deprecated(param, option, suggestion):
366             if self.params.get(param) is not None:
367                 self.report_warning(
368                     '%s is deprecated. Use %s instead.' % (option, suggestion))
369                 return True
370             return False
371
372         if check_deprecated('cn_verification_proxy', '--cn-verification-proxy', '--geo-verification-proxy'):
373             if self.params.get('geo_verification_proxy') is None:
374                 self.params['geo_verification_proxy'] = self.params['cn_verification_proxy']
375
376         check_deprecated('autonumber_size', '--autonumber-size', 'output template with %(autonumber)0Nd, where N in the number of digits')
377         check_deprecated('autonumber', '--auto-number', '-o "%(autonumber)s-%(title)s.%(ext)s"')
378         check_deprecated('usetitle', '--title', '-o "%(title)s-%(id)s.%(ext)s"')
379
380         if params.get('bidi_workaround', False):
381             try:
382                 import pty
383                 master, slave = pty.openpty()
384                 width = compat_get_terminal_size().columns
385                 if width is None:
386                     width_args = []
387                 else:
388                     width_args = ['-w', str(width)]
389                 sp_kwargs = dict(
390                     stdin=subprocess.PIPE,
391                     stdout=slave,
392                     stderr=self._err_file)
393                 try:
394                     self._output_process = subprocess.Popen(
395                         ['bidiv'] + width_args, **sp_kwargs
396                     )
397                 except OSError:
398                     self._output_process = subprocess.Popen(
399                         ['fribidi', '-c', 'UTF-8'] + width_args, **sp_kwargs)
400                 self._output_channel = os.fdopen(master, 'rb')
401             except OSError as ose:
402                 if ose.errno == errno.ENOENT:
403                     self.report_warning('Could not find fribidi executable, ignoring --bidi-workaround . Make sure that  fribidi  is an executable file in one of the directories in your $PATH.')
404                 else:
405                     raise
406
407         if (sys.platform != 'win32'
408                 and sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968']
409                 and not params.get('restrictfilenames', False)):
410             # Unicode filesystem API will throw errors (#1474, #13027)
411             self.report_warning(
412                 'Assuming --restrict-filenames since file system encoding '
413                 'cannot encode all characters. '
414                 'Set the LC_ALL environment variable to fix this.')
415             self.params['restrictfilenames'] = True
416
417         if isinstance(params.get('outtmpl'), bytes):
418             self.report_warning(
419                 'Parameter outtmpl is bytes, but should be a unicode string. '
420                 'Put  from __future__ import unicode_literals  at the top of your code file or consider switching to Python 3.x.')
421
422         self._setup_opener()
423
424         if auto_init:
425             self.print_debug_header()
426             self.add_default_info_extractors()
427
428         for pp_def_raw in self.params.get('postprocessors', []):
429             pp_class = get_postprocessor(pp_def_raw['key'])
430             pp_def = dict(pp_def_raw)
431             del pp_def['key']
432             pp = pp_class(self, **compat_kwargs(pp_def))
433             self.add_post_processor(pp)
434
435         for ph in self.params.get('progress_hooks', []):
436             self.add_progress_hook(ph)
437
438         register_socks_protocols()
439
440     def warn_if_short_id(self, argv):
441         # short YouTube ID starting with dash?
442         idxs = [
443             i for i, a in enumerate(argv)
444             if re.match(r'^-[0-9A-Za-z_-]{10}$', a)]
445         if idxs:
446             correct_argv = (
447                 ['youtube-dl']
448                 + [a for i, a in enumerate(argv) if i not in idxs]
449                 + ['--'] + [argv[i] for i in idxs]
450             )
451             self.report_warning(
452                 'Long argument string detected. '
453                 'Use -- to separate parameters and URLs, like this:\n%s\n' %
454                 args_to_str(correct_argv))
455
456     def add_info_extractor(self, ie):
457         """Add an InfoExtractor object to the end of the list."""
458         self._ies.append(ie)
459         if not isinstance(ie, type):
460             self._ies_instances[ie.ie_key()] = ie
461             ie.set_downloader(self)
462
463     def get_info_extractor(self, ie_key):
464         """
465         Get an instance of an IE with name ie_key, it will try to get one from
466         the _ies list, if there's no instance it will create a new one and add
467         it to the extractor list.
468         """
469         ie = self._ies_instances.get(ie_key)
470         if ie is None:
471             ie = get_info_extractor(ie_key)()
472             self.add_info_extractor(ie)
473         return ie
474
475     def add_default_info_extractors(self):
476         """
477         Add the InfoExtractors returned by gen_extractors to the end of the list
478         """
479         for ie in gen_extractor_classes():
480             self.add_info_extractor(ie)
481
482     def add_post_processor(self, pp):
483         """Add a PostProcessor object to the end of the chain."""
484         self._pps.append(pp)
485         pp.set_downloader(self)
486
487     def add_progress_hook(self, ph):
488         """Add the progress hook (currently only for the file downloader)"""
489         self._progress_hooks.append(ph)
490
491     def _bidi_workaround(self, message):
492         if not hasattr(self, '_output_channel'):
493             return message
494
495         assert hasattr(self, '_output_process')
496         assert isinstance(message, compat_str)
497         line_count = message.count('\n') + 1
498         self._output_process.stdin.write((message + '\n').encode('utf-8'))
499         self._output_process.stdin.flush()
500         res = ''.join(self._output_channel.readline().decode('utf-8')
501                       for _ in range(line_count))
502         return res[:-len('\n')]
503
504     def to_screen(self, message, skip_eol=False):
505         """Print message to stdout if not in quiet mode."""
506         return self.to_stdout(message, skip_eol, check_quiet=True)
507
508     def _write_string(self, s, out=None):
509         write_string(s, out=out, encoding=self.params.get('encoding'))
510
511     def to_stdout(self, message, skip_eol=False, check_quiet=False):
512         """Print message to stdout if not in quiet mode."""
513         if self.params.get('logger'):
514             self.params['logger'].debug(message)
515         elif not check_quiet or not self.params.get('quiet', False):
516             message = self._bidi_workaround(message)
517             terminator = ['\n', ''][skip_eol]
518             output = message + terminator
519
520             self._write_string(output, self._screen_file)
521
522     def to_stderr(self, message):
523         """Print message to stderr."""
524         assert isinstance(message, compat_str)
525         if self.params.get('logger'):
526             self.params['logger'].error(message)
527         else:
528             message = self._bidi_workaround(message)
529             output = message + '\n'
530             self._write_string(output, self._err_file)
531
532     def to_console_title(self, message):
533         if not self.params.get('consoletitle', False):
534             return
535         if compat_os_name == 'nt':
536             if ctypes.windll.kernel32.GetConsoleWindow():
537                 # c_wchar_p() might not be necessary if `message` is
538                 # already of type unicode()
539                 ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
540         elif 'TERM' in os.environ:
541             self._write_string('\033]0;%s\007' % message, self._screen_file)
542
543     def save_console_title(self):
544         if not self.params.get('consoletitle', False):
545             return
546         if self.params.get('simulate', False):
547             return
548         if compat_os_name != 'nt' and 'TERM' in os.environ:
549             # Save the title on stack
550             self._write_string('\033[22;0t', self._screen_file)
551
552     def restore_console_title(self):
553         if not self.params.get('consoletitle', False):
554             return
555         if self.params.get('simulate', False):
556             return
557         if compat_os_name != 'nt' and 'TERM' in os.environ:
558             # Restore the title from stack
559             self._write_string('\033[23;0t', self._screen_file)
560
561     def __enter__(self):
562         self.save_console_title()
563         return self
564
565     def __exit__(self, *args):
566         self.restore_console_title()
567
568         if self.params.get('cookiefile') is not None:
569             self.cookiejar.save(ignore_discard=True, ignore_expires=True)
570
571     def trouble(self, message=None, tb=None):
572         """Determine action to take when a download problem appears.
573
574         Depending on if the downloader has been configured to ignore
575         download errors or not, this method may throw an exception or
576         not when errors are found, after printing the message.
577
578         tb, if given, is additional traceback information.
579         """
580         if message is not None:
581             self.to_stderr(message)
582         if self.params.get('verbose'):
583             if tb is None:
584                 if sys.exc_info()[0]:  # if .trouble has been called from an except block
585                     tb = ''
586                     if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
587                         tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
588                     tb += encode_compat_str(traceback.format_exc())
589                 else:
590                     tb_data = traceback.format_list(traceback.extract_stack())
591                     tb = ''.join(tb_data)
592             self.to_stderr(tb)
593         if not self.params.get('ignoreerrors', False):
594             if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
595                 exc_info = sys.exc_info()[1].exc_info
596             else:
597                 exc_info = sys.exc_info()
598             raise DownloadError(message, exc_info)
599         self._download_retcode = 1
600
601     def report_warning(self, message):
602         '''
603         Print the message to stderr, it will be prefixed with 'WARNING:'
604         If stderr is a tty file the 'WARNING:' will be colored
605         '''
606         if self.params.get('logger') is not None:
607             self.params['logger'].warning(message)
608         else:
609             if self.params.get('no_warnings'):
610                 return
611             if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
612                 _msg_header = '\033[0;33mWARNING:\033[0m'
613             else:
614                 _msg_header = 'WARNING:'
615             warning_message = '%s %s' % (_msg_header, message)
616             self.to_stderr(warning_message)
617
618     def report_error(self, message, tb=None):
619         '''
620         Do the same as trouble, but prefixes the message with 'ERROR:', colored
621         in red if stderr is a tty file.
622         '''
623         if not self.params.get('no_color') and self._err_file.isatty() and compat_os_name != 'nt':
624             _msg_header = '\033[0;31mERROR:\033[0m'
625         else:
626             _msg_header = 'ERROR:'
627         error_message = '%s %s' % (_msg_header, message)
628         self.trouble(error_message, tb)
629
630     def report_file_already_downloaded(self, file_name):
631         """Report file has already been fully downloaded."""
632         try:
633             self.to_screen('[download] %s has already been downloaded' % file_name)
634         except UnicodeEncodeError:
635             self.to_screen('[download] The file has already been downloaded')
636
637     def prepare_filename(self, info_dict):
638         """Generate the output filename."""
639         try:
640             template_dict = dict(info_dict)
641
642             template_dict['epoch'] = int(time.time())
643             autonumber_size = self.params.get('autonumber_size')
644             if autonumber_size is None:
645                 autonumber_size = 5
646             template_dict['autonumber'] = self.params.get('autonumber_start', 1) - 1 + self._num_downloads
647             if template_dict.get('resolution') is None:
648                 if template_dict.get('width') and template_dict.get('height'):
649                     template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
650                 elif template_dict.get('height'):
651                     template_dict['resolution'] = '%sp' % template_dict['height']
652                 elif template_dict.get('width'):
653                     template_dict['resolution'] = '%dx?' % template_dict['width']
654
655             sanitize = lambda k, v: sanitize_filename(
656                 compat_str(v),
657                 restricted=self.params.get('restrictfilenames'),
658                 is_id=(k == 'id' or k.endswith('_id')))
659             template_dict = dict((k, v if isinstance(v, compat_numeric_types) else sanitize(k, v))
660                                  for k, v in template_dict.items()
661                                  if v is not None and not isinstance(v, (list, tuple, dict)))
662             template_dict = collections.defaultdict(lambda: self.params.get('outtmpl_na_placeholder', 'NA'), template_dict)
663
664             outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
665
666             # For fields playlist_index and autonumber convert all occurrences
667             # of %(field)s to %(field)0Nd for backward compatibility
668             field_size_compat_map = {
669                 'playlist_index': len(str(template_dict['n_entries'])),
670                 'autonumber': autonumber_size,
671             }
672             FIELD_SIZE_COMPAT_RE = r'(?<!%)%\((?P<field>autonumber|playlist_index)\)s'
673             mobj = re.search(FIELD_SIZE_COMPAT_RE, outtmpl)
674             if mobj:
675                 outtmpl = re.sub(
676                     FIELD_SIZE_COMPAT_RE,
677                     r'%%(\1)0%dd' % field_size_compat_map[mobj.group('field')],
678                     outtmpl)
679
680             # Missing numeric fields used together with integer presentation types
681             # in format specification will break the argument substitution since
682             # string NA placeholder is returned for missing fields. We will patch
683             # output template for missing fields to meet string presentation type.
684             for numeric_field in self._NUMERIC_FIELDS:
685                 if numeric_field not in template_dict:
686                     # As of [1] format syntax is:
687                     #  %[mapping_key][conversion_flags][minimum_width][.precision][length_modifier]type
688                     # 1. https://docs.python.org/2/library/stdtypes.html#string-formatting
689                     FORMAT_RE = r'''(?x)
690                         (?<!%)
691                         %
692                         \({0}\)  # mapping key
693                         (?:[#0\-+ ]+)?  # conversion flags (optional)
694                         (?:\d+)?  # minimum field width (optional)
695                         (?:\.\d+)?  # precision (optional)
696                         [hlL]?  # length modifier (optional)
697                         [diouxXeEfFgGcrs%]  # conversion type
698                     '''
699                     outtmpl = re.sub(
700                         FORMAT_RE.format(numeric_field),
701                         r'%({0})s'.format(numeric_field), outtmpl)
702
703             # expand_path translates '%%' into '%' and '$$' into '$'
704             # correspondingly that is not what we want since we need to keep
705             # '%%' intact for template dict substitution step. Working around
706             # with boundary-alike separator hack.
707             sep = ''.join([random.choice(ascii_letters) for _ in range(32)])
708             outtmpl = outtmpl.replace('%%', '%{0}%'.format(sep)).replace('$$', '${0}$'.format(sep))
709
710             # outtmpl should be expand_path'ed before template dict substitution
711             # because meta fields may contain env variables we don't want to
712             # be expanded. For example, for outtmpl "%(title)s.%(ext)s" and
713             # title "Hello $PATH", we don't want `$PATH` to be expanded.
714             filename = expand_path(outtmpl).replace(sep, '') % template_dict
715
716             # Temporary fix for #4787
717             # 'Treat' all problem characters by passing filename through preferredencoding
718             # to workaround encoding issues with subprocess on python2 @ Windows
719             if sys.version_info < (3, 0) and sys.platform == 'win32':
720                 filename = encodeFilename(filename, True).decode(preferredencoding())
721             return sanitize_path(filename)
722         except ValueError as err:
723             self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
724             return None
725
726     def _match_entry(self, info_dict, incomplete):
727         """ Returns None iff the file should be downloaded """
728
729         video_title = info_dict.get('title', info_dict.get('id', 'video'))
730         if 'title' in info_dict:
731             # This can happen when we're just evaluating the playlist
732             title = info_dict['title']
733             matchtitle = self.params.get('matchtitle', False)
734             if matchtitle:
735                 if not re.search(matchtitle, title, re.IGNORECASE):
736                     return '"' + title + '" title did not match pattern "' + matchtitle + '"'
737             rejecttitle = self.params.get('rejecttitle', False)
738             if rejecttitle:
739                 if re.search(rejecttitle, title, re.IGNORECASE):
740                     return '"' + title + '" title matched reject pattern "' + rejecttitle + '"'
741         date = info_dict.get('upload_date')
742         if date is not None:
743             dateRange = self.params.get('daterange', DateRange())
744             if date not in dateRange:
745                 return '%s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
746         view_count = info_dict.get('view_count')
747         if view_count is not None:
748             min_views = self.params.get('min_views')
749             if min_views is not None and view_count < min_views:
750                 return 'Skipping %s, because it has not reached minimum view count (%d/%d)' % (video_title, view_count, min_views)
751             max_views = self.params.get('max_views')
752             if max_views is not None and view_count > max_views:
753                 return 'Skipping %s, because it has exceeded the maximum view count (%d/%d)' % (video_title, view_count, max_views)
754         if age_restricted(info_dict.get('age_limit'), self.params.get('age_limit')):
755             return 'Skipping "%s" because it is age restricted' % video_title
756         if self.in_download_archive(info_dict):
757             return '%s has already been recorded in archive' % video_title
758
759         if not incomplete:
760             match_filter = self.params.get('match_filter')
761             if match_filter is not None:
762                 ret = match_filter(info_dict)
763                 if ret is not None:
764                     return ret
765
766         return None
767
768     @staticmethod
769     def add_extra_info(info_dict, extra_info):
770         '''Set the keys from extra_info in info dict if they are missing'''
771         for key, value in extra_info.items():
772             info_dict.setdefault(key, value)
773
774     def extract_info(self, url, download=True, ie_key=None, extra_info={},
775                      process=True, force_generic_extractor=False):
776         """
777         Return a list with a dictionary for each video extracted.
778
779         Arguments:
780         url -- URL to extract
781
782         Keyword arguments:
783         download -- whether to download videos during extraction
784         ie_key -- extractor key hint
785         extra_info -- dictionary containing the extra values to add to each result
786         process -- whether to resolve all unresolved references (URLs, playlist items),
787             must be True for download to work.
788         force_generic_extractor -- force using the generic extractor
789         """
790
791         if not ie_key and force_generic_extractor:
792             ie_key = 'Generic'
793
794         if ie_key:
795             ies = [self.get_info_extractor(ie_key)]
796         else:
797             ies = self._ies
798
799         for ie in ies:
800             if not ie.suitable(url):
801                 continue
802
803             ie = self.get_info_extractor(ie.ie_key())
804             if not ie.working():
805                 self.report_warning('The program functionality for this site has been marked as broken, '
806                                     'and will probably not work.')
807
808             return self.__extract_info(url, ie, download, extra_info, process)
809         else:
810             self.report_error('no suitable InfoExtractor for URL %s' % url)
811
812     def __handle_extraction_exceptions(func):
813         def wrapper(self, *args, **kwargs):
814             try:
815                 return func(self, *args, **kwargs)
816             except GeoRestrictedError as e:
817                 msg = e.msg
818                 if e.countries:
819                     msg += '\nThis video is available in %s.' % ', '.join(
820                         map(ISO3166Utils.short2full, e.countries))
821                 msg += '\nYou might want to use a VPN or a proxy server (with --proxy) to workaround.'
822                 self.report_error(msg)
823             except ExtractorError as e:  # An error we somewhat expected
824                 self.report_error(compat_str(e), e.format_traceback())
825             except MaxDownloadsReached:
826                 raise
827             except Exception as e:
828                 if self.params.get('ignoreerrors', False):
829                     self.report_error(error_to_compat_str(e), tb=encode_compat_str(traceback.format_exc()))
830                 else:
831                     raise
832         return wrapper
833
834     @__handle_extraction_exceptions
835     def __extract_info(self, url, ie, download, extra_info, process):
836         ie_result = ie.extract(url)
837         if ie_result is None:  # Finished already (backwards compatibility; listformats and friends should be moved here)
838             return
839         if isinstance(ie_result, list):
840             # Backwards compatibility: old IE result format
841             ie_result = {
842                 '_type': 'compat_list',
843                 'entries': ie_result,
844             }
845         self.add_default_extra_info(ie_result, ie, url)
846         if process:
847             return self.process_ie_result(ie_result, download, extra_info)
848         else:
849             return ie_result
850
851     def add_default_extra_info(self, ie_result, ie, url):
852         self.add_extra_info(ie_result, {
853             'extractor': ie.IE_NAME,
854             'webpage_url': url,
855             'webpage_url_basename': url_basename(url),
856             'extractor_key': ie.ie_key(),
857         })
858
859     def process_ie_result(self, ie_result, download=True, extra_info={}):
860         """
861         Take the result of the ie(may be modified) and resolve all unresolved
862         references (URLs, playlist items).
863
864         It will also download the videos if 'download'.
865         Returns the resolved ie_result.
866         """
867         result_type = ie_result.get('_type', 'video')
868
869         if result_type in ('url', 'url_transparent'):
870             ie_result['url'] = sanitize_url(ie_result['url'])
871             extract_flat = self.params.get('extract_flat', False)
872             if ((extract_flat == 'in_playlist' and 'playlist' in extra_info)
873                     or extract_flat is True):
874                 self.__forced_printings(
875                     ie_result, self.prepare_filename(ie_result),
876                     incomplete=True)
877                 return ie_result
878
879         if result_type == 'video':
880             self.add_extra_info(ie_result, extra_info)
881             return self.process_video_result(ie_result, download=download)
882         elif result_type == 'url':
883             # We have to add extra_info to the results because it may be
884             # contained in a playlist
885             return self.extract_info(ie_result['url'],
886                                      download,
887                                      ie_key=ie_result.get('ie_key'),
888                                      extra_info=extra_info)
889         elif result_type == 'url_transparent':
890             # Use the information from the embedding page
891             info = self.extract_info(
892                 ie_result['url'], ie_key=ie_result.get('ie_key'),
893                 extra_info=extra_info, download=False, process=False)
894
895             # extract_info may return None when ignoreerrors is enabled and
896             # extraction failed with an error, don't crash and return early
897             # in this case
898             if not info:
899                 return info
900
901             force_properties = dict(
902                 (k, v) for k, v in ie_result.items() if v is not None)
903             for f in ('_type', 'url', 'id', 'extractor', 'extractor_key', 'ie_key'):
904                 if f in force_properties:
905                     del force_properties[f]
906             new_result = info.copy()
907             new_result.update(force_properties)
908
909             # Extracted info may not be a video result (i.e.
910             # info.get('_type', 'video') != video) but rather an url or
911             # url_transparent. In such cases outer metadata (from ie_result)
912             # should be propagated to inner one (info). For this to happen
913             # _type of info should be overridden with url_transparent. This
914             # fixes issue from https://github.com/ytdl-org/youtube-dl/pull/11163.
915             if new_result.get('_type') == 'url':
916                 new_result['_type'] = 'url_transparent'
917
918             return self.process_ie_result(
919                 new_result, download=download, extra_info=extra_info)
920         elif result_type in ('playlist', 'multi_video'):
921             # Protect from infinite recursion due to recursively nested playlists
922             # (see https://github.com/ytdl-org/youtube-dl/issues/27833)
923             webpage_url = ie_result['webpage_url']
924             if webpage_url in self._playlist_urls:
925                 self.to_screen(
926                     '[download] Skipping already downloaded playlist: %s'
927                     % ie_result.get('title') or ie_result.get('id'))
928                 return
929
930             self._playlist_level += 1
931             self._playlist_urls.add(webpage_url)
932             try:
933                 return self.__process_playlist(ie_result, download)
934             finally:
935                 self._playlist_level -= 1
936                 if not self._playlist_level:
937                     self._playlist_urls.clear()
938         elif result_type == 'compat_list':
939             self.report_warning(
940                 'Extractor %s returned a compat_list result. '
941                 'It needs to be updated.' % ie_result.get('extractor'))
942
943             def _fixup(r):
944                 self.add_extra_info(
945                     r,
946                     {
947                         'extractor': ie_result['extractor'],
948                         'webpage_url': ie_result['webpage_url'],
949                         'webpage_url_basename': url_basename(ie_result['webpage_url']),
950                         'extractor_key': ie_result['extractor_key'],
951                     }
952                 )
953                 return r
954             ie_result['entries'] = [
955                 self.process_ie_result(_fixup(r), download, extra_info)
956                 for r in ie_result['entries']
957             ]
958             return ie_result
959         else:
960             raise Exception('Invalid result type: %s' % result_type)
961
962     def __process_playlist(self, ie_result, download):
963         # We process each entry in the playlist
964         playlist = ie_result.get('title') or ie_result.get('id')
965
966         self.to_screen('[download] Downloading playlist: %s' % playlist)
967
968         playlist_results = []
969
970         playliststart = self.params.get('playliststart', 1) - 1
971         playlistend = self.params.get('playlistend')
972         # For backwards compatibility, interpret -1 as whole list
973         if playlistend == -1:
974             playlistend = None
975
976         playlistitems_str = self.params.get('playlist_items')
977         playlistitems = None
978         if playlistitems_str is not None:
979             def iter_playlistitems(format):
980                 for string_segment in format.split(','):
981                     if '-' in string_segment:
982                         start, end = string_segment.split('-')
983                         for item in range(int(start), int(end) + 1):
984                             yield int(item)
985                     else:
986                         yield int(string_segment)
987             playlistitems = orderedSet(iter_playlistitems(playlistitems_str))
988
989         ie_entries = ie_result['entries']
990
991         def make_playlistitems_entries(list_ie_entries):
992             num_entries = len(list_ie_entries)
993             return [
994                 list_ie_entries[i - 1] for i in playlistitems
995                 if -num_entries <= i - 1 < num_entries]
996
997         def report_download(num_entries):
998             self.to_screen(
999                 '[%s] playlist %s: Downloading %d videos' %
1000                 (ie_result['extractor'], playlist, num_entries))
1001
1002         if isinstance(ie_entries, list):
1003             n_all_entries = len(ie_entries)
1004             if playlistitems:
1005                 entries = make_playlistitems_entries(ie_entries)
1006             else:
1007                 entries = ie_entries[playliststart:playlistend]
1008             n_entries = len(entries)
1009             self.to_screen(
1010                 '[%s] playlist %s: Collected %d video ids (downloading %d of them)' %
1011                 (ie_result['extractor'], playlist, n_all_entries, n_entries))
1012         elif isinstance(ie_entries, PagedList):
1013             if playlistitems:
1014                 entries = []
1015                 for item in playlistitems:
1016                     entries.extend(ie_entries.getslice(
1017                         item - 1, item
1018                     ))
1019             else:
1020                 entries = ie_entries.getslice(
1021                     playliststart, playlistend)
1022             n_entries = len(entries)
1023             report_download(n_entries)
1024         else:  # iterable
1025             if playlistitems:
1026                 entries = make_playlistitems_entries(list(itertools.islice(
1027                     ie_entries, 0, max(playlistitems))))
1028             else:
1029                 entries = list(itertools.islice(
1030                     ie_entries, playliststart, playlistend))
1031             n_entries = len(entries)
1032             report_download(n_entries)
1033
1034         if self.params.get('playlistreverse', False):
1035             entries = entries[::-1]
1036
1037         if self.params.get('playlistrandom', False):
1038             random.shuffle(entries)
1039
1040         x_forwarded_for = ie_result.get('__x_forwarded_for_ip')
1041
1042         for i, entry in enumerate(entries, 1):
1043             self.to_screen('[download] Downloading video %s of %s' % (i, n_entries))
1044             # This __x_forwarded_for_ip thing is a bit ugly but requires
1045             # minimal changes
1046             if x_forwarded_for:
1047                 entry['__x_forwarded_for_ip'] = x_forwarded_for
1048             extra = {
1049                 'n_entries': n_entries,
1050                 'playlist': playlist,
1051                 'playlist_id': ie_result.get('id'),
1052                 'playlist_title': ie_result.get('title'),
1053                 'playlist_uploader': ie_result.get('uploader'),
1054                 'playlist_uploader_id': ie_result.get('uploader_id'),
1055                 'playlist_index': playlistitems[i - 1] if playlistitems else i + playliststart,
1056                 'extractor': ie_result['extractor'],
1057                 'webpage_url': ie_result['webpage_url'],
1058                 'webpage_url_basename': url_basename(ie_result['webpage_url']),
1059                 'extractor_key': ie_result['extractor_key'],
1060             }
1061
1062             reason = self._match_entry(entry, incomplete=True)
1063             if reason is not None:
1064                 self.to_screen('[download] ' + reason)
1065                 continue
1066
1067             entry_result = self.__process_iterable_entry(entry, download, extra)
1068             # TODO: skip failed (empty) entries?
1069             playlist_results.append(entry_result)
1070         ie_result['entries'] = playlist_results
1071         self.to_screen('[download] Finished downloading playlist: %s' % playlist)
1072         return ie_result
1073
1074     @__handle_extraction_exceptions
1075     def __process_iterable_entry(self, entry, download, extra_info):
1076         return self.process_ie_result(
1077             entry, download=download, extra_info=extra_info)
1078
1079     def _build_format_filter(self, filter_spec):
1080         " Returns a function to filter the formats according to the filter_spec "
1081
1082         OPERATORS = {
1083             '<': operator.lt,
1084             '<=': operator.le,
1085             '>': operator.gt,
1086             '>=': operator.ge,
1087             '=': operator.eq,
1088             '!=': operator.ne,
1089         }
1090         operator_rex = re.compile(r'''(?x)\s*
1091             (?P<key>width|height|tbr|abr|vbr|asr|filesize|filesize_approx|fps)
1092             \s*(?P<op>%s)(?P<none_inclusive>\s*\?)?\s*
1093             (?P<value>[0-9.]+(?:[kKmMgGtTpPeEzZyY]i?[Bb]?)?)
1094             $
1095             ''' % '|'.join(map(re.escape, OPERATORS.keys())))
1096         m = operator_rex.search(filter_spec)
1097         if m:
1098             try:
1099                 comparison_value = int(m.group('value'))
1100             except ValueError:
1101                 comparison_value = parse_filesize(m.group('value'))
1102                 if comparison_value is None:
1103                     comparison_value = parse_filesize(m.group('value') + 'B')
1104                 if comparison_value is None:
1105                     raise ValueError(
1106                         'Invalid value %r in format specification %r' % (
1107                             m.group('value'), filter_spec))
1108             op = OPERATORS[m.group('op')]
1109
1110         if not m:
1111             STR_OPERATORS = {
1112                 '=': operator.eq,
1113                 '^=': lambda attr, value: attr.startswith(value),
1114                 '$=': lambda attr, value: attr.endswith(value),
1115                 '*=': lambda attr, value: value in attr,
1116             }
1117             str_operator_rex = re.compile(r'''(?x)
1118                 \s*(?P<key>ext|acodec|vcodec|container|protocol|format_id|language)
1119                 \s*(?P<negation>!\s*)?(?P<op>%s)(?P<none_inclusive>\s*\?)?
1120                 \s*(?P<value>[a-zA-Z0-9._-]+)
1121                 \s*$
1122                 ''' % '|'.join(map(re.escape, STR_OPERATORS.keys())))
1123             m = str_operator_rex.search(filter_spec)
1124             if m:
1125                 comparison_value = m.group('value')
1126                 str_op = STR_OPERATORS[m.group('op')]
1127                 if m.group('negation'):
1128                     op = lambda attr, value: not str_op(attr, value)
1129                 else:
1130                     op = str_op
1131
1132         if not m:
1133             raise ValueError('Invalid filter specification %r' % filter_spec)
1134
1135         def _filter(f):
1136             actual_value = f.get(m.group('key'))
1137             if actual_value is None:
1138                 return m.group('none_inclusive')
1139             return op(actual_value, comparison_value)
1140         return _filter
1141
1142     def _default_format_spec(self, info_dict, download=True):
1143
1144         def can_merge():
1145             merger = FFmpegMergerPP(self)
1146             return merger.available and merger.can_merge()
1147
1148         def prefer_best():
1149             if self.params.get('simulate', False):
1150                 return False
1151             if not download:
1152                 return False
1153             if self.params.get('outtmpl', DEFAULT_OUTTMPL) == '-':
1154                 return True
1155             if info_dict.get('is_live'):
1156                 return True
1157             if not can_merge():
1158                 return True
1159             return False
1160
1161         req_format_list = ['bestvideo+bestaudio', 'best']
1162         if prefer_best():
1163             req_format_list.reverse()
1164         return '/'.join(req_format_list)
1165
1166     def build_format_selector(self, format_spec):
1167         def syntax_error(note, start):
1168             message = (
1169                 'Invalid format specification: '
1170                 '{0}\n\t{1}\n\t{2}^'.format(note, format_spec, ' ' * start[1]))
1171             return SyntaxError(message)
1172
1173         PICKFIRST = 'PICKFIRST'
1174         MERGE = 'MERGE'
1175         SINGLE = 'SINGLE'
1176         GROUP = 'GROUP'
1177         FormatSelector = collections.namedtuple('FormatSelector', ['type', 'selector', 'filters'])
1178
1179         def _parse_filter(tokens):
1180             filter_parts = []
1181             for type, string, start, _, _ in tokens:
1182                 if type == tokenize.OP and string == ']':
1183                     return ''.join(filter_parts)
1184                 else:
1185                     filter_parts.append(string)
1186
1187         def _remove_unused_ops(tokens):
1188             # Remove operators that we don't use and join them with the surrounding strings
1189             # for example: 'mp4' '-' 'baseline' '-' '16x9' is converted to 'mp4-baseline-16x9'
1190             ALLOWED_OPS = ('/', '+', ',', '(', ')')
1191             last_string, last_start, last_end, last_line = None, None, None, None
1192             for type, string, start, end, line in tokens:
1193                 if type == tokenize.OP and string == '[':
1194                     if last_string:
1195                         yield tokenize.NAME, last_string, last_start, last_end, last_line
1196                         last_string = None
1197                     yield type, string, start, end, line
1198                     # everything inside brackets will be handled by _parse_filter
1199                     for type, string, start, end, line in tokens:
1200                         yield type, string, start, end, line
1201                         if type == tokenize.OP and string == ']':
1202                             break
1203                 elif type == tokenize.OP and string in ALLOWED_OPS:
1204                     if last_string:
1205                         yield tokenize.NAME, last_string, last_start, last_end, last_line
1206                         last_string = None
1207                     yield type, string, start, end, line
1208                 elif type in [tokenize.NAME, tokenize.NUMBER, tokenize.OP]:
1209                     if not last_string:
1210                         last_string = string
1211                         last_start = start
1212                         last_end = end
1213                     else:
1214                         last_string += string
1215             if last_string:
1216                 yield tokenize.NAME, last_string, last_start, last_end, last_line
1217
1218         def _parse_format_selection(tokens, inside_merge=False, inside_choice=False, inside_group=False):
1219             selectors = []
1220             current_selector = None
1221             for type, string, start, _, _ in tokens:
1222                 # ENCODING is only defined in python 3.x
1223                 if type == getattr(tokenize, 'ENCODING', None):
1224                     continue
1225                 elif type in [tokenize.NAME, tokenize.NUMBER]:
1226                     current_selector = FormatSelector(SINGLE, string, [])
1227                 elif type == tokenize.OP:
1228                     if string == ')':
1229                         if not inside_group:
1230                             # ')' will be handled by the parentheses group
1231                             tokens.restore_last_token()
1232                         break
1233                     elif inside_merge and string in ['/', ',']:
1234                         tokens.restore_last_token()
1235                         break
1236                     elif inside_choice and string == ',':
1237                         tokens.restore_last_token()
1238                         break
1239                     elif string == ',':
1240                         if not current_selector:
1241                             raise syntax_error('"," must follow a format selector', start)
1242                         selectors.append(current_selector)
1243                         current_selector = None
1244                     elif string == '/':
1245                         if not current_selector:
1246                             raise syntax_error('"/" must follow a format selector', start)
1247                         first_choice = current_selector
1248                         second_choice = _parse_format_selection(tokens, inside_choice=True)
1249                         current_selector = FormatSelector(PICKFIRST, (first_choice, second_choice), [])
1250                     elif string == '[':
1251                         if not current_selector:
1252                             current_selector = FormatSelector(SINGLE, 'best', [])
1253                         format_filter = _parse_filter(tokens)
1254                         current_selector.filters.append(format_filter)
1255                     elif string == '(':
1256                         if current_selector:
1257                             raise syntax_error('Unexpected "("', start)
1258                         group = _parse_format_selection(tokens, inside_group=True)
1259                         current_selector = FormatSelector(GROUP, group, [])
1260                     elif string == '+':
1261                         if inside_merge:
1262                             raise syntax_error('Unexpected "+"', start)
1263                         video_selector = current_selector
1264                         audio_selector = _parse_format_selection(tokens, inside_merge=True)
1265                         if not video_selector or not audio_selector:
1266                             raise syntax_error('"+" must be between two format selectors', start)
1267                         current_selector = FormatSelector(MERGE, (video_selector, audio_selector), [])
1268                     else:
1269                         raise syntax_error('Operator not recognized: "{0}"'.format(string), start)
1270                 elif type == tokenize.ENDMARKER:
1271                     break
1272             if current_selector:
1273                 selectors.append(current_selector)
1274             return selectors
1275
1276         def _build_selector_function(selector):
1277             if isinstance(selector, list):
1278                 fs = [_build_selector_function(s) for s in selector]
1279
1280                 def selector_function(ctx):
1281                     for f in fs:
1282                         for format in f(ctx):
1283                             yield format
1284                 return selector_function
1285             elif selector.type == GROUP:
1286                 selector_function = _build_selector_function(selector.selector)
1287             elif selector.type == PICKFIRST:
1288                 fs = [_build_selector_function(s) for s in selector.selector]
1289
1290                 def selector_function(ctx):
1291                     for f in fs:
1292                         picked_formats = list(f(ctx))
1293                         if picked_formats:
1294                             return picked_formats
1295                     return []
1296             elif selector.type == SINGLE:
1297                 format_spec = selector.selector
1298
1299                 def selector_function(ctx):
1300                     formats = list(ctx['formats'])
1301                     if not formats:
1302                         return
1303                     if format_spec == 'all':
1304                         for f in formats:
1305                             yield f
1306                     elif format_spec in ['best', 'worst', None]:
1307                         format_idx = 0 if format_spec == 'worst' else -1
1308                         audiovideo_formats = [
1309                             f for f in formats
1310                             if f.get('vcodec') != 'none' and f.get('acodec') != 'none']
1311                         if audiovideo_formats:
1312                             yield audiovideo_formats[format_idx]
1313                         # for extractors with incomplete formats (audio only (soundcloud)
1314                         # or video only (imgur)) we will fallback to best/worst
1315                         # {video,audio}-only format
1316                         elif ctx['incomplete_formats']:
1317                             yield formats[format_idx]
1318                     elif format_spec == 'bestaudio':
1319                         audio_formats = [
1320                             f for f in formats
1321                             if f.get('vcodec') == 'none']
1322                         if audio_formats:
1323                             yield audio_formats[-1]
1324                     elif format_spec == 'worstaudio':
1325                         audio_formats = [
1326                             f for f in formats
1327                             if f.get('vcodec') == 'none']
1328                         if audio_formats:
1329                             yield audio_formats[0]
1330                     elif format_spec == 'bestvideo':
1331                         video_formats = [
1332                             f for f in formats
1333                             if f.get('acodec') == 'none']
1334                         if video_formats:
1335                             yield video_formats[-1]
1336                     elif format_spec == 'worstvideo':
1337                         video_formats = [
1338                             f for f in formats
1339                             if f.get('acodec') == 'none']
1340                         if video_formats:
1341                             yield video_formats[0]
1342                     else:
1343                         extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
1344                         if format_spec in extensions:
1345                             filter_f = lambda f: f['ext'] == format_spec
1346                         else:
1347                             filter_f = lambda f: f['format_id'] == format_spec
1348                         matches = list(filter(filter_f, formats))
1349                         if matches:
1350                             yield matches[-1]
1351             elif selector.type == MERGE:
1352                 def _merge(formats_info):
1353                     format_1, format_2 = [f['format_id'] for f in formats_info]
1354                     # The first format must contain the video and the
1355                     # second the audio
1356                     if formats_info[0].get('vcodec') == 'none':
1357                         self.report_error('The first format must '
1358                                           'contain the video, try using '
1359                                           '"-f %s+%s"' % (format_2, format_1))
1360                         return
1361                     # Formats must be opposite (video+audio)
1362                     if formats_info[0].get('acodec') == 'none' and formats_info[1].get('acodec') == 'none':
1363                         self.report_error(
1364                             'Both formats %s and %s are video-only, you must specify "-f video+audio"'
1365                             % (format_1, format_2))
1366                         return
1367                     output_ext = (
1368                         formats_info[0]['ext']
1369                         if self.params.get('merge_output_format') is None
1370                         else self.params['merge_output_format'])
1371                     return {
1372                         'requested_formats': formats_info,
1373                         'format': '%s+%s' % (formats_info[0].get('format'),
1374                                              formats_info[1].get('format')),
1375                         'format_id': '%s+%s' % (formats_info[0].get('format_id'),
1376                                                 formats_info[1].get('format_id')),
1377                         'width': formats_info[0].get('width'),
1378                         'height': formats_info[0].get('height'),
1379                         'resolution': formats_info[0].get('resolution'),
1380                         'fps': formats_info[0].get('fps'),
1381                         'vcodec': formats_info[0].get('vcodec'),
1382                         'vbr': formats_info[0].get('vbr'),
1383                         'stretched_ratio': formats_info[0].get('stretched_ratio'),
1384                         'acodec': formats_info[1].get('acodec'),
1385                         'abr': formats_info[1].get('abr'),
1386                         'ext': output_ext,
1387                     }
1388                 video_selector, audio_selector = map(_build_selector_function, selector.selector)
1389
1390                 def selector_function(ctx):
1391                     for pair in itertools.product(
1392                             video_selector(copy.deepcopy(ctx)), audio_selector(copy.deepcopy(ctx))):
1393                         yield _merge(pair)
1394
1395             filters = [self._build_format_filter(f) for f in selector.filters]
1396
1397             def final_selector(ctx):
1398                 ctx_copy = copy.deepcopy(ctx)
1399                 for _filter in filters:
1400                     ctx_copy['formats'] = list(filter(_filter, ctx_copy['formats']))
1401                 return selector_function(ctx_copy)
1402             return final_selector
1403
1404         stream = io.BytesIO(format_spec.encode('utf-8'))
1405         try:
1406             tokens = list(_remove_unused_ops(compat_tokenize_tokenize(stream.readline)))
1407         except tokenize.TokenError:
1408             raise syntax_error('Missing closing/opening brackets or parenthesis', (0, len(format_spec)))
1409
1410         class TokenIterator(object):
1411             def __init__(self, tokens):
1412                 self.tokens = tokens
1413                 self.counter = 0
1414
1415             def __iter__(self):
1416                 return self
1417
1418             def __next__(self):
1419                 if self.counter >= len(self.tokens):
1420                     raise StopIteration()
1421                 value = self.tokens[self.counter]
1422                 self.counter += 1
1423                 return value
1424
1425             next = __next__
1426
1427             def restore_last_token(self):
1428                 self.counter -= 1
1429
1430         parsed_selector = _parse_format_selection(iter(TokenIterator(tokens)))
1431         return _build_selector_function(parsed_selector)
1432
1433     def _calc_headers(self, info_dict):
1434         res = std_headers.copy()
1435
1436         add_headers = info_dict.get('http_headers')
1437         if add_headers:
1438             res.update(add_headers)
1439
1440         cookies = self._calc_cookies(info_dict)
1441         if cookies:
1442             res['Cookie'] = cookies
1443
1444         if 'X-Forwarded-For' not in res:
1445             x_forwarded_for_ip = info_dict.get('__x_forwarded_for_ip')
1446             if x_forwarded_for_ip:
1447                 res['X-Forwarded-For'] = x_forwarded_for_ip
1448
1449         return res
1450
1451     def _calc_cookies(self, info_dict):
1452         pr = sanitized_Request(info_dict['url'])
1453         self.cookiejar.add_cookie_header(pr)
1454         return pr.get_header('Cookie')
1455
1456     def process_video_result(self, info_dict, download=True):
1457         assert info_dict.get('_type', 'video') == 'video'
1458
1459         if 'id' not in info_dict:
1460             raise ExtractorError('Missing "id" field in extractor result')
1461         if 'title' not in info_dict:
1462             raise ExtractorError('Missing "title" field in extractor result')
1463
1464         def report_force_conversion(field, field_not, conversion):
1465             self.report_warning(
1466                 '"%s" field is not %s - forcing %s conversion, there is an error in extractor'
1467                 % (field, field_not, conversion))
1468
1469         def sanitize_string_field(info, string_field):
1470             field = info.get(string_field)
1471             if field is None or isinstance(field, compat_str):
1472                 return
1473             report_force_conversion(string_field, 'a string', 'string')
1474             info[string_field] = compat_str(field)
1475
1476         def sanitize_numeric_fields(info):
1477             for numeric_field in self._NUMERIC_FIELDS:
1478                 field = info.get(numeric_field)
1479                 if field is None or isinstance(field, compat_numeric_types):
1480                     continue
1481                 report_force_conversion(numeric_field, 'numeric', 'int')
1482                 info[numeric_field] = int_or_none(field)
1483
1484         sanitize_string_field(info_dict, 'id')
1485         sanitize_numeric_fields(info_dict)
1486
1487         if 'playlist' not in info_dict:
1488             # It isn't part of a playlist
1489             info_dict['playlist'] = None
1490             info_dict['playlist_index'] = None
1491
1492         thumbnails = info_dict.get('thumbnails')
1493         if thumbnails is None:
1494             thumbnail = info_dict.get('thumbnail')
1495             if thumbnail:
1496                 info_dict['thumbnails'] = thumbnails = [{'url': thumbnail}]
1497         if thumbnails:
1498             thumbnails.sort(key=lambda t: (
1499                 t.get('preference') if t.get('preference') is not None else -1,
1500                 t.get('width') if t.get('width') is not None else -1,
1501                 t.get('height') if t.get('height') is not None else -1,
1502                 t.get('id') if t.get('id') is not None else '', t.get('url')))
1503             for i, t in enumerate(thumbnails):
1504                 t['url'] = sanitize_url(t['url'])
1505                 if t.get('width') and t.get('height'):
1506                     t['resolution'] = '%dx%d' % (t['width'], t['height'])
1507                 if t.get('id') is None:
1508                     t['id'] = '%d' % i
1509
1510         if self.params.get('list_thumbnails'):
1511             self.list_thumbnails(info_dict)
1512             return
1513
1514         thumbnail = info_dict.get('thumbnail')
1515         if thumbnail:
1516             info_dict['thumbnail'] = sanitize_url(thumbnail)
1517         elif thumbnails:
1518             info_dict['thumbnail'] = thumbnails[-1]['url']
1519
1520         if 'display_id' not in info_dict and 'id' in info_dict:
1521             info_dict['display_id'] = info_dict['id']
1522
1523         for ts_key, date_key in (
1524                 ('timestamp', 'upload_date'),
1525                 ('release_timestamp', 'release_date'),
1526         ):
1527             if info_dict.get(date_key) is None and info_dict.get(ts_key) is not None:
1528                 # Working around out-of-range timestamp values (e.g. negative ones on Windows,
1529                 # see http://bugs.python.org/issue1646728)
1530                 try:
1531                     upload_date = datetime.datetime.utcfromtimestamp(info_dict[ts_key])
1532                     info_dict[date_key] = compat_str(upload_date.strftime('%Y%m%d'))
1533                 except (ValueError, OverflowError, OSError):
1534                     pass
1535
1536         # Auto generate title fields corresponding to the *_number fields when missing
1537         # in order to always have clean titles. This is very common for TV series.
1538         for field in ('chapter', 'season', 'episode'):
1539             if info_dict.get('%s_number' % field) is not None and not info_dict.get(field):
1540                 info_dict[field] = '%s %d' % (field.capitalize(), info_dict['%s_number' % field])
1541
1542         for cc_kind in ('subtitles', 'automatic_captions'):
1543             cc = info_dict.get(cc_kind)
1544             if cc:
1545                 for _, subtitle in cc.items():
1546                     for subtitle_format in subtitle:
1547                         if subtitle_format.get('url'):
1548                             subtitle_format['url'] = sanitize_url(subtitle_format['url'])
1549                         if subtitle_format.get('ext') is None:
1550                             subtitle_format['ext'] = determine_ext(subtitle_format['url']).lower()
1551
1552         automatic_captions = info_dict.get('automatic_captions')
1553         subtitles = info_dict.get('subtitles')
1554
1555         if self.params.get('listsubtitles', False):
1556             if 'automatic_captions' in info_dict:
1557                 self.list_subtitles(
1558                     info_dict['id'], automatic_captions, 'automatic captions')
1559             self.list_subtitles(info_dict['id'], subtitles, 'subtitles')
1560             return
1561
1562         info_dict['requested_subtitles'] = self.process_subtitles(
1563             info_dict['id'], subtitles, automatic_captions)
1564
1565         # We now pick which formats have to be downloaded
1566         if info_dict.get('formats') is None:
1567             # There's only one format available
1568             formats = [info_dict]
1569         else:
1570             formats = info_dict['formats']
1571
1572         if not formats:
1573             raise ExtractorError('No video formats found!')
1574
1575         def is_wellformed(f):
1576             url = f.get('url')
1577             if not url:
1578                 self.report_warning(
1579                     '"url" field is missing or empty - skipping format, '
1580                     'there is an error in extractor')
1581                 return False
1582             if isinstance(url, bytes):
1583                 sanitize_string_field(f, 'url')
1584             return True
1585
1586         # Filter out malformed formats for better extraction robustness
1587         formats = list(filter(is_wellformed, formats))
1588
1589         formats_dict = {}
1590
1591         # We check that all the formats have the format and format_id fields
1592         for i, format in enumerate(formats):
1593             sanitize_string_field(format, 'format_id')
1594             sanitize_numeric_fields(format)
1595             format['url'] = sanitize_url(format['url'])
1596             if not format.get('format_id'):
1597                 format['format_id'] = compat_str(i)
1598             else:
1599                 # Sanitize format_id from characters used in format selector expression
1600                 format['format_id'] = re.sub(r'[\s,/+\[\]()]', '_', format['format_id'])
1601             format_id = format['format_id']
1602             if format_id not in formats_dict:
1603                 formats_dict[format_id] = []
1604             formats_dict[format_id].append(format)
1605
1606         # Make sure all formats have unique format_id
1607         for format_id, ambiguous_formats in formats_dict.items():
1608             if len(ambiguous_formats) > 1:
1609                 for i, format in enumerate(ambiguous_formats):
1610                     format['format_id'] = '%s-%d' % (format_id, i)
1611
1612         for i, format in enumerate(formats):
1613             if format.get('format') is None:
1614                 format['format'] = '{id} - {res}{note}'.format(
1615                     id=format['format_id'],
1616                     res=self.format_resolution(format),
1617                     note=' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
1618                 )
1619             # Automatically determine file extension if missing
1620             if format.get('ext') is None:
1621                 format['ext'] = determine_ext(format['url']).lower()
1622             # Automatically determine protocol if missing (useful for format
1623             # selection purposes)
1624             if format.get('protocol') is None:
1625                 format['protocol'] = determine_protocol(format)
1626             # Add HTTP headers, so that external programs can use them from the
1627             # json output
1628             full_format_info = info_dict.copy()
1629             full_format_info.update(format)
1630             format['http_headers'] = self._calc_headers(full_format_info)
1631         # Remove private housekeeping stuff
1632         if '__x_forwarded_for_ip' in info_dict:
1633             del info_dict['__x_forwarded_for_ip']
1634
1635         # TODO Central sorting goes here
1636
1637         if formats[0] is not info_dict:
1638             # only set the 'formats' fields if the original info_dict list them
1639             # otherwise we end up with a circular reference, the first (and unique)
1640             # element in the 'formats' field in info_dict is info_dict itself,
1641             # which can't be exported to json
1642             info_dict['formats'] = formats
1643         if self.params.get('listformats'):
1644             self.list_formats(info_dict)
1645             return
1646
1647         req_format = self.params.get('format')
1648         if req_format is None:
1649             req_format = self._default_format_spec(info_dict, download=download)
1650             if self.params.get('verbose'):
1651                 self._write_string('[debug] Default format spec: %s\n' % req_format)
1652
1653         format_selector = self.build_format_selector(req_format)
1654
1655         # While in format selection we may need to have an access to the original
1656         # format set in order to calculate some metrics or do some processing.
1657         # For now we need to be able to guess whether original formats provided
1658         # by extractor are incomplete or not (i.e. whether extractor provides only
1659         # video-only or audio-only formats) for proper formats selection for
1660         # extractors with such incomplete formats (see
1661         # https://github.com/ytdl-org/youtube-dl/pull/5556).
1662         # Since formats may be filtered during format selection and may not match
1663         # the original formats the results may be incorrect. Thus original formats
1664         # or pre-calculated metrics should be passed to format selection routines
1665         # as well.
1666         # We will pass a context object containing all necessary additional data
1667         # instead of just formats.
1668         # This fixes incorrect format selection issue (see
1669         # https://github.com/ytdl-org/youtube-dl/issues/10083).
1670         incomplete_formats = (
1671             # All formats are video-only or
1672             all(f.get('vcodec') != 'none' and f.get('acodec') == 'none' for f in formats)
1673             # all formats are audio-only
1674             or all(f.get('vcodec') == 'none' and f.get('acodec') != 'none' for f in formats))
1675
1676         ctx = {
1677             'formats': formats,
1678             'incomplete_formats': incomplete_formats,
1679         }
1680
1681         formats_to_download = list(format_selector(ctx))
1682         if not formats_to_download:
1683             raise ExtractorError('requested format not available',
1684                                  expected=True)
1685
1686         if download:
1687             if len(formats_to_download) > 1:
1688                 self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
1689             for format in formats_to_download:
1690                 new_info = dict(info_dict)
1691                 new_info.update(format)
1692                 self.process_info(new_info)
1693         # We update the info dict with the best quality format (backwards compatibility)
1694         info_dict.update(formats_to_download[-1])
1695         return info_dict
1696
1697     def process_subtitles(self, video_id, normal_subtitles, automatic_captions):
1698         """Select the requested subtitles and their format"""
1699         available_subs = {}
1700         if normal_subtitles and self.params.get('writesubtitles'):
1701             available_subs.update(normal_subtitles)
1702         if automatic_captions and self.params.get('writeautomaticsub'):
1703             for lang, cap_info in automatic_captions.items():
1704                 if lang not in available_subs:
1705                     available_subs[lang] = cap_info
1706
1707         if (not self.params.get('writesubtitles') and not
1708                 self.params.get('writeautomaticsub') or not
1709                 available_subs):
1710             return None
1711
1712         if self.params.get('allsubtitles', False):
1713             requested_langs = available_subs.keys()
1714         else:
1715             if self.params.get('subtitleslangs', False):
1716                 requested_langs = self.params.get('subtitleslangs')
1717             elif 'en' in available_subs:
1718                 requested_langs = ['en']
1719             else:
1720                 requested_langs = [list(available_subs.keys())[0]]
1721
1722         formats_query = self.params.get('subtitlesformat', 'best')
1723         formats_preference = formats_query.split('/') if formats_query else []
1724         subs = {}
1725         for lang in requested_langs:
1726             formats = available_subs.get(lang)
1727             if formats is None:
1728                 self.report_warning('%s subtitles not available for %s' % (lang, video_id))
1729                 continue
1730             for ext in formats_preference:
1731                 if ext == 'best':
1732                     f = formats[-1]
1733                     break
1734                 matches = list(filter(lambda f: f['ext'] == ext, formats))
1735                 if matches:
1736                     f = matches[-1]
1737                     break
1738             else:
1739                 f = formats[-1]
1740                 self.report_warning(
1741                     'No subtitle format found matching "%s" for language %s, '
1742                     'using %s' % (formats_query, lang, f['ext']))
1743             subs[lang] = f
1744         return subs
1745
1746     def __forced_printings(self, info_dict, filename, incomplete):
1747         def print_mandatory(field):
1748             if (self.params.get('force%s' % field, False)
1749                     and (not incomplete or info_dict.get(field) is not None)):
1750                 self.to_stdout(info_dict[field])
1751
1752         def print_optional(field):
1753             if (self.params.get('force%s' % field, False)
1754                     and info_dict.get(field) is not None):
1755                 self.to_stdout(info_dict[field])
1756
1757         print_mandatory('title')
1758         print_mandatory('id')
1759         if self.params.get('forceurl', False) and not incomplete:
1760             if info_dict.get('requested_formats') is not None:
1761                 for f in info_dict['requested_formats']:
1762                     self.to_stdout(f['url'] + f.get('play_path', ''))
1763             else:
1764                 # For RTMP URLs, also include the playpath
1765                 self.to_stdout(info_dict['url'] + info_dict.get('play_path', ''))
1766         print_optional('thumbnail')
1767         print_optional('description')
1768         if self.params.get('forcefilename', False) and filename is not None:
1769             self.to_stdout(filename)
1770         if self.params.get('forceduration', False) and info_dict.get('duration') is not None:
1771             self.to_stdout(formatSeconds(info_dict['duration']))
1772         print_mandatory('format')
1773         if self.params.get('forcejson', False):
1774             self.to_stdout(json.dumps(info_dict))
1775
1776     def process_info(self, info_dict):
1777         """Process a single resolved IE result."""
1778
1779         assert info_dict.get('_type', 'video') == 'video'
1780
1781         max_downloads = self.params.get('max_downloads')
1782         if max_downloads is not None:
1783             if self._num_downloads >= int(max_downloads):
1784                 raise MaxDownloadsReached()
1785
1786         # TODO: backward compatibility, to be removed
1787         info_dict['fulltitle'] = info_dict['title']
1788
1789         if 'format' not in info_dict:
1790             info_dict['format'] = info_dict['ext']
1791
1792         reason = self._match_entry(info_dict, incomplete=False)
1793         if reason is not None:
1794             self.to_screen('[download] ' + reason)
1795             return
1796
1797         self._num_downloads += 1
1798
1799         info_dict['_filename'] = filename = self.prepare_filename(info_dict)
1800
1801         # Forced printings
1802         self.__forced_printings(info_dict, filename, incomplete=False)
1803
1804         # Do nothing else if in simulate mode
1805         if self.params.get('simulate', False):
1806             return
1807
1808         if filename is None:
1809             return
1810
1811         def ensure_dir_exists(path):
1812             try:
1813                 dn = os.path.dirname(path)
1814                 if dn and not os.path.exists(dn):
1815                     os.makedirs(dn)
1816                 return True
1817             except (OSError, IOError) as err:
1818                 if isinstance(err, OSError) and err.errno == errno.EEXIST:
1819                     return True
1820                 self.report_error('unable to create directory ' + error_to_compat_str(err))
1821                 return False
1822
1823         if not ensure_dir_exists(sanitize_path(encodeFilename(filename))):
1824             return
1825
1826         if self.params.get('writedescription', False):
1827             descfn = replace_extension(filename, 'description', info_dict.get('ext'))
1828             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(descfn)):
1829                 self.to_screen('[info] Video description is already present')
1830             elif info_dict.get('description') is None:
1831                 self.report_warning('There\'s no description to write.')
1832             else:
1833                 try:
1834                     self.to_screen('[info] Writing video description to: ' + descfn)
1835                     with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
1836                         descfile.write(info_dict['description'])
1837                 except (OSError, IOError):
1838                     self.report_error('Cannot write description file ' + descfn)
1839                     return
1840
1841         if self.params.get('writeannotations', False):
1842             annofn = replace_extension(filename, 'annotations.xml', info_dict.get('ext'))
1843             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(annofn)):
1844                 self.to_screen('[info] Video annotations are already present')
1845             elif not info_dict.get('annotations'):
1846                 self.report_warning('There are no annotations to write.')
1847             else:
1848                 try:
1849                     self.to_screen('[info] Writing video annotations to: ' + annofn)
1850                     with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
1851                         annofile.write(info_dict['annotations'])
1852                 except (KeyError, TypeError):
1853                     self.report_warning('There are no annotations to write.')
1854                 except (OSError, IOError):
1855                     self.report_error('Cannot write annotations file: ' + annofn)
1856                     return
1857
1858         subtitles_are_requested = any([self.params.get('writesubtitles', False),
1859                                        self.params.get('writeautomaticsub')])
1860
1861         if subtitles_are_requested and info_dict.get('requested_subtitles'):
1862             # subtitles download errors are already managed as troubles in relevant IE
1863             # that way it will silently go on when used with unsupporting IE
1864             subtitles = info_dict['requested_subtitles']
1865             ie = self.get_info_extractor(info_dict['extractor_key'])
1866             for sub_lang, sub_info in subtitles.items():
1867                 sub_format = sub_info['ext']
1868                 sub_filename = subtitles_filename(filename, sub_lang, sub_format, info_dict.get('ext'))
1869                 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(sub_filename)):
1870                     self.to_screen('[info] Video subtitle %s.%s is already present' % (sub_lang, sub_format))
1871                 else:
1872                     self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
1873                     if sub_info.get('data') is not None:
1874                         try:
1875                             # Use newline='' to prevent conversion of newline characters
1876                             # See https://github.com/ytdl-org/youtube-dl/issues/10268
1877                             with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8', newline='') as subfile:
1878                                 subfile.write(sub_info['data'])
1879                         except (OSError, IOError):
1880                             self.report_error('Cannot write subtitles file ' + sub_filename)
1881                             return
1882                     else:
1883                         try:
1884                             sub_data = ie._request_webpage(
1885                                 sub_info['url'], info_dict['id'], note=False).read()
1886                             with io.open(encodeFilename(sub_filename), 'wb') as subfile:
1887                                 subfile.write(sub_data)
1888                         except (ExtractorError, IOError, OSError, ValueError) as err:
1889                             self.report_warning('Unable to download subtitle for "%s": %s' %
1890                                                 (sub_lang, error_to_compat_str(err)))
1891                             continue
1892
1893         if self.params.get('writeinfojson', False):
1894             infofn = replace_extension(filename, 'info.json', info_dict.get('ext'))
1895             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(infofn)):
1896                 self.to_screen('[info] Video description metadata is already present')
1897             else:
1898                 self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
1899                 try:
1900                     write_json_file(self.filter_requested_info(info_dict), infofn)
1901                 except (OSError, IOError):
1902                     self.report_error('Cannot write metadata to JSON file ' + infofn)
1903                     return
1904
1905         self._write_thumbnails(info_dict, filename)
1906
1907         if not self.params.get('skip_download', False):
1908             try:
1909                 def checked_get_suitable_downloader(info_dict, params):
1910                     ed_args = params.get('external_downloader_args')
1911                     dler = get_suitable_downloader(info_dict, params)
1912                     if ed_args and not params.get('external_downloader_args'):
1913                         # external_downloader_args was cleared because external_downloader was rejected
1914                         self.report_warning('Requested external downloader cannot be used: '
1915                                             'ignoring --external-downloader-args.')
1916                     return dler
1917
1918                 def dl(name, info):
1919                     fd = checked_get_suitable_downloader(info, self.params)(self, self.params)
1920                     for ph in self._progress_hooks:
1921                         fd.add_progress_hook(ph)
1922                     if self.params.get('verbose'):
1923                         self.to_screen('[debug] Invoking downloader on %r' % info.get('url'))
1924                     return fd.download(name, info)
1925
1926                 if info_dict.get('requested_formats') is not None:
1927                     downloaded = []
1928                     success = True
1929                     merger = FFmpegMergerPP(self)
1930                     if not merger.available:
1931                         postprocessors = []
1932                         self.report_warning('You have requested multiple '
1933                                             'formats but ffmpeg or avconv are not installed.'
1934                                             ' The formats won\'t be merged.')
1935                     else:
1936                         postprocessors = [merger]
1937
1938                     def compatible_formats(formats):
1939                         video, audio = formats
1940                         # Check extension
1941                         video_ext, audio_ext = video.get('ext'), audio.get('ext')
1942                         if video_ext and audio_ext:
1943                             COMPATIBLE_EXTS = (
1944                                 ('mp3', 'mp4', 'm4a', 'm4p', 'm4b', 'm4r', 'm4v', 'ismv', 'isma'),
1945                                 ('webm')
1946                             )
1947                             for exts in COMPATIBLE_EXTS:
1948                                 if video_ext in exts and audio_ext in exts:
1949                                     return True
1950                         # TODO: Check acodec/vcodec
1951                         return False
1952
1953                     filename_real_ext = os.path.splitext(filename)[1][1:]
1954                     filename_wo_ext = (
1955                         os.path.splitext(filename)[0]
1956                         if filename_real_ext == info_dict['ext']
1957                         else filename)
1958                     requested_formats = info_dict['requested_formats']
1959                     if self.params.get('merge_output_format') is None and not compatible_formats(requested_formats):
1960                         info_dict['ext'] = 'mkv'
1961                         self.report_warning(
1962                             'Requested formats are incompatible for merge and will be merged into mkv.')
1963                     # Ensure filename always has a correct extension for successful merge
1964                     filename = '%s.%s' % (filename_wo_ext, info_dict['ext'])
1965                     if os.path.exists(encodeFilename(filename)):
1966                         self.to_screen(
1967                             '[download] %s has already been downloaded and '
1968                             'merged' % filename)
1969                     else:
1970                         for f in requested_formats:
1971                             new_info = dict(info_dict)
1972                             new_info.update(f)
1973                             fname = prepend_extension(
1974                                 self.prepare_filename(new_info),
1975                                 'f%s' % f['format_id'], new_info['ext'])
1976                             if not ensure_dir_exists(fname):
1977                                 return
1978                             downloaded.append(fname)
1979                             partial_success = dl(fname, new_info)
1980                             success = success and partial_success
1981                         info_dict['__postprocessors'] = postprocessors
1982                         info_dict['__files_to_merge'] = downloaded
1983                 else:
1984                     # Just a single file
1985                     success = dl(filename, info_dict)
1986             except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
1987                 self.report_error('unable to download video data: %s' % error_to_compat_str(err))
1988                 return
1989             except (OSError, IOError) as err:
1990                 raise UnavailableVideoError(err)
1991             except (ContentTooShortError, ) as err:
1992                 self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
1993                 return
1994
1995             if success and filename != '-':
1996                 # Fixup content
1997                 fixup_policy = self.params.get('fixup')
1998                 if fixup_policy is None:
1999                     fixup_policy = 'detect_or_warn'
2000
2001                 INSTALL_FFMPEG_MESSAGE = 'Install ffmpeg or avconv to fix this automatically.'
2002
2003                 stretched_ratio = info_dict.get('stretched_ratio')
2004                 if stretched_ratio is not None and stretched_ratio != 1:
2005                     if fixup_policy == 'warn':
2006                         self.report_warning('%s: Non-uniform pixel ratio (%s)' % (
2007                             info_dict['id'], stretched_ratio))
2008                     elif fixup_policy == 'detect_or_warn':
2009                         stretched_pp = FFmpegFixupStretchedPP(self)
2010                         if stretched_pp.available:
2011                             info_dict.setdefault('__postprocessors', [])
2012                             info_dict['__postprocessors'].append(stretched_pp)
2013                         else:
2014                             self.report_warning(
2015                                 '%s: Non-uniform pixel ratio (%s). %s'
2016                                 % (info_dict['id'], stretched_ratio, INSTALL_FFMPEG_MESSAGE))
2017                     else:
2018                         assert fixup_policy in ('ignore', 'never')
2019
2020                 if (info_dict.get('requested_formats') is None
2021                         and info_dict.get('container') == 'm4a_dash'):
2022                     if fixup_policy == 'warn':
2023                         self.report_warning(
2024                             '%s: writing DASH m4a. '
2025                             'Only some players support this container.'
2026                             % info_dict['id'])
2027                     elif fixup_policy == 'detect_or_warn':
2028                         fixup_pp = FFmpegFixupM4aPP(self)
2029                         if fixup_pp.available:
2030                             info_dict.setdefault('__postprocessors', [])
2031                             info_dict['__postprocessors'].append(fixup_pp)
2032                         else:
2033                             self.report_warning(
2034                                 '%s: writing DASH m4a. '
2035                                 'Only some players support this container. %s'
2036                                 % (info_dict['id'], INSTALL_FFMPEG_MESSAGE))
2037                     else:
2038                         assert fixup_policy in ('ignore', 'never')
2039
2040                 if (info_dict.get('protocol') == 'm3u8_native'
2041                         or info_dict.get('protocol') == 'm3u8'
2042                         and self.params.get('hls_prefer_native')):
2043                     if fixup_policy == 'warn':
2044                         self.report_warning('%s: malformed AAC bitstream detected.' % (
2045                             info_dict['id']))
2046                     elif fixup_policy == 'detect_or_warn':
2047                         fixup_pp = FFmpegFixupM3u8PP(self)
2048                         if fixup_pp.available:
2049                             info_dict.setdefault('__postprocessors', [])
2050                             info_dict['__postprocessors'].append(fixup_pp)
2051                         else:
2052                             self.report_warning(
2053                                 '%s: malformed AAC bitstream detected. %s'
2054                                 % (info_dict['id'], INSTALL_FFMPEG_MESSAGE))
2055                     else:
2056                         assert fixup_policy in ('ignore', 'never')
2057
2058                 try:
2059                     self.post_process(filename, info_dict)
2060                 except (PostProcessingError) as err:
2061                     self.report_error('postprocessing: %s' % str(err))
2062                     return
2063                 self.record_download_archive(info_dict)
2064
2065     def download(self, url_list):
2066         """Download a given list of URLs."""
2067         outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
2068         if (len(url_list) > 1
2069                 and outtmpl != '-'
2070                 and '%' not in outtmpl
2071                 and self.params.get('max_downloads') != 1):
2072             raise SameFileError(outtmpl)
2073
2074         for url in url_list:
2075             try:
2076                 # It also downloads the videos
2077                 res = self.extract_info(
2078                     url, force_generic_extractor=self.params.get('force_generic_extractor', False))
2079             except UnavailableVideoError:
2080                 self.report_error('unable to download video')
2081             except MaxDownloadsReached:
2082                 self.to_screen('[info] Maximum number of downloaded files reached.')
2083                 raise
2084             else:
2085                 if self.params.get('dump_single_json', False):
2086                     self.to_stdout(json.dumps(res))
2087
2088         return self._download_retcode
2089
2090     def download_with_info_file(self, info_filename):
2091         with contextlib.closing(fileinput.FileInput(
2092                 [info_filename], mode='r',
2093                 openhook=fileinput.hook_encoded('utf-8'))) as f:
2094             # FileInput doesn't have a read method, we can't call json.load
2095             info = self.filter_requested_info(json.loads('\n'.join(f)))
2096         try:
2097             self.process_ie_result(info, download=True)
2098         except DownloadError:
2099             webpage_url = info.get('webpage_url')
2100             if webpage_url is not None:
2101                 self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
2102                 return self.download([webpage_url])
2103             else:
2104                 raise
2105         return self._download_retcode
2106
2107     @staticmethod
2108     def filter_requested_info(info_dict):
2109         return dict(
2110             (k, v) for k, v in info_dict.items()
2111             if k not in ['requested_formats', 'requested_subtitles'])
2112
2113     def post_process(self, filename, ie_info):
2114         """Run all the postprocessors on the given file."""
2115         info = dict(ie_info)
2116         info['filepath'] = filename
2117         pps_chain = []
2118         if ie_info.get('__postprocessors') is not None:
2119             pps_chain.extend(ie_info['__postprocessors'])
2120         pps_chain.extend(self._pps)
2121         for pp in pps_chain:
2122             files_to_delete = []
2123             try:
2124                 files_to_delete, info = pp.run(info)
2125             except PostProcessingError as e:
2126                 self.report_error(e.msg)
2127             if files_to_delete and not self.params.get('keepvideo', False):
2128                 for old_filename in files_to_delete:
2129                     self.to_screen('Deleting original file %s (pass -k to keep)' % old_filename)
2130                     try:
2131                         os.remove(encodeFilename(old_filename))
2132                     except (IOError, OSError):
2133                         self.report_warning('Unable to remove downloaded original file')
2134
2135     def _make_archive_id(self, info_dict):
2136         video_id = info_dict.get('id')
2137         if not video_id:
2138             return
2139         # Future-proof against any change in case
2140         # and backwards compatibility with prior versions
2141         extractor = info_dict.get('extractor_key') or info_dict.get('ie_key')  # key in a playlist
2142         if extractor is None:
2143             url = str_or_none(info_dict.get('url'))
2144             if not url:
2145                 return
2146             # Try to find matching extractor for the URL and take its ie_key
2147             for ie in self._ies:
2148                 if ie.suitable(url):
2149                     extractor = ie.ie_key()
2150                     break
2151             else:
2152                 return
2153         return extractor.lower() + ' ' + video_id
2154
2155     def in_download_archive(self, info_dict):
2156         fn = self.params.get('download_archive')
2157         if fn is None:
2158             return False
2159
2160         vid_id = self._make_archive_id(info_dict)
2161         if not vid_id:
2162             return False  # Incomplete video information
2163
2164         try:
2165             with locked_file(fn, 'r', encoding='utf-8') as archive_file:
2166                 for line in archive_file:
2167                     if line.strip() == vid_id:
2168                         return True
2169         except IOError as ioe:
2170             if ioe.errno != errno.ENOENT:
2171                 raise
2172         return False
2173
2174     def record_download_archive(self, info_dict):
2175         fn = self.params.get('download_archive')
2176         if fn is None:
2177             return
2178         vid_id = self._make_archive_id(info_dict)
2179         assert vid_id
2180         with locked_file(fn, 'a', encoding='utf-8') as archive_file:
2181             archive_file.write(vid_id + '\n')
2182
2183     @staticmethod
2184     def format_resolution(format, default='unknown'):
2185         if format.get('vcodec') == 'none':
2186             return 'audio only'
2187         if format.get('resolution') is not None:
2188             return format['resolution']
2189         if format.get('height') is not None:
2190             if format.get('width') is not None:
2191                 res = '%sx%s' % (format['width'], format['height'])
2192             else:
2193                 res = '%sp' % format['height']
2194         elif format.get('width') is not None:
2195             res = '%dx?' % format['width']
2196         else:
2197             res = default
2198         return res
2199
2200     def _format_note(self, fdict):
2201         res = ''
2202         if fdict.get('ext') in ['f4f', 'f4m']:
2203             res += '(unsupported) '
2204         if fdict.get('language'):
2205             if res:
2206                 res += ' '
2207             res += '[%s] ' % fdict['language']
2208         if fdict.get('format_note') is not None:
2209             res += fdict['format_note'] + ' '
2210         if fdict.get('tbr') is not None:
2211             res += '%4dk ' % fdict['tbr']
2212         if fdict.get('container') is not None:
2213             if res:
2214                 res += ', '
2215             res += '%s container' % fdict['container']
2216         if (fdict.get('vcodec') is not None
2217                 and fdict.get('vcodec') != 'none'):
2218             if res:
2219                 res += ', '
2220             res += fdict['vcodec']
2221             if fdict.get('vbr') is not None:
2222                 res += '@'
2223         elif fdict.get('vbr') is not None and fdict.get('abr') is not None:
2224             res += 'video@'
2225         if fdict.get('vbr') is not None:
2226             res += '%4dk' % fdict['vbr']
2227         if fdict.get('fps') is not None:
2228             if res:
2229                 res += ', '
2230             res += '%sfps' % fdict['fps']
2231         if fdict.get('acodec') is not None:
2232             if res:
2233                 res += ', '
2234             if fdict['acodec'] == 'none':
2235                 res += 'video only'
2236             else:
2237                 res += '%-5s' % fdict['acodec']
2238         elif fdict.get('abr') is not None:
2239             if res:
2240                 res += ', '
2241             res += 'audio'
2242         if fdict.get('abr') is not None:
2243             res += '@%3dk' % fdict['abr']
2244         if fdict.get('asr') is not None:
2245             res += ' (%5dHz)' % fdict['asr']
2246         if fdict.get('filesize') is not None:
2247             if res:
2248                 res += ', '
2249             res += format_bytes(fdict['filesize'])
2250         elif fdict.get('filesize_approx') is not None:
2251             if res:
2252                 res += ', '
2253             res += '~' + format_bytes(fdict['filesize_approx'])
2254         return res
2255
2256     def list_formats(self, info_dict):
2257         formats = info_dict.get('formats', [info_dict])
2258         table = [
2259             [f['format_id'], f['ext'], self.format_resolution(f), self._format_note(f)]
2260             for f in formats
2261             if f.get('preference') is None or f['preference'] >= -1000]
2262         if len(formats) > 1:
2263             table[-1][-1] += (' ' if table[-1][-1] else '') + '(best)'
2264
2265         header_line = ['format code', 'extension', 'resolution', 'note']
2266         self.to_screen(
2267             '[info] Available formats for %s:\n%s' %
2268             (info_dict['id'], render_table(header_line, table)))
2269
2270     def list_thumbnails(self, info_dict):
2271         thumbnails = info_dict.get('thumbnails')
2272         if not thumbnails:
2273             self.to_screen('[info] No thumbnails present for %s' % info_dict['id'])
2274             return
2275
2276         self.to_screen(
2277             '[info] Thumbnails for %s:' % info_dict['id'])
2278         self.to_screen(render_table(
2279             ['ID', 'width', 'height', 'URL'],
2280             [[t['id'], t.get('width', 'unknown'), t.get('height', 'unknown'), t['url']] for t in thumbnails]))
2281
2282     def list_subtitles(self, video_id, subtitles, name='subtitles'):
2283         if not subtitles:
2284             self.to_screen('%s has no %s' % (video_id, name))
2285             return
2286         self.to_screen(
2287             'Available %s for %s:' % (name, video_id))
2288         self.to_screen(render_table(
2289             ['Language', 'formats'],
2290             [[lang, ', '.join(f['ext'] for f in reversed(formats))]
2291                 for lang, formats in subtitles.items()]))
2292
2293     def urlopen(self, req):
2294         """ Start an HTTP download """
2295         if isinstance(req, compat_basestring):
2296             req = sanitized_Request(req)
2297         return self._opener.open(req, timeout=self._socket_timeout)
2298
2299     def print_debug_header(self):
2300         if not self.params.get('verbose'):
2301             return
2302
2303         if type('') is not compat_str:
2304             # Python 2.6 on SLES11 SP1 (https://github.com/ytdl-org/youtube-dl/issues/3326)
2305             self.report_warning(
2306                 'Your Python is broken! Update to a newer and supported version')
2307
2308         stdout_encoding = getattr(
2309             sys.stdout, 'encoding', 'missing (%s)' % type(sys.stdout).__name__)
2310         encoding_str = (
2311             '[debug] Encodings: locale %s, fs %s, out %s, pref %s\n' % (
2312                 locale.getpreferredencoding(),
2313                 sys.getfilesystemencoding(),
2314                 stdout_encoding,
2315                 self.get_encoding()))
2316         write_string(encoding_str, encoding=None)
2317
2318         self._write_string('[debug] youtube-dl version ' + __version__ + '\n')
2319         if _LAZY_LOADER:
2320             self._write_string('[debug] Lazy loading extractors enabled' + '\n')
2321         try:
2322             sp = subprocess.Popen(
2323                 ['git', 'rev-parse', '--short', 'HEAD'],
2324                 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
2325                 cwd=os.path.dirname(os.path.abspath(__file__)))
2326             out, err = sp.communicate()
2327             out = out.decode().strip()
2328             if re.match('[0-9a-f]+', out):
2329                 self._write_string('[debug] Git HEAD: ' + out + '\n')
2330         except Exception:
2331             try:
2332                 sys.exc_clear()
2333             except Exception:
2334                 pass
2335
2336         def python_implementation():
2337             impl_name = platform.python_implementation()
2338             if impl_name == 'PyPy' and hasattr(sys, 'pypy_version_info'):
2339                 return impl_name + ' version %d.%d.%d' % sys.pypy_version_info[:3]
2340             return impl_name
2341
2342         self._write_string('[debug] Python version %s (%s) - %s\n' % (
2343             platform.python_version(), python_implementation(),
2344             platform_name()))
2345
2346         exe_versions = FFmpegPostProcessor.get_versions(self)
2347         exe_versions['rtmpdump'] = rtmpdump_version()
2348         exe_versions['phantomjs'] = PhantomJSwrapper._version()
2349         exe_str = ', '.join(
2350             '%s %s' % (exe, v)
2351             for exe, v in sorted(exe_versions.items())
2352             if v
2353         )
2354         if not exe_str:
2355             exe_str = 'none'
2356         self._write_string('[debug] exe versions: %s\n' % exe_str)
2357
2358         proxy_map = {}
2359         for handler in self._opener.handlers:
2360             if hasattr(handler, 'proxies'):
2361                 proxy_map.update(handler.proxies)
2362         self._write_string('[debug] Proxy map: ' + compat_str(proxy_map) + '\n')
2363
2364         if self.params.get('call_home', False):
2365             ipaddr = self.urlopen('https://yt-dl.org/ip').read().decode('utf-8')
2366             self._write_string('[debug] Public IP address: %s\n' % ipaddr)
2367             latest_version = self.urlopen(
2368                 'https://yt-dl.org/latest/version').read().decode('utf-8')
2369             if version_tuple(latest_version) > version_tuple(__version__):
2370                 self.report_warning(
2371                     'You are using an outdated version (newest version: %s)! '
2372                     'See https://yt-dl.org/update if you need help updating.' %
2373                     latest_version)
2374
2375     def _setup_opener(self):
2376         timeout_val = self.params.get('socket_timeout')
2377         self._socket_timeout = 600 if timeout_val is None else float(timeout_val)
2378
2379         opts_cookiefile = self.params.get('cookiefile')
2380         opts_proxy = self.params.get('proxy')
2381
2382         if opts_cookiefile is None:
2383             self.cookiejar = compat_cookiejar.CookieJar()
2384         else:
2385             opts_cookiefile = expand_path(opts_cookiefile)
2386             self.cookiejar = YoutubeDLCookieJar(opts_cookiefile)
2387             if os.access(opts_cookiefile, os.R_OK):
2388                 self.cookiejar.load(ignore_discard=True, ignore_expires=True)
2389
2390         cookie_processor = YoutubeDLCookieProcessor(self.cookiejar)
2391         if opts_proxy is not None:
2392             if opts_proxy == '':
2393                 proxies = {}
2394             else:
2395                 proxies = {'http': opts_proxy, 'https': opts_proxy}
2396         else:
2397             proxies = compat_urllib_request.getproxies()
2398             # Set HTTPS proxy to HTTP one if given (https://github.com/ytdl-org/youtube-dl/issues/805)
2399             if 'http' in proxies and 'https' not in proxies:
2400                 proxies['https'] = proxies['http']
2401         proxy_handler = PerRequestProxyHandler(proxies)
2402
2403         debuglevel = 1 if self.params.get('debug_printtraffic') else 0
2404         https_handler = make_HTTPS_handler(self.params, debuglevel=debuglevel)
2405         ydlh = YoutubeDLHandler(self.params, debuglevel=debuglevel)
2406         redirect_handler = YoutubeDLRedirectHandler()
2407         data_handler = compat_urllib_request_DataHandler()
2408
2409         # When passing our own FileHandler instance, build_opener won't add the
2410         # default FileHandler and allows us to disable the file protocol, which
2411         # can be used for malicious purposes (see
2412         # https://github.com/ytdl-org/youtube-dl/issues/8227)
2413         file_handler = compat_urllib_request.FileHandler()
2414
2415         def file_open(*args, **kwargs):
2416             raise compat_urllib_error.URLError('file:// scheme is explicitly disabled in youtube-dl for security reasons')
2417         file_handler.file_open = file_open
2418
2419         opener = compat_urllib_request.build_opener(
2420             proxy_handler, https_handler, cookie_processor, ydlh, redirect_handler, data_handler, file_handler)
2421
2422         # Delete the default user-agent header, which would otherwise apply in
2423         # cases where our custom HTTP handler doesn't come into play
2424         # (See https://github.com/ytdl-org/youtube-dl/issues/1309 for details)
2425         opener.addheaders = []
2426         self._opener = opener
2427
2428     def encode(self, s):
2429         if isinstance(s, bytes):
2430             return s  # Already encoded
2431
2432         try:
2433             return s.encode(self.get_encoding())
2434         except UnicodeEncodeError as err:
2435             err.reason = err.reason + '. Check your system encoding configuration or use the --encoding option.'
2436             raise
2437
2438     def get_encoding(self):
2439         encoding = self.params.get('encoding')
2440         if encoding is None:
2441             encoding = preferredencoding()
2442         return encoding
2443
2444     def _write_thumbnails(self, info_dict, filename):
2445         if self.params.get('writethumbnail', False):
2446             thumbnails = info_dict.get('thumbnails')
2447             if thumbnails:
2448                 thumbnails = [thumbnails[-1]]
2449         elif self.params.get('write_all_thumbnails', False):
2450             thumbnails = info_dict.get('thumbnails')
2451         else:
2452             return
2453
2454         if not thumbnails:
2455             # No thumbnails present, so return immediately
2456             return
2457
2458         for t in thumbnails:
2459             thumb_ext = determine_ext(t['url'], 'jpg')
2460             suffix = '_%s' % t['id'] if len(thumbnails) > 1 else ''
2461             thumb_display_id = '%s ' % t['id'] if len(thumbnails) > 1 else ''
2462             t['filename'] = thumb_filename = replace_extension(filename + suffix, thumb_ext, info_dict.get('ext'))
2463
2464             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
2465                 self.to_screen('[%s] %s: Thumbnail %sis already present' %
2466                                (info_dict['extractor'], info_dict['id'], thumb_display_id))
2467             else:
2468                 self.to_screen('[%s] %s: Downloading thumbnail %s...' %
2469                                (info_dict['extractor'], info_dict['id'], thumb_display_id))
2470                 try:
2471                     uf = self.urlopen(t['url'])
2472                     with open(encodeFilename(thumb_filename), 'wb') as thumbf:
2473                         shutil.copyfileobj(uf, thumbf)
2474                     self.to_screen('[%s] %s: Writing thumbnail %sto: %s' %
2475                                    (info_dict['extractor'], info_dict['id'], thumb_display_id, thumb_filename))
2476                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
2477                     self.report_warning('Unable to download thumbnail "%s": %s' %
2478                                         (t['url'], error_to_compat_str(err)))