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