Merge remote-tracking branch 'lenaten/8tracks'
[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 datetime
8 import errno
9 import io
10 import itertools
11 import json
12 import locale
13 import os
14 import platform
15 import re
16 import shutil
17 import subprocess
18 import socket
19 import sys
20 import time
21 import traceback
22
23 if os.name == 'nt':
24     import ctypes
25
26 from .compat import (
27     compat_cookiejar,
28     compat_expanduser,
29     compat_http_client,
30     compat_kwargs,
31     compat_str,
32     compat_urllib_error,
33     compat_urllib_request,
34 )
35 from .utils import (
36     escape_url,
37     ContentTooShortError,
38     date_from_str,
39     DateRange,
40     DEFAULT_OUTTMPL,
41     determine_ext,
42     DownloadError,
43     encodeFilename,
44     ExtractorError,
45     format_bytes,
46     formatSeconds,
47     get_term_width,
48     locked_file,
49     make_HTTPS_handler,
50     MaxDownloadsReached,
51     PagedList,
52     PostProcessingError,
53     platform_name,
54     preferredencoding,
55     SameFileError,
56     sanitize_filename,
57     subtitles_filename,
58     takewhile_inclusive,
59     UnavailableVideoError,
60     url_basename,
61     write_json_file,
62     write_string,
63     YoutubeDLHandler,
64     prepend_extension,
65     args_to_str,
66     age_restricted,
67 )
68 from .cache import Cache
69 from .extractor import get_info_extractor, gen_extractors
70 from .downloader import get_suitable_downloader
71 from .downloader.rtmp import rtmpdump_version
72 from .postprocessor import (
73     FFmpegFixupStretchedPP,
74     FFmpegMergerPP,
75     FFmpegPostProcessor,
76     get_postprocessor,
77 )
78 from .version import __version__
79
80
81 class YoutubeDL(object):
82     """YoutubeDL class.
83
84     YoutubeDL objects are the ones responsible of downloading the
85     actual video file and writing it to disk if the user has requested
86     it, among some other tasks. In most cases there should be one per
87     program. As, given a video URL, the downloader doesn't know how to
88     extract all the needed information, task that InfoExtractors do, it
89     has to pass the URL to one of them.
90
91     For this, YoutubeDL objects have a method that allows
92     InfoExtractors to be registered in a given order. When it is passed
93     a URL, the YoutubeDL object handles it to the first InfoExtractor it
94     finds that reports being able to handle it. The InfoExtractor extracts
95     all the information about the video or videos the URL refers to, and
96     YoutubeDL process the extracted information, possibly using a File
97     Downloader to download the video.
98
99     YoutubeDL objects accept a lot of parameters. In order not to saturate
100     the object constructor with arguments, it receives a dictionary of
101     options instead. These options are available through the params
102     attribute for the InfoExtractors to use. The YoutubeDL also
103     registers itself as the downloader in charge for the InfoExtractors
104     that are added to it, so this is a "mutual registration".
105
106     Available options:
107
108     username:          Username for authentication purposes.
109     password:          Password for authentication purposes.
110     videopassword:     Password for acces a video.
111     usenetrc:          Use netrc for authentication instead.
112     verbose:           Print additional info to stdout.
113     quiet:             Do not print messages to stdout.
114     no_warnings:       Do not print out anything for warnings.
115     forceurl:          Force printing final URL.
116     forcetitle:        Force printing title.
117     forceid:           Force printing ID.
118     forcethumbnail:    Force printing thumbnail URL.
119     forcedescription:  Force printing description.
120     forcefilename:     Force printing final filename.
121     forceduration:     Force printing duration.
122     forcejson:         Force printing info_dict as JSON.
123     dump_single_json:  Force printing the info_dict of the whole playlist
124                        (or video) as a single JSON line.
125     simulate:          Do not download the video files.
126     format:            Video format code. See options.py for more information.
127     format_limit:      Highest quality format to try.
128     outtmpl:           Template for output names.
129     restrictfilenames: Do not allow "&" and spaces in file names
130     ignoreerrors:      Do not stop on download errors.
131     nooverwrites:      Prevent overwriting files.
132     playliststart:     Playlist item to start at.
133     playlistend:       Playlist item to end at.
134     playlistreverse:   Download playlist items in reverse order.
135     matchtitle:        Download only matching titles.
136     rejecttitle:       Reject downloads for matching titles.
137     logger:            Log messages to a logging.Logger instance.
138     logtostderr:       Log messages to stderr instead of stdout.
139     writedescription:  Write the video description to a .description file
140     writeinfojson:     Write the video description to a .info.json file
141     writeannotations:  Write the video annotations to a .annotations.xml file
142     writethumbnail:    Write the thumbnail image to a file
143     writesubtitles:    Write the video subtitles to a file
144     writeautomaticsub: Write the automatic subtitles to a file
145     allsubtitles:      Downloads all the subtitles of the video
146                        (requires writesubtitles or writeautomaticsub)
147     listsubtitles:     Lists all available subtitles for the video
148     subtitlesformat:   Subtitle format [srt/sbv/vtt] (default=srt)
149     subtitleslangs:    List of languages of the subtitles to download
150     keepvideo:         Keep the video file after post-processing
151     daterange:         A DateRange object, download only if the upload_date is in the range.
152     skip_download:     Skip the actual download of the video file
153     cachedir:          Location of the cache files in the filesystem.
154                        False to disable filesystem cache.
155     noplaylist:        Download single video instead of a playlist if in doubt.
156     age_limit:         An integer representing the user's age in years.
157                        Unsuitable videos for the given age are skipped.
158     min_views:         An integer representing the minimum view count the video
159                        must have in order to not be skipped.
160                        Videos without view count information are always
161                        downloaded. None for no limit.
162     max_views:         An integer representing the maximum view count.
163                        Videos that are more popular than that are not
164                        downloaded.
165                        Videos without view count information are always
166                        downloaded. None for no limit.
167     download_archive:  File name of a file where all downloads are recorded.
168                        Videos already present in the file are not downloaded
169                        again.
170     cookiefile:        File name where cookies should be read from and dumped to.
171     nocheckcertificate:Do not verify SSL certificates
172     prefer_insecure:   Use HTTP instead of HTTPS to retrieve information.
173                        At the moment, this is only supported by YouTube.
174     proxy:             URL of the proxy server to use
175     socket_timeout:    Time to wait for unresponsive hosts, in seconds
176     bidi_workaround:   Work around buggy terminals without bidirectional text
177                        support, using fridibi
178     debug_printtraffic:Print out sent and received HTTP traffic
179     include_ads:       Download ads as well
180     default_search:    Prepend this string if an input url is not valid.
181                        'auto' for elaborate guessing
182     encoding:          Use this encoding instead of the system-specified.
183     extract_flat:      Do not resolve URLs, return the immediate result.
184                        Pass in 'in_playlist' to only show this behavior for
185                        playlist items.
186     postprocessors:    A list of dictionaries, each with an entry
187                        * key:  The name of the postprocessor. See
188                                youtube_dl/postprocessor/__init__.py for a list.
189                        as well as any further keyword arguments for the
190                        postprocessor.
191     progress_hooks:    A list of functions that get called on download
192                        progress, with a dictionary with the entries
193                        * filename: The final filename
194                        * status: One of "downloading" and "finished"
195
196                        The dict may also have some of the following entries:
197
198                        * downloaded_bytes: Bytes on disk
199                        * total_bytes: Size of the whole file, None if unknown
200                        * tmpfilename: The filename we're currently writing to
201                        * eta: The estimated time in seconds, None if unknown
202                        * speed: The download speed in bytes/second, None if
203                                 unknown
204
205                        Progress hooks are guaranteed to be called at least once
206                        (with status "finished") if the download is successful.
207     merge_output_format: Extension to use when merging formats.
208     fixup:             Automatically correct known faults of the file.
209                        One of:
210                        - "never": do nothing
211                        - "warn": only emit a warning
212                        - "detect_or_warn": check whether we can do anything
213                                            about it, warn otherwise
214
215
216     The following parameters are not used by YoutubeDL itself, they are used by
217     the FileDownloader:
218     nopart, updatetime, buffersize, ratelimit, min_filesize, max_filesize, test,
219     noresizebuffer, retries, continuedl, noprogress, consoletitle
220
221     The following options are used by the post processors:
222     prefer_ffmpeg:     If True, use ffmpeg instead of avconv if both are available,
223                        otherwise prefer avconv.
224     exec_cmd:          Arbitrary command to run after downloading
225     """
226
227     params = None
228     _ies = []
229     _pps = []
230     _download_retcode = None
231     _num_downloads = None
232     _screen_file = None
233
234     def __init__(self, params=None, auto_init=True):
235         """Create a FileDownloader object with the given options."""
236         if params is None:
237             params = {}
238         self._ies = []
239         self._ies_instances = {}
240         self._pps = []
241         self._progress_hooks = []
242         self._download_retcode = 0
243         self._num_downloads = 0
244         self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
245         self._err_file = sys.stderr
246         self.params = params
247         self.cache = Cache(self)
248
249         if params.get('bidi_workaround', False):
250             try:
251                 import pty
252                 master, slave = pty.openpty()
253                 width = get_term_width()
254                 if width is None:
255                     width_args = []
256                 else:
257                     width_args = ['-w', str(width)]
258                 sp_kwargs = dict(
259                     stdin=subprocess.PIPE,
260                     stdout=slave,
261                     stderr=self._err_file)
262                 try:
263                     self._output_process = subprocess.Popen(
264                         ['bidiv'] + width_args, **sp_kwargs
265                     )
266                 except OSError:
267                     self._output_process = subprocess.Popen(
268                         ['fribidi', '-c', 'UTF-8'] + width_args, **sp_kwargs)
269                 self._output_channel = os.fdopen(master, 'rb')
270             except OSError as ose:
271                 if ose.errno == 2:
272                     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.')
273                 else:
274                     raise
275
276         if (sys.version_info >= (3,) and sys.platform != 'win32' and
277                 sys.getfilesystemencoding() in ['ascii', 'ANSI_X3.4-1968']
278                 and not params.get('restrictfilenames', False)):
279             # On Python 3, the Unicode filesystem API will throw errors (#1474)
280             self.report_warning(
281                 'Assuming --restrict-filenames since file system encoding '
282                 'cannot encode all characters. '
283                 'Set the LC_ALL environment variable to fix this.')
284             self.params['restrictfilenames'] = True
285
286         if '%(stitle)s' in self.params.get('outtmpl', ''):
287             self.report_warning('%(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
288
289         self._setup_opener()
290
291         if auto_init:
292             self.print_debug_header()
293             self.add_default_info_extractors()
294
295         for pp_def_raw in self.params.get('postprocessors', []):
296             pp_class = get_postprocessor(pp_def_raw['key'])
297             pp_def = dict(pp_def_raw)
298             del pp_def['key']
299             pp = pp_class(self, **compat_kwargs(pp_def))
300             self.add_post_processor(pp)
301
302         for ph in self.params.get('progress_hooks', []):
303             self.add_progress_hook(ph)
304
305     def warn_if_short_id(self, argv):
306         # short YouTube ID starting with dash?
307         idxs = [
308             i for i, a in enumerate(argv)
309             if re.match(r'^-[0-9A-Za-z_-]{10}$', a)]
310         if idxs:
311             correct_argv = (
312                 ['youtube-dl'] +
313                 [a for i, a in enumerate(argv) if i not in idxs] +
314                 ['--'] + [argv[i] for i in idxs]
315             )
316             self.report_warning(
317                 'Long argument string detected. '
318                 'Use -- to separate parameters and URLs, like this:\n%s\n' %
319                 args_to_str(correct_argv))
320
321     def add_info_extractor(self, ie):
322         """Add an InfoExtractor object to the end of the list."""
323         self._ies.append(ie)
324         self._ies_instances[ie.ie_key()] = ie
325         ie.set_downloader(self)
326
327     def get_info_extractor(self, ie_key):
328         """
329         Get an instance of an IE with name ie_key, it will try to get one from
330         the _ies list, if there's no instance it will create a new one and add
331         it to the extractor list.
332         """
333         ie = self._ies_instances.get(ie_key)
334         if ie is None:
335             ie = get_info_extractor(ie_key)()
336             self.add_info_extractor(ie)
337         return ie
338
339     def add_default_info_extractors(self):
340         """
341         Add the InfoExtractors returned by gen_extractors to the end of the list
342         """
343         for ie in gen_extractors():
344             self.add_info_extractor(ie)
345
346     def add_post_processor(self, pp):
347         """Add a PostProcessor object to the end of the chain."""
348         self._pps.append(pp)
349         pp.set_downloader(self)
350
351     def add_progress_hook(self, ph):
352         """Add the progress hook (currently only for the file downloader)"""
353         self._progress_hooks.append(ph)
354
355     def _bidi_workaround(self, message):
356         if not hasattr(self, '_output_channel'):
357             return message
358
359         assert hasattr(self, '_output_process')
360         assert isinstance(message, compat_str)
361         line_count = message.count('\n') + 1
362         self._output_process.stdin.write((message + '\n').encode('utf-8'))
363         self._output_process.stdin.flush()
364         res = ''.join(self._output_channel.readline().decode('utf-8')
365                       for _ in range(line_count))
366         return res[:-len('\n')]
367
368     def to_screen(self, message, skip_eol=False):
369         """Print message to stdout if not in quiet mode."""
370         return self.to_stdout(message, skip_eol, check_quiet=True)
371
372     def _write_string(self, s, out=None):
373         write_string(s, out=out, encoding=self.params.get('encoding'))
374
375     def to_stdout(self, message, skip_eol=False, check_quiet=False):
376         """Print message to stdout if not in quiet mode."""
377         if self.params.get('logger'):
378             self.params['logger'].debug(message)
379         elif not check_quiet or not self.params.get('quiet', False):
380             message = self._bidi_workaround(message)
381             terminator = ['\n', ''][skip_eol]
382             output = message + terminator
383
384             self._write_string(output, self._screen_file)
385
386     def to_stderr(self, message):
387         """Print message to stderr."""
388         assert isinstance(message, compat_str)
389         if self.params.get('logger'):
390             self.params['logger'].error(message)
391         else:
392             message = self._bidi_workaround(message)
393             output = message + '\n'
394             self._write_string(output, self._err_file)
395
396     def to_console_title(self, message):
397         if not self.params.get('consoletitle', False):
398             return
399         if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
400             # c_wchar_p() might not be necessary if `message` is
401             # already of type unicode()
402             ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
403         elif 'TERM' in os.environ:
404             self._write_string('\033]0;%s\007' % message, self._screen_file)
405
406     def save_console_title(self):
407         if not self.params.get('consoletitle', False):
408             return
409         if 'TERM' in os.environ:
410             # Save the title on stack
411             self._write_string('\033[22;0t', self._screen_file)
412
413     def restore_console_title(self):
414         if not self.params.get('consoletitle', False):
415             return
416         if 'TERM' in os.environ:
417             # Restore the title from stack
418             self._write_string('\033[23;0t', self._screen_file)
419
420     def __enter__(self):
421         self.save_console_title()
422         return self
423
424     def __exit__(self, *args):
425         self.restore_console_title()
426
427         if self.params.get('cookiefile') is not None:
428             self.cookiejar.save()
429
430     def trouble(self, message=None, tb=None):
431         """Determine action to take when a download problem appears.
432
433         Depending on if the downloader has been configured to ignore
434         download errors or not, this method may throw an exception or
435         not when errors are found, after printing the message.
436
437         tb, if given, is additional traceback information.
438         """
439         if message is not None:
440             self.to_stderr(message)
441         if self.params.get('verbose'):
442             if tb is None:
443                 if sys.exc_info()[0]:  # if .trouble has been called from an except block
444                     tb = ''
445                     if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
446                         tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
447                     tb += compat_str(traceback.format_exc())
448                 else:
449                     tb_data = traceback.format_list(traceback.extract_stack())
450                     tb = ''.join(tb_data)
451             self.to_stderr(tb)
452         if not self.params.get('ignoreerrors', False):
453             if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
454                 exc_info = sys.exc_info()[1].exc_info
455             else:
456                 exc_info = sys.exc_info()
457             raise DownloadError(message, exc_info)
458         self._download_retcode = 1
459
460     def report_warning(self, message):
461         '''
462         Print the message to stderr, it will be prefixed with 'WARNING:'
463         If stderr is a tty file the 'WARNING:' will be colored
464         '''
465         if self.params.get('logger') is not None:
466             self.params['logger'].warning(message)
467         else:
468             if self.params.get('no_warnings'):
469                 return
470             if self._err_file.isatty() and os.name != 'nt':
471                 _msg_header = '\033[0;33mWARNING:\033[0m'
472             else:
473                 _msg_header = 'WARNING:'
474             warning_message = '%s %s' % (_msg_header, message)
475             self.to_stderr(warning_message)
476
477     def report_error(self, message, tb=None):
478         '''
479         Do the same as trouble, but prefixes the message with 'ERROR:', colored
480         in red if stderr is a tty file.
481         '''
482         if self._err_file.isatty() and os.name != 'nt':
483             _msg_header = '\033[0;31mERROR:\033[0m'
484         else:
485             _msg_header = 'ERROR:'
486         error_message = '%s %s' % (_msg_header, message)
487         self.trouble(error_message, tb)
488
489     def report_file_already_downloaded(self, file_name):
490         """Report file has already been fully downloaded."""
491         try:
492             self.to_screen('[download] %s has already been downloaded' % file_name)
493         except UnicodeEncodeError:
494             self.to_screen('[download] The file has already been downloaded')
495
496     def prepare_filename(self, info_dict):
497         """Generate the output filename."""
498         try:
499             template_dict = dict(info_dict)
500
501             template_dict['epoch'] = int(time.time())
502             autonumber_size = self.params.get('autonumber_size')
503             if autonumber_size is None:
504                 autonumber_size = 5
505             autonumber_templ = '%0' + str(autonumber_size) + 'd'
506             template_dict['autonumber'] = autonumber_templ % self._num_downloads
507             if template_dict.get('playlist_index') is not None:
508                 template_dict['playlist_index'] = '%0*d' % (len(str(template_dict['n_entries'])), template_dict['playlist_index'])
509             if template_dict.get('resolution') is None:
510                 if template_dict.get('width') and template_dict.get('height'):
511                     template_dict['resolution'] = '%dx%d' % (template_dict['width'], template_dict['height'])
512                 elif template_dict.get('height'):
513                     template_dict['resolution'] = '%sp' % template_dict['height']
514                 elif template_dict.get('width'):
515                     template_dict['resolution'] = '?x%d' % template_dict['width']
516
517             sanitize = lambda k, v: sanitize_filename(
518                 compat_str(v),
519                 restricted=self.params.get('restrictfilenames'),
520                 is_id=(k == 'id'))
521             template_dict = dict((k, sanitize(k, v))
522                                  for k, v in template_dict.items()
523                                  if v is not None)
524             template_dict = collections.defaultdict(lambda: 'NA', template_dict)
525
526             outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
527             tmpl = compat_expanduser(outtmpl)
528             filename = tmpl % template_dict
529             return filename
530         except ValueError as err:
531             self.report_error('Error in output template: ' + str(err) + ' (encoding: ' + repr(preferredencoding()) + ')')
532             return None
533
534     def _match_entry(self, info_dict):
535         """ Returns None iff the file should be downloaded """
536
537         video_title = info_dict.get('title', info_dict.get('id', 'video'))
538         if 'title' in info_dict:
539             # This can happen when we're just evaluating the playlist
540             title = info_dict['title']
541             matchtitle = self.params.get('matchtitle', False)
542             if matchtitle:
543                 if not re.search(matchtitle, title, re.IGNORECASE):
544                     return '"' + title + '" title did not match pattern "' + matchtitle + '"'
545             rejecttitle = self.params.get('rejecttitle', False)
546             if rejecttitle:
547                 if re.search(rejecttitle, title, re.IGNORECASE):
548                     return '"' + title + '" title matched reject pattern "' + rejecttitle + '"'
549         date = info_dict.get('upload_date', None)
550         if date is not None:
551             dateRange = self.params.get('daterange', DateRange())
552             if date not in dateRange:
553                 return '%s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
554         view_count = info_dict.get('view_count', None)
555         if view_count is not None:
556             min_views = self.params.get('min_views')
557             if min_views is not None and view_count < min_views:
558                 return 'Skipping %s, because it has not reached minimum view count (%d/%d)' % (video_title, view_count, min_views)
559             max_views = self.params.get('max_views')
560             if max_views is not None and view_count > max_views:
561                 return 'Skipping %s, because it has exceeded the maximum view count (%d/%d)' % (video_title, view_count, max_views)
562         if age_restricted(info_dict.get('age_limit'), self.params.get('age_limit')):
563             return 'Skipping "%s" because it is age restricted' % title
564         if self.in_download_archive(info_dict):
565             return '%s has already been recorded in archive' % video_title
566         return None
567
568     @staticmethod
569     def add_extra_info(info_dict, extra_info):
570         '''Set the keys from extra_info in info dict if they are missing'''
571         for key, value in extra_info.items():
572             info_dict.setdefault(key, value)
573
574     def extract_info(self, url, download=True, ie_key=None, extra_info={},
575                      process=True):
576         '''
577         Returns a list with a dictionary for each video we find.
578         If 'download', also downloads the videos.
579         extra_info is a dict containing the extra values to add to each result
580          '''
581
582         if ie_key:
583             ies = [self.get_info_extractor(ie_key)]
584         else:
585             ies = self._ies
586
587         for ie in ies:
588             if not ie.suitable(url):
589                 continue
590
591             if not ie.working():
592                 self.report_warning('The program functionality for this site has been marked as broken, '
593                                     'and will probably not work.')
594
595             try:
596                 ie_result = ie.extract(url)
597                 if ie_result is None:  # Finished already (backwards compatibility; listformats and friends should be moved here)
598                     break
599                 if isinstance(ie_result, list):
600                     # Backwards compatibility: old IE result format
601                     ie_result = {
602                         '_type': 'compat_list',
603                         'entries': ie_result,
604                     }
605                 self.add_default_extra_info(ie_result, ie, url)
606                 if process:
607                     return self.process_ie_result(ie_result, download, extra_info)
608                 else:
609                     return ie_result
610             except ExtractorError as de:  # An error we somewhat expected
611                 self.report_error(compat_str(de), de.format_traceback())
612                 break
613             except MaxDownloadsReached:
614                 raise
615             except Exception as e:
616                 if self.params.get('ignoreerrors', False):
617                     self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
618                     break
619                 else:
620                     raise
621         else:
622             self.report_error('no suitable InfoExtractor for URL %s' % url)
623
624     def add_default_extra_info(self, ie_result, ie, url):
625         self.add_extra_info(ie_result, {
626             'extractor': ie.IE_NAME,
627             'webpage_url': url,
628             'webpage_url_basename': url_basename(url),
629             'extractor_key': ie.ie_key(),
630         })
631
632     def process_ie_result(self, ie_result, download=True, extra_info={}):
633         """
634         Take the result of the ie(may be modified) and resolve all unresolved
635         references (URLs, playlist items).
636
637         It will also download the videos if 'download'.
638         Returns the resolved ie_result.
639         """
640
641         result_type = ie_result.get('_type', 'video')
642
643         if result_type in ('url', 'url_transparent'):
644             extract_flat = self.params.get('extract_flat', False)
645             if ((extract_flat == 'in_playlist' and 'playlist' in extra_info) or
646                     extract_flat is True):
647                 if self.params.get('forcejson', False):
648                     self.to_stdout(json.dumps(ie_result))
649                 return ie_result
650
651         if result_type == 'video':
652             self.add_extra_info(ie_result, extra_info)
653             return self.process_video_result(ie_result, download=download)
654         elif result_type == 'url':
655             # We have to add extra_info to the results because it may be
656             # contained in a playlist
657             return self.extract_info(ie_result['url'],
658                                      download,
659                                      ie_key=ie_result.get('ie_key'),
660                                      extra_info=extra_info)
661         elif result_type == 'url_transparent':
662             # Use the information from the embedding page
663             info = self.extract_info(
664                 ie_result['url'], ie_key=ie_result.get('ie_key'),
665                 extra_info=extra_info, download=False, process=False)
666
667             force_properties = dict(
668                 (k, v) for k, v in ie_result.items() if v is not None)
669             for f in ('_type', 'url'):
670                 if f in force_properties:
671                     del force_properties[f]
672             new_result = info.copy()
673             new_result.update(force_properties)
674
675             assert new_result.get('_type') != 'url_transparent'
676
677             return self.process_ie_result(
678                 new_result, download=download, extra_info=extra_info)
679         elif result_type == 'playlist' or result_type == 'multi_video':
680             # We process each entry in the playlist
681             playlist = ie_result.get('title', None) or ie_result.get('id', None)
682             self.to_screen('[download] Downloading playlist: %s' % playlist)
683
684             playlist_results = []
685
686             playliststart = self.params.get('playliststart', 1) - 1
687             playlistend = self.params.get('playlistend', None)
688             # For backwards compatibility, interpret -1 as whole list
689             if playlistend == -1:
690                 playlistend = None
691
692             ie_entries = ie_result['entries']
693             if isinstance(ie_entries, list):
694                 n_all_entries = len(ie_entries)
695                 entries = ie_entries[playliststart:playlistend]
696                 n_entries = len(entries)
697                 self.to_screen(
698                     "[%s] playlist %s: Collected %d video ids (downloading %d of them)" %
699                     (ie_result['extractor'], playlist, n_all_entries, n_entries))
700             elif isinstance(ie_entries, PagedList):
701                 entries = ie_entries.getslice(
702                     playliststart, playlistend)
703                 n_entries = len(entries)
704                 self.to_screen(
705                     "[%s] playlist %s: Downloading %d videos" %
706                     (ie_result['extractor'], playlist, n_entries))
707             else:  # iterable
708                 entries = list(itertools.islice(
709                     ie_entries, playliststart, playlistend))
710                 n_entries = len(entries)
711                 self.to_screen(
712                     "[%s] playlist %s: Downloading %d videos" %
713                     (ie_result['extractor'], playlist, n_entries))
714
715             if self.params.get('playlistreverse', False):
716                 entries = entries[::-1]
717
718             for i, entry in enumerate(entries, 1):
719                 self.to_screen('[download] Downloading video %s of %s' % (i, n_entries))
720                 extra = {
721                     'n_entries': n_entries,
722                     'playlist': playlist,
723                     'playlist_id': ie_result.get('id'),
724                     'playlist_title': ie_result.get('title'),
725                     'playlist_index': i + playliststart,
726                     'extractor': ie_result['extractor'],
727                     'webpage_url': ie_result['webpage_url'],
728                     'webpage_url_basename': url_basename(ie_result['webpage_url']),
729                     'extractor_key': ie_result['extractor_key'],
730                 }
731
732                 reason = self._match_entry(entry)
733                 if reason is not None:
734                     self.to_screen('[download] ' + reason)
735                     continue
736
737                 entry_result = self.process_ie_result(entry,
738                                                       download=download,
739                                                       extra_info=extra)
740                 playlist_results.append(entry_result)
741             ie_result['entries'] = playlist_results
742             return ie_result
743         elif result_type == 'compat_list':
744             self.report_warning(
745                 'Extractor %s returned a compat_list result. '
746                 'It needs to be updated.' % ie_result.get('extractor'))
747
748             def _fixup(r):
749                 self.add_extra_info(
750                     r,
751                     {
752                         'extractor': ie_result['extractor'],
753                         'webpage_url': ie_result['webpage_url'],
754                         'webpage_url_basename': url_basename(ie_result['webpage_url']),
755                         'extractor_key': ie_result['extractor_key'],
756                     }
757                 )
758                 return r
759             ie_result['entries'] = [
760                 self.process_ie_result(_fixup(r), download, extra_info)
761                 for r in ie_result['entries']
762             ]
763             return ie_result
764         else:
765             raise Exception('Invalid result type: %s' % result_type)
766
767     def select_format(self, format_spec, available_formats):
768         if format_spec == 'best' or format_spec is None:
769             return available_formats[-1]
770         elif format_spec == 'worst':
771             return available_formats[0]
772         elif format_spec == 'bestaudio':
773             audio_formats = [
774                 f for f in available_formats
775                 if f.get('vcodec') == 'none']
776             if audio_formats:
777                 return audio_formats[-1]
778         elif format_spec == 'worstaudio':
779             audio_formats = [
780                 f for f in available_formats
781                 if f.get('vcodec') == 'none']
782             if audio_formats:
783                 return audio_formats[0]
784         elif format_spec == 'bestvideo':
785             video_formats = [
786                 f for f in available_formats
787                 if f.get('acodec') == 'none']
788             if video_formats:
789                 return video_formats[-1]
790         elif format_spec == 'worstvideo':
791             video_formats = [
792                 f for f in available_formats
793                 if f.get('acodec') == 'none']
794             if video_formats:
795                 return video_formats[0]
796         else:
797             extensions = ['mp4', 'flv', 'webm', '3gp', 'm4a', 'mp3', 'ogg', 'aac', 'wav']
798             if format_spec in extensions:
799                 filter_f = lambda f: f['ext'] == format_spec
800             else:
801                 filter_f = lambda f: f['format_id'] == format_spec
802             matches = list(filter(filter_f, available_formats))
803             if matches:
804                 return matches[-1]
805         return None
806
807     def process_video_result(self, info_dict, download=True):
808         assert info_dict.get('_type', 'video') == 'video'
809
810         if 'id' not in info_dict:
811             raise ExtractorError('Missing "id" field in extractor result')
812         if 'title' not in info_dict:
813             raise ExtractorError('Missing "title" field in extractor result')
814
815         if 'playlist' not in info_dict:
816             # It isn't part of a playlist
817             info_dict['playlist'] = None
818             info_dict['playlist_index'] = None
819
820         thumbnails = info_dict.get('thumbnails')
821         if thumbnails:
822             thumbnails.sort(key=lambda t: (
823                 t.get('width'), t.get('height'), t.get('url')))
824             for t in thumbnails:
825                 if 'width' in t and 'height' in t:
826                     t['resolution'] = '%dx%d' % (t['width'], t['height'])
827
828         if thumbnails and 'thumbnail' not in info_dict:
829             info_dict['thumbnail'] = thumbnails[-1]['url']
830
831         if 'display_id' not in info_dict and 'id' in info_dict:
832             info_dict['display_id'] = info_dict['id']
833
834         if info_dict.get('upload_date') is None and info_dict.get('timestamp') is not None:
835             # Working around negative timestamps in Windows
836             # (see http://bugs.python.org/issue1646728)
837             if info_dict['timestamp'] < 0 and os.name == 'nt':
838                 info_dict['timestamp'] = 0
839             upload_date = datetime.datetime.utcfromtimestamp(
840                 info_dict['timestamp'])
841             info_dict['upload_date'] = upload_date.strftime('%Y%m%d')
842
843         # This extractors handle format selection themselves
844         if info_dict['extractor'] in ['Youku']:
845             if download:
846                 self.process_info(info_dict)
847             return info_dict
848
849         # We now pick which formats have to be downloaded
850         if info_dict.get('formats') is None:
851             # There's only one format available
852             formats = [info_dict]
853         else:
854             formats = info_dict['formats']
855
856         if not formats:
857             raise ExtractorError('No video formats found!')
858
859         # We check that all the formats have the format and format_id fields
860         for i, format in enumerate(formats):
861             if 'url' not in format:
862                 raise ExtractorError('Missing "url" key in result (index %d)' % i)
863
864             if format.get('format_id') is None:
865                 format['format_id'] = compat_str(i)
866             if format.get('format') is None:
867                 format['format'] = '{id} - {res}{note}'.format(
868                     id=format['format_id'],
869                     res=self.format_resolution(format),
870                     note=' ({0})'.format(format['format_note']) if format.get('format_note') is not None else '',
871                 )
872             # Automatically determine file extension if missing
873             if 'ext' not in format:
874                 format['ext'] = determine_ext(format['url']).lower()
875
876         format_limit = self.params.get('format_limit', None)
877         if format_limit:
878             formats = list(takewhile_inclusive(
879                 lambda f: f['format_id'] != format_limit, formats
880             ))
881
882         # TODO Central sorting goes here
883
884         if formats[0] is not info_dict:
885             # only set the 'formats' fields if the original info_dict list them
886             # otherwise we end up with a circular reference, the first (and unique)
887             # element in the 'formats' field in info_dict is info_dict itself,
888             # wich can't be exported to json
889             info_dict['formats'] = formats
890         if self.params.get('listformats', None):
891             self.list_formats(info_dict)
892             return
893
894         req_format = self.params.get('format')
895         if req_format is None:
896             req_format = 'best'
897         formats_to_download = []
898         # The -1 is for supporting YoutubeIE
899         if req_format in ('-1', 'all'):
900             formats_to_download = formats
901         else:
902             for rfstr in req_format.split(','):
903                 # We can accept formats requested in the format: 34/5/best, we pick
904                 # the first that is available, starting from left
905                 req_formats = rfstr.split('/')
906                 for rf in req_formats:
907                     if re.match(r'.+?\+.+?', rf) is not None:
908                         # Two formats have been requested like '137+139'
909                         format_1, format_2 = rf.split('+')
910                         formats_info = (self.select_format(format_1, formats),
911                                         self.select_format(format_2, formats))
912                         if all(formats_info):
913                             # The first format must contain the video and the
914                             # second the audio
915                             if formats_info[0].get('vcodec') == 'none':
916                                 self.report_error('The first format must '
917                                                   'contain the video, try using '
918                                                   '"-f %s+%s"' % (format_2, format_1))
919                                 return
920                             output_ext = (
921                                 formats_info[0]['ext']
922                                 if self.params.get('merge_output_format') is None
923                                 else self.params['merge_output_format'])
924                             selected_format = {
925                                 'requested_formats': formats_info,
926                                 'format': rf,
927                                 'ext': formats_info[0]['ext'],
928                                 'width': formats_info[0].get('width'),
929                                 'height': formats_info[0].get('height'),
930                                 'resolution': formats_info[0].get('resolution'),
931                                 'fps': formats_info[0].get('fps'),
932                                 'vcodec': formats_info[0].get('vcodec'),
933                                 'vbr': formats_info[0].get('vbr'),
934                                 'stretched_ratio': formats_info[0].get('stretched_ratio'),
935                                 'acodec': formats_info[1].get('acodec'),
936                                 'abr': formats_info[1].get('abr'),
937                                 'ext': output_ext,
938                             }
939                         else:
940                             selected_format = None
941                     else:
942                         selected_format = self.select_format(rf, formats)
943                     if selected_format is not None:
944                         formats_to_download.append(selected_format)
945                         break
946         if not formats_to_download:
947             raise ExtractorError('requested format not available',
948                                  expected=True)
949
950         if download:
951             if len(formats_to_download) > 1:
952                 self.to_screen('[info] %s: downloading video in %s formats' % (info_dict['id'], len(formats_to_download)))
953             for format in formats_to_download:
954                 new_info = dict(info_dict)
955                 new_info.update(format)
956                 self.process_info(new_info)
957         # We update the info dict with the best quality format (backwards compatibility)
958         info_dict.update(formats_to_download[-1])
959         return info_dict
960
961     def process_info(self, info_dict):
962         """Process a single resolved IE result."""
963
964         assert info_dict.get('_type', 'video') == 'video'
965
966         max_downloads = self.params.get('max_downloads')
967         if max_downloads is not None:
968             if self._num_downloads >= int(max_downloads):
969                 raise MaxDownloadsReached()
970
971         info_dict['fulltitle'] = info_dict['title']
972         if len(info_dict['title']) > 200:
973             info_dict['title'] = info_dict['title'][:197] + '...'
974
975         # Keep for backwards compatibility
976         info_dict['stitle'] = info_dict['title']
977
978         if 'format' not in info_dict:
979             info_dict['format'] = info_dict['ext']
980
981         reason = self._match_entry(info_dict)
982         if reason is not None:
983             self.to_screen('[download] ' + reason)
984             return
985
986         self._num_downloads += 1
987
988         filename = self.prepare_filename(info_dict)
989
990         # Forced printings
991         if self.params.get('forcetitle', False):
992             self.to_stdout(info_dict['fulltitle'])
993         if self.params.get('forceid', False):
994             self.to_stdout(info_dict['id'])
995         if self.params.get('forceurl', False):
996             if info_dict.get('requested_formats') is not None:
997                 for f in info_dict['requested_formats']:
998                     self.to_stdout(f['url'] + f.get('play_path', ''))
999             else:
1000                 # For RTMP URLs, also include the playpath
1001                 self.to_stdout(info_dict['url'] + info_dict.get('play_path', ''))
1002         if self.params.get('forcethumbnail', False) and info_dict.get('thumbnail') is not None:
1003             self.to_stdout(info_dict['thumbnail'])
1004         if self.params.get('forcedescription', False) and info_dict.get('description') is not None:
1005             self.to_stdout(info_dict['description'])
1006         if self.params.get('forcefilename', False) and filename is not None:
1007             self.to_stdout(filename)
1008         if self.params.get('forceduration', False) and info_dict.get('duration') is not None:
1009             self.to_stdout(formatSeconds(info_dict['duration']))
1010         if self.params.get('forceformat', False):
1011             self.to_stdout(info_dict['format'])
1012         if self.params.get('forcejson', False):
1013             info_dict['_filename'] = filename
1014             self.to_stdout(json.dumps(info_dict))
1015         if self.params.get('dump_single_json', False):
1016             info_dict['_filename'] = filename
1017
1018         # Do nothing else if in simulate mode
1019         if self.params.get('simulate', False):
1020             return
1021
1022         if filename is None:
1023             return
1024
1025         try:
1026             dn = os.path.dirname(encodeFilename(filename))
1027             if dn and not os.path.exists(dn):
1028                 os.makedirs(dn)
1029         except (OSError, IOError) as err:
1030             self.report_error('unable to create directory ' + compat_str(err))
1031             return
1032
1033         if self.params.get('writedescription', False):
1034             descfn = filename + '.description'
1035             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(descfn)):
1036                 self.to_screen('[info] Video description is already present')
1037             elif info_dict.get('description') is None:
1038                 self.report_warning('There\'s no description to write.')
1039             else:
1040                 try:
1041                     self.to_screen('[info] Writing video description to: ' + descfn)
1042                     with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
1043                         descfile.write(info_dict['description'])
1044                 except (OSError, IOError):
1045                     self.report_error('Cannot write description file ' + descfn)
1046                     return
1047
1048         if self.params.get('writeannotations', False):
1049             annofn = filename + '.annotations.xml'
1050             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(annofn)):
1051                 self.to_screen('[info] Video annotations are already present')
1052             else:
1053                 try:
1054                     self.to_screen('[info] Writing video annotations to: ' + annofn)
1055                     with io.open(encodeFilename(annofn), 'w', encoding='utf-8') as annofile:
1056                         annofile.write(info_dict['annotations'])
1057                 except (KeyError, TypeError):
1058                     self.report_warning('There are no annotations to write.')
1059                 except (OSError, IOError):
1060                     self.report_error('Cannot write annotations file: ' + annofn)
1061                     return
1062
1063         subtitles_are_requested = any([self.params.get('writesubtitles', False),
1064                                        self.params.get('writeautomaticsub')])
1065
1066         if subtitles_are_requested and 'subtitles' in info_dict and info_dict['subtitles']:
1067             # subtitles download errors are already managed as troubles in relevant IE
1068             # that way it will silently go on when used with unsupporting IE
1069             subtitles = info_dict['subtitles']
1070             sub_format = self.params.get('subtitlesformat', 'srt')
1071             for sub_lang in subtitles.keys():
1072                 sub = subtitles[sub_lang]
1073                 if sub is None:
1074                     continue
1075                 try:
1076                     sub_filename = subtitles_filename(filename, sub_lang, sub_format)
1077                     if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(sub_filename)):
1078                         self.to_screen('[info] Video subtitle %s.%s is already_present' % (sub_lang, sub_format))
1079                     else:
1080                         self.to_screen('[info] Writing video subtitles to: ' + sub_filename)
1081                         with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
1082                             subfile.write(sub)
1083                 except (OSError, IOError):
1084                     self.report_error('Cannot write subtitles file ' + sub_filename)
1085                     return
1086
1087         if self.params.get('writeinfojson', False):
1088             infofn = os.path.splitext(filename)[0] + '.info.json'
1089             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(infofn)):
1090                 self.to_screen('[info] Video description metadata is already present')
1091             else:
1092                 self.to_screen('[info] Writing video description metadata as JSON to: ' + infofn)
1093                 try:
1094                     write_json_file(info_dict, infofn)
1095                 except (OSError, IOError):
1096                     self.report_error('Cannot write metadata to JSON file ' + infofn)
1097                     return
1098
1099         if self.params.get('writethumbnail', False):
1100             if info_dict.get('thumbnail') is not None:
1101                 thumb_format = determine_ext(info_dict['thumbnail'], 'jpg')
1102                 thumb_filename = os.path.splitext(filename)[0] + '.' + thumb_format
1103                 if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(thumb_filename)):
1104                     self.to_screen('[%s] %s: Thumbnail is already present' %
1105                                    (info_dict['extractor'], info_dict['id']))
1106                 else:
1107                     self.to_screen('[%s] %s: Downloading thumbnail ...' %
1108                                    (info_dict['extractor'], info_dict['id']))
1109                     try:
1110                         uf = self.urlopen(info_dict['thumbnail'])
1111                         with open(thumb_filename, 'wb') as thumbf:
1112                             shutil.copyfileobj(uf, thumbf)
1113                         self.to_screen('[%s] %s: Writing thumbnail to: %s' %
1114                                        (info_dict['extractor'], info_dict['id'], thumb_filename))
1115                     except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
1116                         self.report_warning('Unable to download thumbnail "%s": %s' %
1117                                             (info_dict['thumbnail'], compat_str(err)))
1118
1119         if not self.params.get('skip_download', False):
1120             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
1121                 success = True
1122             else:
1123                 try:
1124                     def dl(name, info):
1125                         fd = get_suitable_downloader(info)(self, self.params)
1126                         for ph in self._progress_hooks:
1127                             fd.add_progress_hook(ph)
1128                         if self.params.get('verbose'):
1129                             self.to_stdout('[debug] Invoking downloader on %r' % info.get('url'))
1130                         return fd.download(name, info)
1131                     if info_dict.get('requested_formats') is not None:
1132                         downloaded = []
1133                         success = True
1134                         merger = FFmpegMergerPP(self, not self.params.get('keepvideo'))
1135                         if not merger._executable:
1136                             postprocessors = []
1137                             self.report_warning('You have requested multiple '
1138                                                 'formats but ffmpeg or avconv are not installed.'
1139                                                 ' The formats won\'t be merged')
1140                         else:
1141                             postprocessors = [merger]
1142                         for f in info_dict['requested_formats']:
1143                             new_info = dict(info_dict)
1144                             new_info.update(f)
1145                             fname = self.prepare_filename(new_info)
1146                             fname = prepend_extension(fname, 'f%s' % f['format_id'])
1147                             downloaded.append(fname)
1148                             partial_success = dl(fname, new_info)
1149                             success = success and partial_success
1150                         info_dict['__postprocessors'] = postprocessors
1151                         info_dict['__files_to_merge'] = downloaded
1152                     else:
1153                         # Just a single file
1154                         success = dl(filename, info_dict)
1155                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
1156                     self.report_error('unable to download video data: %s' % str(err))
1157                     return
1158                 except (OSError, IOError) as err:
1159                     raise UnavailableVideoError(err)
1160                 except (ContentTooShortError, ) as err:
1161                     self.report_error('content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
1162                     return
1163
1164             if success:
1165                 # Fixup content
1166                 stretched_ratio = info_dict.get('stretched_ratio')
1167                 if stretched_ratio is not None and stretched_ratio != 1:
1168                     fixup_policy = self.params.get('fixup')
1169                     if fixup_policy is None:
1170                         fixup_policy = 'detect_or_warn'
1171                     if fixup_policy == 'warn':
1172                         self.report_warning('%s: Non-uniform pixel ratio (%s)' % (
1173                             info_dict['id'], stretched_ratio))
1174                     elif fixup_policy == 'detect_or_warn':
1175                         stretched_pp = FFmpegFixupStretchedPP(self)
1176                         if stretched_pp.available:
1177                             info_dict.setdefault('__postprocessors', [])
1178                             info_dict['__postprocessors'].append(stretched_pp)
1179                         else:
1180                             self.report_warning(
1181                                 '%s: Non-uniform pixel ratio (%s). Install ffmpeg or avconv to fix this automatically.' % (
1182                                     info_dict['id'], stretched_ratio))
1183                     else:
1184                         assert fixup_policy == 'ignore'
1185
1186                 try:
1187                     self.post_process(filename, info_dict)
1188                 except (PostProcessingError) as err:
1189                     self.report_error('postprocessing: %s' % str(err))
1190                     return
1191                 self.record_download_archive(info_dict)
1192
1193     def download(self, url_list):
1194         """Download a given list of URLs."""
1195         outtmpl = self.params.get('outtmpl', DEFAULT_OUTTMPL)
1196         if (len(url_list) > 1 and
1197                 '%' not in outtmpl
1198                 and self.params.get('max_downloads') != 1):
1199             raise SameFileError(outtmpl)
1200
1201         for url in url_list:
1202             try:
1203                 # It also downloads the videos
1204                 res = self.extract_info(url)
1205             except UnavailableVideoError:
1206                 self.report_error('unable to download video')
1207             except MaxDownloadsReached:
1208                 self.to_screen('[info] Maximum number of downloaded files reached.')
1209                 raise
1210             else:
1211                 if self.params.get('dump_single_json', False):
1212                     self.to_stdout(json.dumps(res))
1213
1214         return self._download_retcode
1215
1216     def download_with_info_file(self, info_filename):
1217         with io.open(info_filename, 'r', encoding='utf-8') as f:
1218             info = json.load(f)
1219         try:
1220             self.process_ie_result(info, download=True)
1221         except DownloadError:
1222             webpage_url = info.get('webpage_url')
1223             if webpage_url is not None:
1224                 self.report_warning('The info failed to download, trying with "%s"' % webpage_url)
1225                 return self.download([webpage_url])
1226             else:
1227                 raise
1228         return self._download_retcode
1229
1230     def post_process(self, filename, ie_info):
1231         """Run all the postprocessors on the given file."""
1232         info = dict(ie_info)
1233         info['filepath'] = filename
1234         keep_video = None
1235         pps_chain = []
1236         if ie_info.get('__postprocessors') is not None:
1237             pps_chain.extend(ie_info['__postprocessors'])
1238         pps_chain.extend(self._pps)
1239         for pp in pps_chain:
1240             try:
1241                 keep_video_wish, new_info = pp.run(info)
1242                 if keep_video_wish is not None:
1243                     if keep_video_wish:
1244                         keep_video = keep_video_wish
1245                     elif keep_video is None:
1246                         # No clear decision yet, let IE decide
1247                         keep_video = keep_video_wish
1248             except PostProcessingError as e:
1249                 self.report_error(e.msg)
1250         if keep_video is False and not self.params.get('keepvideo', False):
1251             try:
1252                 self.to_screen('Deleting original file %s (pass -k to keep)' % filename)
1253                 os.remove(encodeFilename(filename))
1254             except (IOError, OSError):
1255                 self.report_warning('Unable to remove downloaded video file')
1256
1257     def _make_archive_id(self, info_dict):
1258         # Future-proof against any change in case
1259         # and backwards compatibility with prior versions
1260         extractor = info_dict.get('extractor_key')
1261         if extractor is None:
1262             if 'id' in info_dict:
1263                 extractor = info_dict.get('ie_key')  # key in a playlist
1264         if extractor is None:
1265             return None  # Incomplete video information
1266         return extractor.lower() + ' ' + info_dict['id']
1267
1268     def in_download_archive(self, info_dict):
1269         fn = self.params.get('download_archive')
1270         if fn is None:
1271             return False
1272
1273         vid_id = self._make_archive_id(info_dict)
1274         if vid_id is None:
1275             return False  # Incomplete video information
1276
1277         try:
1278             with locked_file(fn, 'r', encoding='utf-8') as archive_file:
1279                 for line in archive_file:
1280                     if line.strip() == vid_id:
1281                         return True
1282         except IOError as ioe:
1283             if ioe.errno != errno.ENOENT:
1284                 raise
1285         return False
1286
1287     def record_download_archive(self, info_dict):
1288         fn = self.params.get('download_archive')
1289         if fn is None:
1290             return
1291         vid_id = self._make_archive_id(info_dict)
1292         assert vid_id
1293         with locked_file(fn, 'a', encoding='utf-8') as archive_file:
1294             archive_file.write(vid_id + '\n')
1295
1296     @staticmethod
1297     def format_resolution(format, default='unknown'):
1298         if format.get('vcodec') == 'none':
1299             return 'audio only'
1300         if format.get('resolution') is not None:
1301             return format['resolution']
1302         if format.get('height') is not None:
1303             if format.get('width') is not None:
1304                 res = '%sx%s' % (format['width'], format['height'])
1305             else:
1306                 res = '%sp' % format['height']
1307         elif format.get('width') is not None:
1308             res = '?x%d' % format['width']
1309         else:
1310             res = default
1311         return res
1312
1313     def _format_note(self, fdict):
1314         res = ''
1315         if fdict.get('ext') in ['f4f', 'f4m']:
1316             res += '(unsupported) '
1317         if fdict.get('format_note') is not None:
1318             res += fdict['format_note'] + ' '
1319         if fdict.get('tbr') is not None:
1320             res += '%4dk ' % fdict['tbr']
1321         if fdict.get('container') is not None:
1322             if res:
1323                 res += ', '
1324             res += '%s container' % fdict['container']
1325         if (fdict.get('vcodec') is not None and
1326                 fdict.get('vcodec') != 'none'):
1327             if res:
1328                 res += ', '
1329             res += fdict['vcodec']
1330             if fdict.get('vbr') is not None:
1331                 res += '@'
1332         elif fdict.get('vbr') is not None and fdict.get('abr') is not None:
1333             res += 'video@'
1334         if fdict.get('vbr') is not None:
1335             res += '%4dk' % fdict['vbr']
1336         if fdict.get('fps') is not None:
1337             res += ', %sfps' % fdict['fps']
1338         if fdict.get('acodec') is not None:
1339             if res:
1340                 res += ', '
1341             if fdict['acodec'] == 'none':
1342                 res += 'video only'
1343             else:
1344                 res += '%-5s' % fdict['acodec']
1345         elif fdict.get('abr') is not None:
1346             if res:
1347                 res += ', '
1348             res += 'audio'
1349         if fdict.get('abr') is not None:
1350             res += '@%3dk' % fdict['abr']
1351         if fdict.get('asr') is not None:
1352             res += ' (%5dHz)' % fdict['asr']
1353         if fdict.get('filesize') is not None:
1354             if res:
1355                 res += ', '
1356             res += format_bytes(fdict['filesize'])
1357         elif fdict.get('filesize_approx') is not None:
1358             if res:
1359                 res += ', '
1360             res += '~' + format_bytes(fdict['filesize_approx'])
1361         return res
1362
1363     def list_formats(self, info_dict):
1364         def line(format, idlen=20):
1365             return (('%-' + compat_str(idlen + 1) + 's%-10s%-12s%s') % (
1366                 format['format_id'],
1367                 format['ext'],
1368                 self.format_resolution(format),
1369                 self._format_note(format),
1370             ))
1371
1372         formats = info_dict.get('formats', [info_dict])
1373         idlen = max(len('format code'),
1374                     max(len(f['format_id']) for f in formats))
1375         formats_s = [
1376             line(f, idlen) for f in formats
1377             if f.get('preference') is None or f['preference'] >= -1000]
1378         if len(formats) > 1:
1379             formats_s[0] += (' ' if self._format_note(formats[0]) else '') + '(worst)'
1380             formats_s[-1] += (' ' if self._format_note(formats[-1]) else '') + '(best)'
1381
1382         header_line = line({
1383             'format_id': 'format code', 'ext': 'extension',
1384             'resolution': 'resolution', 'format_note': 'note'}, idlen=idlen)
1385         self.to_screen('[info] Available formats for %s:\n%s\n%s' %
1386                        (info_dict['id'], header_line, '\n'.join(formats_s)))
1387
1388     def urlopen(self, req):
1389         """ Start an HTTP download """
1390
1391         # According to RFC 3986, URLs can not contain non-ASCII characters, however this is not
1392         # always respected by websites, some tend to give out URLs with non percent-encoded
1393         # non-ASCII characters (see telemb.py, ard.py [#3412])
1394         # urllib chokes on URLs with non-ASCII characters (see http://bugs.python.org/issue3991)
1395         # To work around aforementioned issue we will replace request's original URL with
1396         # percent-encoded one
1397         req_is_string = isinstance(req, basestring if sys.version_info < (3, 0) else compat_str)
1398         url = req if req_is_string else req.get_full_url()
1399         url_escaped = escape_url(url)
1400
1401         # Substitute URL if any change after escaping
1402         if url != url_escaped:
1403             if req_is_string:
1404                 req = url_escaped
1405             else:
1406                 req = compat_urllib_request.Request(
1407                     url_escaped, data=req.data, headers=req.headers,
1408                     origin_req_host=req.origin_req_host, unverifiable=req.unverifiable)
1409
1410         return self._opener.open(req, timeout=self._socket_timeout)
1411
1412     def print_debug_header(self):
1413         if not self.params.get('verbose'):
1414             return
1415
1416         if type('') is not compat_str:
1417             # Python 2.6 on SLES11 SP1 (https://github.com/rg3/youtube-dl/issues/3326)
1418             self.report_warning(
1419                 'Your Python is broken! Update to a newer and supported version')
1420
1421         stdout_encoding = getattr(
1422             sys.stdout, 'encoding', 'missing (%s)' % type(sys.stdout).__name__)
1423         encoding_str = (
1424             '[debug] Encodings: locale %s, fs %s, out %s, pref %s\n' % (
1425                 locale.getpreferredencoding(),
1426                 sys.getfilesystemencoding(),
1427                 stdout_encoding,
1428                 self.get_encoding()))
1429         write_string(encoding_str, encoding=None)
1430
1431         self._write_string('[debug] youtube-dl version ' + __version__ + '\n')
1432         try:
1433             sp = subprocess.Popen(
1434                 ['git', 'rev-parse', '--short', 'HEAD'],
1435                 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1436                 cwd=os.path.dirname(os.path.abspath(__file__)))
1437             out, err = sp.communicate()
1438             out = out.decode().strip()
1439             if re.match('[0-9a-f]+', out):
1440                 self._write_string('[debug] Git HEAD: ' + out + '\n')
1441         except:
1442             try:
1443                 sys.exc_clear()
1444             except:
1445                 pass
1446         self._write_string('[debug] Python version %s - %s\n' % (
1447             platform.python_version(), platform_name()))
1448
1449         exe_versions = FFmpegPostProcessor.get_versions()
1450         exe_versions['rtmpdump'] = rtmpdump_version()
1451         exe_str = ', '.join(
1452             '%s %s' % (exe, v)
1453             for exe, v in sorted(exe_versions.items())
1454             if v
1455         )
1456         if not exe_str:
1457             exe_str = 'none'
1458         self._write_string('[debug] exe versions: %s\n' % exe_str)
1459
1460         proxy_map = {}
1461         for handler in self._opener.handlers:
1462             if hasattr(handler, 'proxies'):
1463                 proxy_map.update(handler.proxies)
1464         self._write_string('[debug] Proxy map: ' + compat_str(proxy_map) + '\n')
1465
1466     def _setup_opener(self):
1467         timeout_val = self.params.get('socket_timeout')
1468         self._socket_timeout = 600 if timeout_val is None else float(timeout_val)
1469
1470         opts_cookiefile = self.params.get('cookiefile')
1471         opts_proxy = self.params.get('proxy')
1472
1473         if opts_cookiefile is None:
1474             self.cookiejar = compat_cookiejar.CookieJar()
1475         else:
1476             self.cookiejar = compat_cookiejar.MozillaCookieJar(
1477                 opts_cookiefile)
1478             if os.access(opts_cookiefile, os.R_OK):
1479                 self.cookiejar.load()
1480
1481         cookie_processor = compat_urllib_request.HTTPCookieProcessor(
1482             self.cookiejar)
1483         if opts_proxy is not None:
1484             if opts_proxy == '':
1485                 proxies = {}
1486             else:
1487                 proxies = {'http': opts_proxy, 'https': opts_proxy}
1488         else:
1489             proxies = compat_urllib_request.getproxies()
1490             # Set HTTPS proxy to HTTP one if given (https://github.com/rg3/youtube-dl/issues/805)
1491             if 'http' in proxies and 'https' not in proxies:
1492                 proxies['https'] = proxies['http']
1493         proxy_handler = compat_urllib_request.ProxyHandler(proxies)
1494
1495         debuglevel = 1 if self.params.get('debug_printtraffic') else 0
1496         https_handler = make_HTTPS_handler(
1497             self.params.get('nocheckcertificate', False), debuglevel=debuglevel)
1498         ydlh = YoutubeDLHandler(debuglevel=debuglevel)
1499         opener = compat_urllib_request.build_opener(
1500             https_handler, proxy_handler, cookie_processor, ydlh)
1501         # Delete the default user-agent header, which would otherwise apply in
1502         # cases where our custom HTTP handler doesn't come into play
1503         # (See https://github.com/rg3/youtube-dl/issues/1309 for details)
1504         opener.addheaders = []
1505         self._opener = opener
1506
1507     def encode(self, s):
1508         if isinstance(s, bytes):
1509             return s  # Already encoded
1510
1511         try:
1512             return s.encode(self.get_encoding())
1513         except UnicodeEncodeError as err:
1514             err.reason = err.reason + '. Check your system encoding configuration or use the --encoding option.'
1515             raise
1516
1517     def get_encoding(self):
1518         encoding = self.params.get('encoding')
1519         if encoding is None:
1520             encoding = preferredencoding()
1521         return encoding