[--load-info] Always read file as UTF-8
[youtube-dl] / youtube_dl / YoutubeDL.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from __future__ import absolute_import
5
6 import errno
7 import io
8 import json
9 import os
10 import platform
11 import re
12 import shutil
13 import subprocess
14 import socket
15 import sys
16 import time
17 import traceback
18
19 if os.name == 'nt':
20     import ctypes
21
22 from .utils import (
23     compat_cookiejar,
24     compat_http_client,
25     compat_str,
26     compat_urllib_error,
27     compat_urllib_request,
28     ContentTooShortError,
29     date_from_str,
30     DateRange,
31     determine_ext,
32     DownloadError,
33     encodeFilename,
34     ExtractorError,
35     format_bytes,
36     locked_file,
37     make_HTTPS_handler,
38     MaxDownloadsReached,
39     PostProcessingError,
40     platform_name,
41     preferredencoding,
42     SameFileError,
43     sanitize_filename,
44     subtitles_filename,
45     takewhile_inclusive,
46     UnavailableVideoError,
47     write_json_file,
48     write_string,
49     YoutubeDLHandler,
50 )
51 from .extractor import get_info_extractor, gen_extractors
52 from .FileDownloader import FileDownloader
53 from .version import __version__
54
55
56 class YoutubeDL(object):
57     """YoutubeDL class.
58
59     YoutubeDL objects are the ones responsible of downloading the
60     actual video file and writing it to disk if the user has requested
61     it, among some other tasks. In most cases there should be one per
62     program. As, given a video URL, the downloader doesn't know how to
63     extract all the needed information, task that InfoExtractors do, it
64     has to pass the URL to one of them.
65
66     For this, YoutubeDL objects have a method that allows
67     InfoExtractors to be registered in a given order. When it is passed
68     a URL, the YoutubeDL object handles it to the first InfoExtractor it
69     finds that reports being able to handle it. The InfoExtractor extracts
70     all the information about the video or videos the URL refers to, and
71     YoutubeDL process the extracted information, possibly using a File
72     Downloader to download the video.
73
74     YoutubeDL objects accept a lot of parameters. In order not to saturate
75     the object constructor with arguments, it receives a dictionary of
76     options instead. These options are available through the params
77     attribute for the InfoExtractors to use. The YoutubeDL also
78     registers itself as the downloader in charge for the InfoExtractors
79     that are added to it, so this is a "mutual registration".
80
81     Available options:
82
83     username:          Username for authentication purposes.
84     password:          Password for authentication purposes.
85     videopassword:     Password for acces a video.
86     usenetrc:          Use netrc for authentication instead.
87     verbose:           Print additional info to stdout.
88     quiet:             Do not print messages to stdout.
89     forceurl:          Force printing final URL.
90     forcetitle:        Force printing title.
91     forceid:           Force printing ID.
92     forcethumbnail:    Force printing thumbnail URL.
93     forcedescription:  Force printing description.
94     forcefilename:     Force printing final filename.
95     forcejson:         Force printing info_dict as JSON.
96     simulate:          Do not download the video files.
97     format:            Video format code.
98     format_limit:      Highest quality format to try.
99     outtmpl:           Template for output names.
100     restrictfilenames: Do not allow "&" and spaces in file names
101     ignoreerrors:      Do not stop on download errors.
102     nooverwrites:      Prevent overwriting files.
103     playliststart:     Playlist item to start at.
104     playlistend:       Playlist item to end at.
105     matchtitle:        Download only matching titles.
106     rejecttitle:       Reject downloads for matching titles.
107     logger:            Log messages to a logging.Logger instance.
108     logtostderr:       Log messages to stderr instead of stdout.
109     writedescription:  Write the video description to a .description file
110     writeinfojson:     Write the video description to a .info.json file
111     writeannotations:  Write the video annotations to a .annotations.xml file
112     writethumbnail:    Write the thumbnail image to a file
113     writesubtitles:    Write the video subtitles to a file
114     writeautomaticsub: Write the automatic subtitles to a file
115     allsubtitles:      Downloads all the subtitles of the video
116                        (requires writesubtitles or writeautomaticsub)
117     listsubtitles:     Lists all available subtitles for the video
118     subtitlesformat:   Subtitle format [srt/sbv/vtt] (default=srt)
119     subtitleslangs:    List of languages of the subtitles to download
120     keepvideo:         Keep the video file after post-processing
121     daterange:         A DateRange object, download only if the upload_date is in the range.
122     skip_download:     Skip the actual download of the video file
123     cachedir:          Location of the cache files in the filesystem.
124                        None to disable filesystem cache.
125     noplaylist:        Download single video instead of a playlist if in doubt.
126     age_limit:         An integer representing the user's age in years.
127                        Unsuitable videos for the given age are skipped.
128     download_archive:   File name of a file where all downloads are recorded.
129                        Videos already present in the file are not downloaded
130                        again.
131     cookiefile:        File name where cookies should be read from and dumped to.
132     nocheckcertificate:Do not verify SSL certificates
133     proxy:             URL of the proxy server to use
134     socket_timeout:    Time to wait for unresponsive hosts, in seconds
135     bidi_workaround:   Work around buggy terminals without bidirectional text
136                        support, using fridibi
137
138     The following parameters are not used by YoutubeDL itself, they are used by
139     the FileDownloader:
140     nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
141     noresizebuffer, retries, continuedl, noprogress, consoletitle
142     """
143
144     params = None
145     _ies = []
146     _pps = []
147     _download_retcode = None
148     _num_downloads = None
149     _screen_file = None
150
151     def __init__(self, params=None):
152         """Create a FileDownloader object with the given options."""
153         self._ies = []
154         self._ies_instances = {}
155         self._pps = []
156         self._progress_hooks = []
157         self._download_retcode = 0
158         self._num_downloads = 0
159         self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
160         self._err_file = sys.stderr
161         self.params = {} if params is None else params
162
163         # Pipe messsages through fribidi
164         if params.get('bidi_workaround', False):
165             # fribidi does not support ungetting, so force newlines
166             params['progress_with_newline'] = True
167
168             for fid in ['_screen_file', '_err_file']:
169                 class FribidiOut(object):
170                     def __init__(self, outfile, errfile):
171                         self.outfile = outfile
172                         self.process = subprocess.Popen(
173                             ['fribidi'],
174                             stdin=subprocess.PIPE,
175                             stdout=outfile,
176                             stderr=errfile)
177
178                     def write(self, s):
179                         res = self.process.stdin.write(s)
180                         self.flush()
181                         return res
182
183                     def flush(self):
184                         return self.process.stdin.flush()
185
186                     def isatty(self):
187                         return self.outfile.isatty()
188
189                 try:
190                     vout = FribidiOut(getattr(self, fid), self._err_file)
191                     setattr(self, fid, vout)
192                 except OSError as ose:
193                     if ose.errno == 2:
194                         self.report_warning(u'Could not find fribidi executable, ignoring --bidi-workaround . Make sure that  fribidi  is an executable file in one of the directories in your $PATH.')
195                         break
196                     else:
197                         raise
198
199         if (sys.version_info >= (3,) and sys.platform != 'win32' and
200                 sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968']
201                 and not params['restrictfilenames']):
202             # On Python 3, the Unicode filesystem API will throw errors (#1474)
203             self.report_warning(
204                 u'Assuming --restrict-filenames since file system encoding '
205                 u'cannot encode all charactes. '
206                 u'Set the LC_ALL environment variable to fix this.')
207             self.params['restrictfilenames'] = True
208
209         self.fd = FileDownloader(self, self.params)
210
211         if '%(stitle)s' in self.params.get('outtmpl', ''):
212             self.report_warning(u'%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
213
214         self._setup_opener()
215
216     def add_info_extractor(self, ie):
217         """Add an InfoExtractor object to the end of the list."""
218         self._ies.append(ie)
219         self._ies_instances[ie.ie_key()] = ie
220         ie.set_downloader(self)
221
222     def get_info_extractor(self, ie_key):
223         """
224         Get an instance of an IE with name ie_key, it will try to get one from
225         the _ies list, if there's no instance it will create a new one and add
226         it to the extractor list.
227         """
228         ie = self._ies_instances.get(ie_key)
229         if ie is None:
230             ie = get_info_extractor(ie_key)()
231             self.add_info_extractor(ie)
232         return ie
233
234     def add_default_info_extractors(self):
235         """
236         Add the InfoExtractors returned by gen_extractors to the end of the list
237         """
238         for ie in gen_extractors():
239             self.add_info_extractor(ie)
240
241     def add_post_processor(self, pp):
242         """Add a PostProcessor object to the end of the chain."""
243         self._pps.append(pp)
244         pp.set_downloader(self)
245
246     def to_screen(self, message, skip_eol=False):
247         """Print message to stdout if not in quiet mode."""
248         return self.to_stdout(message, skip_eol, check_quiet=True)
249
250     def to_stdout(self, message, skip_eol=False, check_quiet=False):
251         """Print message to stdout if not in quiet mode."""
252         if self.params.get('logger'):
253             self.params['logger'].debug(message)
254         elif not check_quiet or not self.params.get('quiet', False):
255             terminator = [u'\n', u''][skip_eol]
256             output = message + terminator
257             write_string(output, self._screen_file)
258
259     def to_stderr(self, message):
260         """Print message to stderr."""
261         assert type(message) == type(u'')
262         if self.params.get('logger'):
263             self.params['logger'].error(message)
264         else:
265             output = message + u'\n'
266             write_string(output, self._err_file)
267
268     def to_console_title(self, message):
269         if not self.params.get('consoletitle', False):
270             return
271         if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
272             # c_wchar_p() might not be necessary if `message` is
273             # already of type unicode()
274             ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
275         elif 'TERM' in os.environ:
276             write_string(u'\033]0;%s\007' % message, self._screen_file)
277
278     def save_console_title(self):
279         if not self.params.get('consoletitle', False):
280             return
281         if 'TERM' in os.environ:
282             # Save the title on stack
283             write_string(u'\033[22;0t', self._screen_file)
284
285     def restore_console_title(self):
286         if not self.params.get('consoletitle', False):
287             return
288         if 'TERM' in os.environ:
289             # Restore the title from stack
290             write_string(u'\033[23;0t', self._screen_file)
291
292     def __enter__(self):
293         self.save_console_title()
294         return self
295
296     def __exit__(self, *args):
297         self.restore_console_title()
298     
299         if self.params.get('cookiefile') is not None:
300             self.cookiejar.save()
301
302     def trouble(self, message=None, tb=None):
303         """Determine action to take when a download problem appears.
304
305         Depending on if the downloader has been configured to ignore
306         download errors or not, this method may throw an exception or
307         not when errors are found, after printing the message.
308
309         tb, if given, is additional traceback information.
310         """
311         if message is not None:
312             self.to_stderr(message)
313         if self.params.get('verbose'):
314             if tb is None:
315                 if sys.exc_info()[0]:  # if .trouble has been called from an except block
316                     tb = u''
317                     if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
318                         tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
319                     tb += compat_str(traceback.format_exc())
320                 else:
321                     tb_data = traceback.format_list(traceback.extract_stack())
322                     tb = u''.join(tb_data)
323             self.to_stderr(tb)
324         if not self.params.get('ignoreerrors', False):
325             if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
326                 exc_info = sys.exc_info()[1].exc_info
327             else:
328                 exc_info = sys.exc_info()
329             raise DownloadError(message, exc_info)
330         self._download_retcode = 1
331
332     def report_warning(self, message):
333         '''
334         Print the message to stderr, it will be prefixed with 'WARNING:'
335         If stderr is a tty file the 'WARNING:' will be colored
336         '''
337         if self._err_file.isatty() and os.name != 'nt':
338             _msg_header = u'\033[0;33mWARNING:\033[0m'
339         else:
340             _msg_header = u'WARNING:'
341         warning_message = u'%s %s' % (_msg_header, message)
342         self.to_stderr(warning_message)
343
344     def report_error(self, message, tb=None):
345         '''
346         Do the same as trouble, but prefixes the message with 'ERROR:', colored
347         in red if stderr is a tty file.
348         '''
349         if self._err_file.isatty() and os.name != 'nt':
350             _msg_header = u'\033[0;31mERROR:\033[0m'
351         else:
352             _msg_header = u'ERROR:'
353         error_message = u'%s %s' % (_msg_header, message)
354         self.trouble(error_message, tb)
355
356     def report_writedescription(self, descfn):
357         """ Report that the description file is being written """
358         self.to_screen(u'[info] Writing video description to: ' + descfn)
359
360     def report_writesubtitles(self, sub_filename):
361         """ Report that the subtitles file is being written """
362         self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename)
363
364     def report_writeinfojson(self, infofn):
365         """ Report that the metadata file has been written """
366         self.to_screen(u'[info] Video description metadata as JSON to: ' + infofn)
367
368     def report_writeannotations(self, annofn):
369         """ Report that the annotations file has been written. """
370         self.to_screen(u'[info] Writing video annotations to: ' + annofn)
371
372     def report_file_already_downloaded(self, file_name):
373         """Report file has already been fully downloaded."""
374         try:
375             self.to_screen(u'[download] %s has already been downloaded' % file_name)
376         except UnicodeEncodeError:
377             self.to_screen(u'[download] The file has already been downloaded')
378
379     def increment_downloads(self):
380         """Increment the ordinal that assigns a number to each file."""
381         self._num_downloads += 1
382
383     def prepare_filename(self, info_dict):
384         """Generate the output filename."""
385         try:
386             template_dict = dict(info_dict)
387
388             template_dict['epoch'] = int(time.time())
389             autonumber_size = self.params.get('autonumber_size')
390             if autonumber_size is None:
391                 autonumber_size = 5
392             autonumber_templ = u'%0' + str(autonumber_size) + u'd'
393             template_dict['autonumber'] = autonumber_templ % self._num_downloads
394             if template_dict.get('playlist_index') is not None:
395                 template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index']
396
397             sanitize = lambda k, v: sanitize_filename(
398                 u'NA' if v is None else compat_str(v),
399                 restricted=self.params.get('restrictfilenames'),
400                 is_id=(k == u'id'))
401             template_dict = dict((k, sanitize(k, v))
402                                  for k, v in template_dict.items())
403
404             tmpl = os.path.expanduser(self.params['outtmpl'])
405             filename = tmpl % template_dict
406             return filename
407         except KeyError as err:
408             self.report_error(u'Erroneous output template')
409             return None
410         except ValueError as err:
411             self.report_error(u'Error in output template: ' + str(err) + u' (encoding: ' + repr(preferredencoding()) + ')')
412             return None
413
414     def _match_entry(self, info_dict):
415         """ Returns None iff the file should be downloaded """
416
417         if 'title' in info_dict:
418             # This can happen when we're just evaluating the playlist
419             title = info_dict['title']
420             matchtitle = self.params.get('matchtitle', False)
421             if matchtitle:
422                 if not re.search(matchtitle, title, re.IGNORECASE):
423                     return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
424             rejecttitle = self.params.get('rejecttitle', False)
425             if rejecttitle:
426                 if re.search(rejecttitle, title, re.IGNORECASE):
427                     return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
428         date = info_dict.get('upload_date', None)
429         if date is not None:
430             dateRange = self.params.get('daterange', DateRange())
431             if date not in dateRange:
432                 return u'[download] %s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
433         age_limit = self.params.get('age_limit')
434         if age_limit is not None:
435             if age_limit < info_dict.get('age_limit', 0):
436                 return u'Skipping "' + title + '" because it is age restricted'
437         if self.in_download_archive(info_dict):
438             return (u'%s has already been recorded in archive'
439                     % info_dict.get('title', info_dict.get('id', u'video')))
440         return None
441
442     @staticmethod
443     def add_extra_info(info_dict, extra_info):
444         '''Set the keys from extra_info in info dict if they are missing'''
445         for key, value in extra_info.items():
446             info_dict.setdefault(key, value)
447
448     def extract_info(self, url, download=True, ie_key=None, extra_info={},
449                      process=True):
450         '''
451         Returns a list with a dictionary for each video we find.
452         If 'download', also downloads the videos.
453         extra_info is a dict containing the extra values to add to each result
454          '''
455
456         if ie_key:
457             ies = [self.get_info_extractor(ie_key)]
458         else:
459             ies = self._ies
460
461         for ie in ies:
462             if not ie.suitable(url):
463                 continue
464
465             if not ie.working():
466                 self.report_warning(u'The program functionality for this site has been marked as broken, '
467                                     u'and will probably not work.')
468
469             try:
470                 ie_result = ie.extract(url)
471                 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
472                     break
473                 if isinstance(ie_result, list):
474                     # Backwards compatibility: old IE result format
475                     ie_result = {
476                         '_type': 'compat_list',
477                         'entries': ie_result,
478                     }
479                 self.add_extra_info(ie_result,
480                     {
481                         'extractor': ie.IE_NAME,
482                         'webpage_url': url,
483                         'extractor_key': ie.ie_key(),
484                     })
485                 if process:
486                     return self.process_ie_result(ie_result, download, extra_info)
487                 else:
488                     return ie_result
489             except ExtractorError as de: # An error we somewhat expected
490                 self.report_error(compat_str(de), de.format_traceback())
491                 break
492             except Exception as e:
493                 if self.params.get('ignoreerrors', False):
494                     self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
495                     break
496                 else:
497                     raise
498         else:
499             self.report_error(u'no suitable InfoExtractor: %s' % url)
500
501     def process_ie_result(self, ie_result, download=True, extra_info={}):
502         """
503         Take the result of the ie(may be modified) and resolve all unresolved
504         references (URLs, playlist items).
505
506         It will also download the videos if 'download'.
507         Returns the resolved ie_result.
508         """
509
510         result_type = ie_result.get('_type', 'video') # If not given we suppose it's a video, support the default old system
511         if result_type == 'video':
512             self.add_extra_info(ie_result, extra_info)
513             return self.process_video_result(ie_result, download=download)
514         elif result_type == 'url':
515             # We have to add extra_info to the results because it may be
516             # contained in a playlist
517             return self.extract_info(ie_result['url'],
518                                      download,
519                                      ie_key=ie_result.get('ie_key'),
520                                      extra_info=extra_info)
521         elif result_type == 'url_transparent':
522             # Use the information from the embedding page
523             info = self.extract_info(
524                 ie_result['url'], ie_key=ie_result.get('ie_key'),
525                 extra_info=extra_info, download=False, process=False)
526
527             def make_result(embedded_info):
528                 new_result = ie_result.copy()
529                 for f in ('_type', 'url', 'ext', 'player_url', 'formats',
530                           'entries', 'urlhandle', 'ie_key', 'duration',
531                           'subtitles', 'annotations', 'format',
532                           'thumbnail', 'thumbnails'):
533                     if f in new_result:
534                         del new_result[f]
535                     if f in embedded_info:
536                         new_result[f] = embedded_info[f]
537                 return new_result
538             new_result = make_result(info)
539
540             assert new_result.get('_type') != 'url_transparent'
541             if new_result.get('_type') == 'compat_list':
542                 new_result['entries'] = [
543                     make_result(e) for e in new_result['entries']]
544
545             return self.process_ie_result(
546                 new_result, download=download, extra_info=extra_info)
547         elif result_type == 'playlist':
548             # We process each entry in the playlist
549             playlist = ie_result.get('title', None) or ie_result.get('id', None)
550             self.to_screen(u'[download] Downloading playlist: %s' % playlist)
551
552             playlist_results = []
553
554             n_all_entries = len(ie_result['entries'])
555             playliststart = self.params.get('playliststart', 1) - 1
556             playlistend = self.params.get('playlistend', -1)
557
558             if playlistend == -1:
559                 entries = ie_result['entries'][playliststart:]
560             else:
561                 entries = ie_result['entries'][playliststart:playlistend]
562
563             n_entries = len(entries)
564
565             self.to_screen(u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
566                 (ie_result['extractor'], playlist, n_all_entries, n_entries))
567
568             for i, entry in enumerate(entries, 1):
569                 self.to_screen(u'[download] Downloading video #%s of %s' % (i, n_entries))
570                 extra = {
571                     'playlist': playlist,
572                     'playlist_index': i + playliststart,
573                     'extractor': ie_result['extractor'],
574                     'webpage_url': ie_result['webpage_url'],
575                     'extractor_key': ie_result['extractor_key'],
576                 }
577
578                 reason = self._match_entry(entry)
579                 if reason is not None:
580                     self.to_screen(u'[download] ' + reason)
581                     continue
582
583                 entry_result = self.process_ie_result(entry,
584                                                       download=download,
585                                                       extra_info=extra)
586                 playlist_results.append(entry_result)
587             ie_result['entries'] = playlist_results
588             return ie_result
589         elif result_type == 'compat_list':
590             def _fixup(r):
591                 self.add_extra_info(r,
592                     {
593                         'extractor': ie_result['extractor'],
594                         'webpage_url': ie_result['webpage_url'],
595                         'extractor_key': ie_result['extractor_key'],
596                     })
597                 return r
598             ie_result['entries'] = [
599                 self.process_ie_result(_fixup(r), download, extra_info)
600                 for r in ie_result['entries']
601             ]
602             return ie_result
603         else:
604             raise Exception('Invalid result type: %s' % result_type)
605
606     def select_format(self, format_spec, available_formats):
607         if format_spec == 'best' or format_spec is None:
608             return available_formats[-1]
609         elif format_spec == 'worst':
610             return available_formats[0]
611         else:
612             extensions = [u'mp4', u'flv', u'webm', u'3gp']
613             if format_spec in extensions:
614                 filter_f = lambda f: f['ext'] == format_spec
615             else:
616                 filter_f = lambda f: f['format_id'] == format_spec
617             matches = list(filter(filter_f, available_formats))
618             if matches:
619                 return matches[-1]
620         return None
621
622     def process_video_result(self, info_dict, download=True):
623         assert info_dict.get('_type', 'video') == 'video'
624
625         if 'playlist' not in info_dict:
626             # It isn't part of a playlist
627             info_dict['playlist'] = None
628             info_dict['playlist_index'] = None
629
630         # This extractors handle format selection themselves
631         if info_dict['extractor'] in [u'youtube', u'Youku']:
632             if download:
633                 self.process_info(info_dict)
634             return info_dict
635
636         # We now pick which formats have to be downloaded
637         if info_dict.get('formats') is None:
638             # There's only one format available
639             formats = [info_dict]
640         else:
641             formats = info_dict['formats']
642
643         # We check that all the formats have the format and format_id fields
644         for (i, format) in enumerate(formats):
645             if format.get('format_id') is None:
646                 format['format_id'] = compat_str(i)
647             if format.get('format') is None:
648                 format['format'] = u'{id} - {res}{note}'.format(
649                     id=format['format_id'],
650                     res=self.format_resolution(format),
651                     note=u' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
652                 )
653             # Automatically determine file extension if missing
654             if 'ext' not in format:
655                 format['ext'] = determine_ext(format['url'])
656
657         if self.params.get('listformats', None):
658             self.list_formats(info_dict)
659             return
660
661         format_limit = self.params.get('format_limit', None)
662         if format_limit:
663             formats = list(takewhile_inclusive(
664                 lambda f: f['format_id'] != format_limit, formats
665             ))
666         if self.params.get('prefer_free_formats'):
667             def _free_formats_key(f):
668                 try:
669                     ext_ord = [u'flv', u'mp4', u'webm'].index(f['ext'])
670                 except ValueError:
671                     ext_ord = -1
672                 # We only compare the extension if they have the same height and width
673                 return (f.get('height'), f.get('width'), ext_ord)
674             formats = sorted(formats, key=_free_formats_key)
675
676         req_format = self.params.get('format', 'best')
677         if req_format is None:
678             req_format = 'best'
679         formats_to_download = []
680         # The -1 is for supporting YoutubeIE
681         if req_format in ('-1', 'all'):
682             formats_to_download = formats
683         else:
684             # We can accept formats requestd in the format: 34/5/best, we pick
685             # the first that is available, starting from left
686             req_formats = req_format.split('/')
687             for rf in req_formats:
688                 selected_format = self.select_format(rf, formats)
689                 if selected_format is not None:
690                     formats_to_download = [selected_format]
691                     break
692         if not formats_to_download:
693             raise ExtractorError(u'requested format not available',
694                                  expected=True)
695
696         if download:
697             if len(formats_to_download) > 1:
698                 self.to_screen(u'[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
699             for format in formats_to_download:
700                 new_info = dict(info_dict)
701                 new_info.update(format)
702                 self.process_info(new_info)
703         # We update the info dict with the best quality format (backwards compatibility)
704         info_dict.update(formats_to_download[-1])
705         return info_dict
706
707     def process_info(self, info_dict):
708         """Process a single resolved IE result."""
709
710         assert info_dict.get('_type', 'video') == 'video'
711         #We increment the download the download count here to match the previous behaviour.
712         self.increment_downloads()
713
714         info_dict['fulltitle'] = info_dict['title']
715         if len(info_dict['title']) > 200:
716             info_dict['title'] = info_dict['title'][:197] + u'...'
717
718         # Keep for backwards compatibility
719         info_dict['stitle'] = info_dict['title']
720
721         if not 'format' in info_dict:
722             info_dict['format'] = info_dict['ext']
723
724         reason = self._match_entry(info_dict)
725         if reason is not None:
726             self.to_screen(u'[download] ' + reason)
727             return
728
729         max_downloads = self.params.get('max_downloads')
730         if max_downloads is not None:
731             if self._num_downloads > int(max_downloads):
732                 raise MaxDownloadsReached()
733
734         filename = self.prepare_filename(info_dict)
735
736         # Forced printings
737         if self.params.get('forcetitle', False):
738             self.to_stdout(info_dict['fulltitle'])
739         if self.params.get('forceid', False):
740             self.to_stdout(info_dict['id'])
741         if self.params.get('forceurl', False):
742             # For RTMP URLs, also include the playpath
743             self.to_stdout(info_dict['url'] + info_dict.get('play_path', u''))
744         if self.params.get('forcethumbnail', False) and info_dict.get('thumbnail') is not None:
745             self.to_stdout(info_dict['thumbnail'])
746         if self.params.get('forcedescription', False) and info_dict.get('description') is not None:
747             self.to_stdout(info_dict['description'])
748         if self.params.get('forcefilename', False) and filename is not None:
749             self.to_stdout(filename)
750         if self.params.get('forceformat', False):
751             self.to_stdout(info_dict['format'])
752         if self.params.get('forcejson', False):
753             info_dict['_filename'] = filename
754             self.to_stdout(json.dumps(info_dict))
755
756         # Do nothing else if in simulate mode
757         if self.params.get('simulate', False):
758             return
759
760         if filename is None:
761             return
762
763         try:
764             dn = os.path.dirname(encodeFilename(filename))
765             if dn != '' and not os.path.exists(dn):
766                 os.makedirs(dn)
767         except (OSError, IOError) as err:
768             self.report_error(u'unable to create directory ' + compat_str(err))
769             return
770
771         if self.params.get('writedescription', False):
772             try:
773                 descfn = filename + u'.description'
774                 self.report_writedescription(descfn)
775                 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
776                     descfile.write(info_dict['description'])
777             except (KeyError, TypeError):
778                 self.report_warning(u'There\'s no description to write.')
779             except (OSError, IOError):
780                 self.report_error(u'Cannot write description file ' + descfn)
781                 return
782
783         if self.params.get('writeannotations', False):
784             try:
785                 annofn = filename + u'.annotations.xml'
786                 self.report_writeannotations(annofn)
787                 with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
788                     annofile.write(info_dict['annotations'])
789             except (KeyError, TypeError):
790                 self.report_warning(u'There are no annotations to write.')
791             except (OSError, IOError):
792                 self.report_error(u'Cannot write annotations file: ' + annofn)
793                 return
794
795         subtitles_are_requested = any([self.params.get('writesubtitles', False),
796                                        self.params.get('writeautomaticsub')])
797
798         if subtitles_are_requested and 'subtitles' in info_dict and info_dict['subtitles']:
799             # subtitles download errors are already managed as troubles in relevant IE
800             # that way it will silently go on when used with unsupporting IE
801             subtitles = info_dict['subtitles']
802             sub_format = self.params.get('subtitlesformat', 'srt')
803             for sub_lang in subtitles.keys():
804                 sub = subtitles[sub_lang]
805                 if sub is None:
806                     continue
807                 try:
808                     sub_filename = subtitles_filename(filename, sub_lang, sub_format)
809                     self.report_writesubtitles(sub_filename)
810                     with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
811                             subfile.write(sub)
812                 except (OSError, IOError):
813                     self.report_error(u'Cannot write subtitles file ' + descfn)
814                     return
815
816         if self.params.get('writeinfojson', False):
817             infofn = os.path.splitext(filename)[0] + u'.info.json'
818             self.report_writeinfojson(infofn)
819             try:
820                 json_info_dict = dict((k, v) for k, v in info_dict.items() if not k in ['urlhandle'])
821                 write_json_file(json_info_dict, encodeFilename(infofn))
822             except (OSError, IOError):
823                 self.report_error(u'Cannot write metadata to JSON file ' + infofn)
824                 return
825
826         if self.params.get('writethumbnail', False):
827             if info_dict.get('thumbnail') is not None:
828                 thumb_format = determine_ext(info_dict['thumbnail'], u'jpg')
829                 thumb_filename = filename.rpartition('.')[0] + u'.' + thumb_format
830                 self.to_screen(u'[%s] %s: Downloading thumbnail ...' %
831                                (info_dict['extractor'], info_dict['id']))
832                 try:
833                     uf = compat_urllib_request.urlopen(info_dict['thumbnail'])
834                     with open(thumb_filename, 'wb') as thumbf:
835                         shutil.copyfileobj(uf, thumbf)
836                     self.to_screen(u'[%s] %s: Writing thumbnail to: %s' %
837                         (info_dict['extractor'], info_dict['id'], thumb_filename))
838                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
839                     self.report_warning(u'Unable to download thumbnail "%s": %s' %
840                         (info_dict['thumbnail'], compat_str(err)))
841
842         if not self.params.get('skip_download', False):
843             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
844                 success = True
845             else:
846                 try:
847                     success = self.fd._do_download(filename, info_dict)
848                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
849                     self.report_error(u'unable to download video data: %s' % str(err))
850                     return
851                 except (OSError, IOError) as err:
852                     raise UnavailableVideoError(err)
853                 except (ContentTooShortError, ) as err:
854                     self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
855                     return
856
857             if success:
858                 try:
859                     self.post_process(filename, info_dict)
860                 except (PostProcessingError) as err:
861                     self.report_error(u'postprocessing: %s' % str(err))
862                     return
863
864         self.record_download_archive(info_dict)
865
866     def download(self, url_list):
867         """Download a given list of URLs."""
868         if (len(url_list) > 1 and
869                 '%' not in self.params['outtmpl']
870                 and self.params.get('max_downloads') != 1):
871             raise SameFileError(self.params['outtmpl'])
872
873         for url in url_list:
874             try:
875                 #It also downloads the videos
876                 self.extract_info(url)
877             except UnavailableVideoError:
878                 self.report_error(u'unable to download video')
879             except MaxDownloadsReached:
880                 self.to_screen(u'[info] Maximum number of downloaded files reached.')
881                 raise
882
883         return self._download_retcode
884
885     def download_with_info_file(self, info_filename):
886         with io.open(info_filename, 'r', encoding='utf-8') as f:
887             info = json.load(f)
888         try:
889             self.process_ie_result(info, download=True)
890         except DownloadError:
891             webpage_url = info.get('webpage_url')
892             if webpage_url is not None:
893                 self.report_warning(u'The info failed to download, trying with "%s"' % webpage_url)
894                 return self.download([webpage_url])
895             else:
896                 raise
897         return self._download_retcode
898
899     def post_process(self, filename, ie_info):
900         """Run all the postprocessors on the given file."""
901         info = dict(ie_info)
902         info['filepath'] = filename
903         keep_video = None
904         for pp in self._pps:
905             try:
906                 keep_video_wish, new_info = pp.run(info)
907                 if keep_video_wish is not None:
908                     if keep_video_wish:
909                         keep_video = keep_video_wish
910                     elif keep_video is None:
911                         # No clear decision yet, let IE decide
912                         keep_video = keep_video_wish
913             except PostProcessingError as e:
914                 self.report_error(e.msg)
915         if keep_video is False and not self.params.get('keepvideo', False):
916             try:
917                 self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)
918                 os.remove(encodeFilename(filename))
919             except (IOError, OSError):
920                 self.report_warning(u'Unable to remove downloaded video file')
921
922     def _make_archive_id(self, info_dict):
923         # Future-proof against any change in case
924         # and backwards compatibility with prior versions
925         extractor = info_dict.get('extractor_key')
926         if extractor is None:
927             if 'id' in info_dict:
928                 extractor = info_dict.get('ie_key')  # key in a playlist
929         if extractor is None:
930             return None  # Incomplete video information
931         return extractor.lower() + u' ' + info_dict['id']
932
933     def in_download_archive(self, info_dict):
934         fn = self.params.get('download_archive')
935         if fn is None:
936             return False
937
938         vid_id = self._make_archive_id(info_dict)
939         if vid_id is None:
940             return False  # Incomplete video information
941
942         try:
943             with locked_file(fn, 'r', encoding='utf-8') as archive_file:
944                 for line in archive_file:
945                     if line.strip() == vid_id:
946                         return True
947         except IOError as ioe:
948             if ioe.errno != errno.ENOENT:
949                 raise
950         return False
951
952     def record_download_archive(self, info_dict):
953         fn = self.params.get('download_archive')
954         if fn is None:
955             return
956         vid_id = self._make_archive_id(info_dict)
957         assert vid_id
958         with locked_file(fn, 'a', encoding='utf-8') as archive_file:
959             archive_file.write(vid_id + u'\n')
960
961     @staticmethod
962     def format_resolution(format, default='unknown'):
963         if format.get('vcodec') == 'none':
964             return 'audio only'
965         if format.get('_resolution') is not None:
966             return format['_resolution']
967         if format.get('height') is not None:
968             if format.get('width') is not None:
969                 res = u'%sx%s' % (format['width'], format['height'])
970             else:
971                 res = u'%sp' % format['height']
972         else:
973             res = default
974         return res
975
976     def list_formats(self, info_dict):
977         def format_note(fdict):
978             res = u''
979             if fdict.get('format_note') is not None:
980                 res += fdict['format_note'] + u' '
981             if (fdict.get('vcodec') is not None and
982                     fdict.get('vcodec') != 'none'):
983                 res += u'%-5s' % fdict['vcodec']
984             elif fdict.get('vbr') is not None:
985                 res += u'video'
986             if fdict.get('vbr') is not None:
987                 res += u'@%4dk' % fdict['vbr']
988             if fdict.get('acodec') is not None:
989                 if res:
990                     res += u', '
991                 res += u'%-5s' % fdict['acodec']
992             elif fdict.get('abr') is not None:
993                 if res:
994                     res += u', '
995                 res += 'audio'
996             if fdict.get('abr') is not None:
997                 res += u'@%3dk' % fdict['abr']
998             if fdict.get('filesize') is not None:
999                 if res:
1000                     res += u', '
1001                 res += format_bytes(fdict['filesize'])
1002             return res
1003
1004         def line(format, idlen=20):
1005             return ((u'%-' + compat_str(idlen + 1) + u's%-10s%-12s%s') % (
1006                 format['format_id'],
1007                 format['ext'],
1008                 self.format_resolution(format),
1009                 format_note(format),
1010             ))
1011
1012         formats = info_dict.get('formats', [info_dict])
1013         idlen = max(len(u'format code'),
1014                     max(len(f['format_id']) for f in formats))
1015         formats_s = [line(f, idlen) for f in formats]
1016         if len(formats) > 1:
1017             formats_s[0] += (' ' if format_note(formats[0]) else '') + '(worst)'
1018             formats_s[-1] += (' ' if format_note(formats[-1]) else '') + '(best)'
1019
1020         header_line = line({
1021             'format_id': u'format code', 'ext': u'extension',
1022             '_resolution': u'resolution', 'format_note': u'note'}, idlen=idlen)
1023         self.to_screen(u'[info] Available formats for %s:\n%s\n%s' %
1024                        (info_dict['id'], header_line, u"\n".join(formats_s)))
1025
1026     def urlopen(self, req):
1027         """ Start an HTTP download """
1028         return self._opener.open(req)
1029
1030     def print_debug_header(self):
1031         if not self.params.get('verbose'):
1032             return
1033         write_string(u'[debug] youtube-dl version ' + __version__ + u'\n')
1034         try:
1035             sp = subprocess.Popen(
1036                 ['git', 'rev-parse', '--short', 'HEAD'],
1037                 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1038                 cwd=os.path.dirname(os.path.abspath(__file__)))
1039             out, err = sp.communicate()
1040             out = out.decode().strip()
1041             if re.match('[0-9a-f]+', out):
1042                 write_string(u'[debug] Git HEAD: ' + out + u'\n')
1043         except:
1044             try:
1045                 sys.exc_clear()
1046             except:
1047                 pass
1048         write_string(u'[debug] Python version %s - %s' %
1049                      (platform.python_version(), platform_name()) + u'\n')
1050
1051         proxy_map = {}
1052         for handler in self._opener.handlers:
1053             if hasattr(handler, 'proxies'):
1054                 proxy_map.update(handler.proxies)
1055         write_string(u'[debug] Proxy map: ' + compat_str(proxy_map) + u'\n')
1056
1057     def _setup_opener(self):
1058         timeout_val = self.params.get('socket_timeout')
1059         timeout = 600 if timeout_val is None else float(timeout_val)
1060
1061         opts_cookiefile = self.params.get('cookiefile')
1062         opts_proxy = self.params.get('proxy')
1063
1064         if opts_cookiefile is None:
1065             self.cookiejar = compat_cookiejar.CookieJar()
1066         else:
1067             self.cookiejar = compat_cookiejar.MozillaCookieJar(
1068                 opts_cookiefile)
1069             if os.access(opts_cookiefile, os.R_OK):
1070                 self.cookiejar.load()
1071
1072         cookie_processor = compat_urllib_request.HTTPCookieProcessor(
1073             self.cookiejar)
1074         if opts_proxy is not None:
1075             if opts_proxy == '':
1076                 proxies = {}
1077             else:
1078                 proxies = {'http': opts_proxy, 'https': opts_proxy}
1079         else:
1080             proxies = compat_urllib_request.getproxies()
1081             # Set HTTPS proxy to HTTP one if given (https://github.com/rg3/youtube-dl/issues/805)
1082             if 'http' in proxies and 'https' not in proxies:
1083                 proxies['https'] = proxies['http']
1084         proxy_handler = compat_urllib_request.ProxyHandler(proxies)
1085         https_handler = make_HTTPS_handler(
1086             self.params.get('nocheckcertificate', False))
1087         opener = compat_urllib_request.build_opener(
1088             https_handler, proxy_handler, cookie_processor, YoutubeDLHandler())
1089         # Delete the default user-agent header, which would otherwise apply in
1090         # cases where our custom HTTP handler doesn't come into play
1091         # (See https://github.com/rg3/youtube-dl/issues/1309 for details)
1092         opener.addheaders = []
1093         self._opener = opener
1094
1095         # TODO remove this global modification
1096         compat_urllib_request.install_opener(opener)
1097         socket.setdefaulttimeout(timeout)