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