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