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