]> gitweb @ CieloNegro.org - youtube-dl.git/blob - youtube_dl/FileDownloader.py
Merge pull request #887 from anisse/master
[youtube-dl.git] / youtube_dl / FileDownloader.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from __future__ import absolute_import
5
6 import math
7 import io
8 import os
9 import re
10 import shutil
11 import socket
12 import subprocess
13 import sys
14 import time
15 import traceback
16
17 if os.name == 'nt':
18     import ctypes
19
20 from .utils import *
21 from .InfoExtractors import get_info_extractor
22
23
24 class FileDownloader(object):
25     """File Downloader class.
26
27     File downloader objects are the ones responsible of downloading the
28     actual video file and writing it to disk if the user has requested
29     it, among some other tasks. In most cases there should be one per
30     program. As, given a video URL, the downloader doesn't know how to
31     extract all the needed information, task that InfoExtractors do, it
32     has to pass the URL to one of them.
33
34     For this, file downloader objects have a method that allows
35     InfoExtractors to be registered in a given order. When it is passed
36     a URL, the file downloader handles it to the first InfoExtractor it
37     finds that reports being able to handle it. The InfoExtractor extracts
38     all the information about the video or videos the URL refers to, and
39     asks the FileDownloader to process the video information, possibly
40     downloading the video.
41
42     File downloaders accept a lot of parameters. In order not to saturate
43     the object constructor with arguments, it receives a dictionary of
44     options instead. These options are available through the params
45     attribute for the InfoExtractors to use. The FileDownloader also
46     registers itself as the downloader in charge for the InfoExtractors
47     that are added to it, so this is a "mutual registration".
48
49     Available options:
50
51     username:          Username for authentication purposes.
52     password:          Password for authentication purposes.
53     usenetrc:          Use netrc for authentication instead.
54     quiet:             Do not print messages to stdout.
55     forceurl:          Force printing final URL.
56     forcetitle:        Force printing title.
57     forceid:           Force printing ID.
58     forcethumbnail:    Force printing thumbnail URL.
59     forcedescription:  Force printing description.
60     forcefilename:     Force printing final filename.
61     simulate:          Do not download the video files.
62     format:            Video format code.
63     format_limit:      Highest quality format to try.
64     outtmpl:           Template for output names.
65     restrictfilenames: Do not allow "&" and spaces in file names
66     ignoreerrors:      Do not stop on download errors.
67     ratelimit:         Download speed limit, in bytes/sec.
68     nooverwrites:      Prevent overwriting files.
69     retries:           Number of times to retry for HTTP error 5xx
70     buffersize:        Size of download buffer in bytes.
71     noresizebuffer:    Do not automatically resize the download buffer.
72     continuedl:        Try to continue downloads if possible.
73     noprogress:        Do not print the progress bar.
74     playliststart:     Playlist item to start at.
75     playlistend:       Playlist item to end at.
76     matchtitle:        Download only matching titles.
77     rejecttitle:       Reject downloads for matching titles.
78     logtostderr:       Log messages to stderr instead of stdout.
79     consoletitle:      Display progress in console window's titlebar.
80     nopart:            Do not use temporary .part files.
81     updatetime:        Use the Last-modified header to set output file timestamps.
82     writedescription:  Write the video description to a .description file
83     writeinfojson:     Write the video description to a .info.json file
84     writethumbnail:    Write the thumbnail image to a file
85     writesubtitles:    Write the video subtitles to a file
86     allsubtitles:      Downloads all the subtitles of the video
87     listsubtitles:     Lists all available subtitles for the video
88     subtitlesformat:   Subtitle format [sbv/srt] (default=srt)
89     subtitleslang:     Language of the subtitles to download
90     test:              Download only first bytes to test the downloader.
91     keepvideo:         Keep the video file after post-processing
92     min_filesize:      Skip files smaller than this size
93     max_filesize:      Skip files larger than this size
94     daterange:         A DateRange object, download only if the upload_date is in the range.
95     skip_download:     Skip the actual download of the video file
96     """
97
98     params = None
99     _ies = []
100     _pps = []
101     _download_retcode = None
102     _num_downloads = None
103     _screen_file = None
104
105     def __init__(self, params):
106         """Create a FileDownloader object with the given options."""
107         self._ies = []
108         self._pps = []
109         self._progress_hooks = []
110         self._download_retcode = 0
111         self._num_downloads = 0
112         self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
113         self.params = params
114
115         if '%(stitle)s' in self.params['outtmpl']:
116             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.')
117
118     @staticmethod
119     def format_bytes(bytes):
120         if bytes is None:
121             return 'N/A'
122         if type(bytes) is str:
123             bytes = float(bytes)
124         if bytes == 0.0:
125             exponent = 0
126         else:
127             exponent = int(math.log(bytes, 1024.0))
128         suffix = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'][exponent]
129         converted = float(bytes) / float(1024 ** exponent)
130         return '%.2f%s' % (converted, suffix)
131
132     @staticmethod
133     def calc_percent(byte_counter, data_len):
134         if data_len is None:
135             return '---.-%'
136         return '%6s' % ('%3.1f%%' % (float(byte_counter) / float(data_len) * 100.0))
137
138     @staticmethod
139     def calc_eta(start, now, total, current):
140         if total is None:
141             return '--:--'
142         dif = now - start
143         if current == 0 or dif < 0.001: # One millisecond
144             return '--:--'
145         rate = float(current) / dif
146         eta = int((float(total) - float(current)) / rate)
147         (eta_mins, eta_secs) = divmod(eta, 60)
148         if eta_mins > 99:
149             return '--:--'
150         return '%02d:%02d' % (eta_mins, eta_secs)
151
152     @staticmethod
153     def calc_speed(start, now, bytes):
154         dif = now - start
155         if bytes == 0 or dif < 0.001: # One millisecond
156             return '%10s' % '---b/s'
157         return '%10s' % ('%s/s' % FileDownloader.format_bytes(float(bytes) / dif))
158
159     @staticmethod
160     def best_block_size(elapsed_time, bytes):
161         new_min = max(bytes / 2.0, 1.0)
162         new_max = min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB
163         if elapsed_time < 0.001:
164             return int(new_max)
165         rate = bytes / elapsed_time
166         if rate > new_max:
167             return int(new_max)
168         if rate < new_min:
169             return int(new_min)
170         return int(rate)
171
172     @staticmethod
173     def parse_bytes(bytestr):
174         """Parse a string indicating a byte quantity into an integer."""
175         matchobj = re.match(r'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr)
176         if matchobj is None:
177             return None
178         number = float(matchobj.group(1))
179         multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
180         return int(round(number * multiplier))
181
182     def add_info_extractor(self, ie):
183         """Add an InfoExtractor object to the end of the list."""
184         self._ies.append(ie)
185         ie.set_downloader(self)
186
187     def add_post_processor(self, pp):
188         """Add a PostProcessor object to the end of the chain."""
189         self._pps.append(pp)
190         pp.set_downloader(self)
191
192     def to_screen(self, message, skip_eol=False):
193         """Print message to stdout if not in quiet mode."""
194         assert type(message) == type(u'')
195         if not self.params.get('quiet', False):
196             terminator = [u'\n', u''][skip_eol]
197             output = message + terminator
198             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
199                 output = output.encode(preferredencoding(), 'ignore')
200             self._screen_file.write(output)
201             self._screen_file.flush()
202
203     def to_stderr(self, message):
204         """Print message to stderr."""
205         assert type(message) == type(u'')
206         output = message + u'\n'
207         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
208             output = output.encode(preferredencoding())
209         sys.stderr.write(output)
210
211     def to_cons_title(self, message):
212         """Set console/terminal window title to message."""
213         if not self.params.get('consoletitle', False):
214             return
215         if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
216             # c_wchar_p() might not be necessary if `message` is
217             # already of type unicode()
218             ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
219         elif 'TERM' in os.environ:
220             self.to_screen('\033]0;%s\007' % message, skip_eol=True)
221
222     def fixed_template(self):
223         """Checks if the output template is fixed."""
224         return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None)
225
226     def trouble(self, message=None, tb=None):
227         """Determine action to take when a download problem appears.
228
229         Depending on if the downloader has been configured to ignore
230         download errors or not, this method may throw an exception or
231         not when errors are found, after printing the message.
232
233         tb, if given, is additional traceback information.
234         """
235         if message is not None:
236             self.to_stderr(message)
237         if self.params.get('verbose'):
238             if tb is None:
239                 if sys.exc_info()[0]:  # if .trouble has been called from an except block
240                     tb = u''
241                     if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
242                         tb += u''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
243                     tb += compat_str(traceback.format_exc())
244                 else:
245                     tb_data = traceback.format_list(traceback.extract_stack())
246                     tb = u''.join(tb_data)
247             self.to_stderr(tb)
248         if not self.params.get('ignoreerrors', False):
249             if sys.exc_info()[0] and hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
250                 exc_info = sys.exc_info()[1].exc_info
251             else:
252                 exc_info = sys.exc_info()
253             raise DownloadError(message, exc_info)
254         self._download_retcode = 1
255
256     def report_warning(self, message):
257         '''
258         Print the message to stderr, it will be prefixed with 'WARNING:'
259         If stderr is a tty file the 'WARNING:' will be colored
260         '''
261         if sys.stderr.isatty() and os.name != 'nt':
262             _msg_header=u'\033[0;33mWARNING:\033[0m'
263         else:
264             _msg_header=u'WARNING:'
265         warning_message=u'%s %s' % (_msg_header,message)
266         self.to_stderr(warning_message)
267
268     def report_error(self, message, tb=None):
269         '''
270         Do the same as trouble, but prefixes the message with 'ERROR:', colored
271         in red if stderr is a tty file.
272         '''
273         if sys.stderr.isatty() and os.name != 'nt':
274             _msg_header = u'\033[0;31mERROR:\033[0m'
275         else:
276             _msg_header = u'ERROR:'
277         error_message = u'%s %s' % (_msg_header, message)
278         self.trouble(error_message, tb)
279
280     def slow_down(self, start_time, byte_counter):
281         """Sleep if the download speed is over the rate limit."""
282         rate_limit = self.params.get('ratelimit', None)
283         if rate_limit is None or byte_counter == 0:
284             return
285         now = time.time()
286         elapsed = now - start_time
287         if elapsed <= 0.0:
288             return
289         speed = float(byte_counter) / elapsed
290         if speed > rate_limit:
291             time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
292
293     def temp_name(self, filename):
294         """Returns a temporary filename for the given filename."""
295         if self.params.get('nopart', False) or filename == u'-' or \
296                 (os.path.exists(encodeFilename(filename)) and not os.path.isfile(encodeFilename(filename))):
297             return filename
298         return filename + u'.part'
299
300     def undo_temp_name(self, filename):
301         if filename.endswith(u'.part'):
302             return filename[:-len(u'.part')]
303         return filename
304
305     def try_rename(self, old_filename, new_filename):
306         try:
307             if old_filename == new_filename:
308                 return
309             os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
310         except (IOError, OSError) as err:
311             self.report_error(u'unable to rename file')
312
313     def try_utime(self, filename, last_modified_hdr):
314         """Try to set the last-modified time of the given file."""
315         if last_modified_hdr is None:
316             return
317         if not os.path.isfile(encodeFilename(filename)):
318             return
319         timestr = last_modified_hdr
320         if timestr is None:
321             return
322         filetime = timeconvert(timestr)
323         if filetime is None:
324             return filetime
325         try:
326             os.utime(filename, (time.time(), filetime))
327         except:
328             pass
329         return filetime
330
331     def report_writedescription(self, descfn):
332         """ Report that the description file is being written """
333         self.to_screen(u'[info] Writing video description to: ' + descfn)
334
335     def report_writesubtitles(self, sub_filename):
336         """ Report that the subtitles file is being written """
337         self.to_screen(u'[info] Writing video subtitles to: ' + sub_filename)
338
339     def report_writeinfojson(self, infofn):
340         """ Report that the metadata file has been written """
341         self.to_screen(u'[info] Video description metadata as JSON to: ' + infofn)
342
343     def report_destination(self, filename):
344         """Report destination filename."""
345         self.to_screen(u'[download] Destination: ' + filename)
346
347     def report_progress(self, percent_str, data_len_str, speed_str, eta_str):
348         """Report download progress."""
349         if self.params.get('noprogress', False):
350             return
351         clear_line = (u'\x1b[K' if sys.stderr.isatty() and os.name != 'nt' else u'')
352         if self.params.get('progress_with_newline', False):
353             self.to_screen(u'[download] %s of %s at %s ETA %s' %
354                 (percent_str, data_len_str, speed_str, eta_str))
355         else:
356             self.to_screen(u'\r%s[download] %s of %s at %s ETA %s' %
357                 (clear_line, percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
358         self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' %
359                 (percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))
360
361     def report_resuming_byte(self, resume_len):
362         """Report attempt to resume at given byte."""
363         self.to_screen(u'[download] Resuming download at byte %s' % resume_len)
364
365     def report_retry(self, count, retries):
366         """Report retry in case of HTTP error 5xx"""
367         self.to_screen(u'[download] Got server HTTP error. Retrying (attempt %d of %d)...' % (count, retries))
368
369     def report_file_already_downloaded(self, file_name):
370         """Report file has already been fully downloaded."""
371         try:
372             self.to_screen(u'[download] %s has already been downloaded' % file_name)
373         except (UnicodeEncodeError) as err:
374             self.to_screen(u'[download] The file has already been downloaded')
375
376     def report_unable_to_resume(self):
377         """Report it was impossible to resume download."""
378         self.to_screen(u'[download] Unable to resume')
379
380     def report_finish(self):
381         """Report download finished."""
382         if self.params.get('noprogress', False):
383             self.to_screen(u'[download] Download completed')
384         else:
385             self.to_screen(u'')
386
387     def increment_downloads(self):
388         """Increment the ordinal that assigns a number to each file."""
389         self._num_downloads += 1
390
391     def prepare_filename(self, info_dict):
392         """Generate the output filename."""
393         try:
394             template_dict = dict(info_dict)
395
396             template_dict['epoch'] = int(time.time())
397             autonumber_size = self.params.get('autonumber_size')
398             if autonumber_size is None:
399                 autonumber_size = 5
400             autonumber_templ = u'%0' + str(autonumber_size) + u'd'
401             template_dict['autonumber'] = autonumber_templ % self._num_downloads
402             if template_dict['playlist_index'] is not None:
403                 template_dict['playlist_index'] = u'%05d' % template_dict['playlist_index']
404
405             sanitize = lambda k,v: sanitize_filename(
406                 u'NA' if v is None else compat_str(v),
407                 restricted=self.params.get('restrictfilenames'),
408                 is_id=(k==u'id'))
409             template_dict = dict((k, sanitize(k, v)) for k,v in template_dict.items())
410
411             filename = self.params['outtmpl'] % template_dict
412             return filename
413         except KeyError as err:
414             self.report_error(u'Erroneous output template')
415             return None
416         except ValueError as err:
417             self.report_error(u'Insufficient system charset ' + repr(preferredencoding()))
418             return None
419
420     def _match_entry(self, info_dict):
421         """ Returns None iff the file should be downloaded """
422
423         title = info_dict['title']
424         matchtitle = self.params.get('matchtitle', False)
425         if matchtitle:
426             if not re.search(matchtitle, title, re.IGNORECASE):
427                 return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
428         rejecttitle = self.params.get('rejecttitle', False)
429         if rejecttitle:
430             if re.search(rejecttitle, title, re.IGNORECASE):
431                 return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
432         date = info_dict.get('upload_date', None)
433         if date is not None:
434             dateRange = self.params.get('daterange', DateRange())
435             if date not in dateRange:
436                 return u'[download] %s upload date is not in range %s' % (date_from_str(date).isoformat(), dateRange)
437         return None
438         
439     def extract_info(self, url, download=True, ie_key=None, extra_info={}):
440         '''
441         Returns a list with a dictionary for each video we find.
442         If 'download', also downloads the videos.
443         extra_info is a dict containing the extra values to add to each result
444          '''
445         
446         if ie_key:
447             ie = get_info_extractor(ie_key)()
448             ie.set_downloader(self)
449             ies = [ie]
450         else:
451             ies = self._ies
452
453         for ie in ies:
454             if not ie.suitable(url):
455                 continue
456
457             if not ie.working():
458                 self.report_warning(u'The program functionality for this site has been marked as broken, '
459                                     u'and will probably not work.')
460
461             try:
462                 ie_result = ie.extract(url)
463                 if ie_result is None: # Finished already (backwards compatibility; listformats and friends should be moved here)
464                     break
465                 if isinstance(ie_result, list):
466                     # Backwards compatibility: old IE result format
467                     for result in ie_result:
468                         result.update(extra_info)
469                     ie_result = {
470                         '_type': 'compat_list',
471                         'entries': ie_result,
472                     }
473                 else:
474                     ie_result.update(extra_info)
475                 if 'extractor' not in ie_result:
476                     ie_result['extractor'] = ie.IE_NAME
477                 return self.process_ie_result(ie_result, download=download)
478             except ExtractorError as de: # An error we somewhat expected
479                 self.report_error(compat_str(de), de.format_traceback())
480                 break
481             except Exception as e:
482                 if self.params.get('ignoreerrors', False):
483                     self.report_error(compat_str(e), tb=compat_str(traceback.format_exc()))
484                     break
485                 else:
486                     raise
487         else:
488             self.report_error(u'no suitable InfoExtractor: %s' % url)
489         
490     def process_ie_result(self, ie_result, download=True, extra_info={}):
491         """
492         Take the result of the ie(may be modified) and resolve all unresolved
493         references (URLs, playlist items).
494
495         It will also download the videos if 'download'.
496         Returns the resolved ie_result.
497         """
498
499         result_type = ie_result.get('_type', 'video') # If not given we suppose it's a video, support the default old system
500         if result_type == 'video':
501             if 'playlist' not in ie_result:
502                 # It isn't part of a playlist
503                 ie_result['playlist'] = None
504                 ie_result['playlist_index'] = None
505             if download:
506                 self.process_info(ie_result)
507             return ie_result
508         elif result_type == 'url':
509             # We have to add extra_info to the results because it may be
510             # contained in a playlist
511             return self.extract_info(ie_result['url'],
512                                      download,
513                                      ie_key=ie_result.get('ie_key'),
514                                      extra_info=extra_info)
515         elif result_type == 'playlist':
516             # We process each entry in the playlist
517             playlist = ie_result.get('title', None) or ie_result.get('id', None)
518             self.to_screen(u'[download] Downloading playlist: %s'  % playlist)
519
520             playlist_results = []
521
522             n_all_entries = len(ie_result['entries'])
523             playliststart = self.params.get('playliststart', 1) - 1
524             playlistend = self.params.get('playlistend', -1)
525
526             if playlistend == -1:
527                 entries = ie_result['entries'][playliststart:]
528             else:
529                 entries = ie_result['entries'][playliststart:playlistend]
530
531             n_entries = len(entries)
532
533             self.to_screen(u"[%s] playlist '%s': Collected %d video ids (downloading %d of them)" %
534                 (ie_result['extractor'], playlist, n_all_entries, n_entries))
535
536             for i,entry in enumerate(entries,1):
537                 self.to_screen(u'[download] Downloading video #%s of %s' %(i, n_entries))
538                 extra = {
539                          'playlist': playlist, 
540                          'playlist_index': i + playliststart,
541                          }
542                 if not 'extractor' in entry:
543                     # We set the extractor, if it's an url it will be set then to
544                     # the new extractor, but if it's already a video we must make
545                     # sure it's present: see issue #877
546                     entry['extractor'] = ie_result['extractor']
547                 entry_result = self.process_ie_result(entry,
548                                                       download=download,
549                                                       extra_info=extra)
550                 playlist_results.append(entry_result)
551             ie_result['entries'] = playlist_results
552             return ie_result
553         elif result_type == 'compat_list':
554             def _fixup(r):
555                 r.setdefault('extractor', ie_result['extractor'])
556                 return r
557             ie_result['entries'] = [
558                 self.process_ie_result(_fixup(r), download=download)
559                 for r in ie_result['entries']
560             ]
561             return ie_result
562         else:
563             raise Exception('Invalid result type: %s' % result_type)
564
565     def process_info(self, info_dict):
566         """Process a single resolved IE result."""
567
568         assert info_dict.get('_type', 'video') == 'video'
569         #We increment the download the download count here to match the previous behaviour.
570         self.increment_downloads()
571
572         info_dict['fulltitle'] = info_dict['title']
573         if len(info_dict['title']) > 200:
574             info_dict['title'] = info_dict['title'][:197] + u'...'
575
576         # Keep for backwards compatibility
577         info_dict['stitle'] = info_dict['title']
578
579         if not 'format' in info_dict:
580             info_dict['format'] = info_dict['ext']
581
582         reason = self._match_entry(info_dict)
583         if reason is not None:
584             self.to_screen(u'[download] ' + reason)
585             return
586
587         max_downloads = self.params.get('max_downloads')
588         if max_downloads is not None:
589             if self._num_downloads > int(max_downloads):
590                 raise MaxDownloadsReached()
591
592         filename = self.prepare_filename(info_dict)
593
594         # Forced printings
595         if self.params.get('forcetitle', False):
596             compat_print(info_dict['title'])
597         if self.params.get('forceid', False):
598             compat_print(info_dict['id'])
599         if self.params.get('forceurl', False):
600             compat_print(info_dict['url'])
601         if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
602             compat_print(info_dict['thumbnail'])
603         if self.params.get('forcedescription', False) and 'description' in info_dict:
604             compat_print(info_dict['description'])
605         if self.params.get('forcefilename', False) and filename is not None:
606             compat_print(filename)
607         if self.params.get('forceformat', False):
608             compat_print(info_dict['format'])
609
610         # Do nothing else if in simulate mode
611         if self.params.get('simulate', False):
612             return
613
614         if filename is None:
615             return
616
617         try:
618             dn = os.path.dirname(encodeFilename(filename))
619             if dn != '' and not os.path.exists(dn):
620                 os.makedirs(dn)
621         except (OSError, IOError) as err:
622             self.report_error(u'unable to create directory ' + compat_str(err))
623             return
624
625         if self.params.get('writedescription', False):
626             try:
627                 descfn = filename + u'.description'
628                 self.report_writedescription(descfn)
629                 with io.open(encodeFilename(descfn), 'w', encoding='utf-8') as descfile:
630                     descfile.write(info_dict['description'])
631             except (OSError, IOError):
632                 self.report_error(u'Cannot write description file ' + descfn)
633                 return
634
635         if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
636             # subtitles download errors are already managed as troubles in relevant IE
637             # that way it will silently go on when used with unsupporting IE
638             subtitle = info_dict['subtitles'][0]
639             (sub_error, sub_lang, sub) = subtitle
640             sub_format = self.params.get('subtitlesformat')
641             if sub_error:
642                 self.report_warning("Some error while getting the subtitles")
643             else:
644                 try:
645                     sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
646                     self.report_writesubtitles(sub_filename)
647                     with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
648                         subfile.write(sub)
649                 except (OSError, IOError):
650                     self.report_error(u'Cannot write subtitles file ' + descfn)
651                     return
652
653         if self.params.get('allsubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
654             subtitles = info_dict['subtitles']
655             sub_format = self.params.get('subtitlesformat')
656             for subtitle in subtitles:
657                 (sub_error, sub_lang, sub) = subtitle
658                 if sub_error:
659                     self.report_warning("Some error while getting the subtitles")
660                 else:
661                     try:
662                         sub_filename = filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
663                         self.report_writesubtitles(sub_filename)
664                         with io.open(encodeFilename(sub_filename), 'w', encoding='utf-8') as subfile:
665                                 subfile.write(sub)
666                     except (OSError, IOError):
667                         self.report_error(u'Cannot write subtitles file ' + descfn)
668                         return
669
670         if self.params.get('writeinfojson', False):
671             infofn = filename + u'.info.json'
672             self.report_writeinfojson(infofn)
673             try:
674                 json_info_dict = dict((k, v) for k,v in info_dict.items() if not k in ['urlhandle'])
675                 write_json_file(json_info_dict, encodeFilename(infofn))
676             except (OSError, IOError):
677                 self.report_error(u'Cannot write metadata to JSON file ' + infofn)
678                 return
679
680         if self.params.get('writethumbnail', False):
681             if 'thumbnail' in info_dict:
682                 thumb_format = info_dict['thumbnail'].rpartition(u'/')[2].rpartition(u'.')[2]
683                 if not thumb_format:
684                     thumb_format = 'jpg'
685                 thumb_filename = filename.rpartition('.')[0] + u'.' + thumb_format
686                 self.to_screen(u'[%s] %s: Downloading thumbnail ...' %
687                                (info_dict['extractor'], info_dict['id']))
688                 uf = compat_urllib_request.urlopen(info_dict['thumbnail'])
689                 with open(thumb_filename, 'wb') as thumbf:
690                     shutil.copyfileobj(uf, thumbf)
691                 self.to_screen(u'[%s] %s: Writing thumbnail to: %s' %
692                                (info_dict['extractor'], info_dict['id'], thumb_filename))
693
694         if not self.params.get('skip_download', False):
695             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
696                 success = True
697             else:
698                 try:
699                     success = self._do_download(filename, info_dict)
700                 except (OSError, IOError) as err:
701                     raise UnavailableVideoError()
702                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
703                     self.report_error(u'unable to download video data: %s' % str(err))
704                     return
705                 except (ContentTooShortError, ) as err:
706                     self.report_error(u'content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
707                     return
708
709             if success:
710                 try:
711                     self.post_process(filename, info_dict)
712                 except (PostProcessingError) as err:
713                     self.report_error(u'postprocessing: %s' % str(err))
714                     return
715
716     def download(self, url_list):
717         """Download a given list of URLs."""
718         if len(url_list) > 1 and self.fixed_template():
719             raise SameFileError(self.params['outtmpl'])
720
721         for url in url_list:
722             try:
723                 #It also downloads the videos
724                 videos = self.extract_info(url)
725             except UnavailableVideoError:
726                 self.report_error(u'unable to download video')
727             except MaxDownloadsReached:
728                 self.to_screen(u'[info] Maximum number of downloaded files reached.')
729                 raise
730
731         return self._download_retcode
732
733     def post_process(self, filename, ie_info):
734         """Run all the postprocessors on the given file."""
735         info = dict(ie_info)
736         info['filepath'] = filename
737         keep_video = None
738         for pp in self._pps:
739             try:
740                 keep_video_wish,new_info = pp.run(info)
741                 if keep_video_wish is not None:
742                     if keep_video_wish:
743                         keep_video = keep_video_wish
744                     elif keep_video is None:
745                         # No clear decision yet, let IE decide
746                         keep_video = keep_video_wish
747             except PostProcessingError as e:
748                 self.to_stderr(u'ERROR: ' + e.msg)
749         if keep_video is False and not self.params.get('keepvideo', False):
750             try:
751                 self.to_screen(u'Deleting original file %s (pass -k to keep)' % filename)
752                 os.remove(encodeFilename(filename))
753             except (IOError, OSError):
754                 self.report_warning(u'Unable to remove downloaded video file')
755
756     def _download_with_rtmpdump(self, filename, url, player_url, page_url, play_path, tc_url):
757         self.report_destination(filename)
758         tmpfilename = self.temp_name(filename)
759
760         # Check for rtmpdump first
761         try:
762             subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
763         except (OSError, IOError):
764             self.report_error(u'RTMP download detected but "rtmpdump" could not be run')
765             return False
766         verbosity_option = '--verbose' if self.params.get('verbose', False) else '--quiet'
767
768         # Download using rtmpdump. rtmpdump returns exit code 2 when
769         # the connection was interrumpted and resuming appears to be
770         # possible. This is part of rtmpdump's normal usage, AFAIK.
771         basic_args = ['rtmpdump', verbosity_option, '-r', url, '-o', tmpfilename]
772         if player_url is not None:
773             basic_args += ['--swfVfy', player_url]
774         if page_url is not None:
775             basic_args += ['--pageUrl', page_url]
776         if play_path is not None:
777             basic_args += ['--playpath', play_path]
778         if tc_url is not None:
779             basic_args += ['--tcUrl', url]
780         args = basic_args + [[], ['--resume', '--skip', '1']][self.params.get('continuedl', False)]
781         if self.params.get('verbose', False):
782             try:
783                 import pipes
784                 shell_quote = lambda args: ' '.join(map(pipes.quote, args))
785             except ImportError:
786                 shell_quote = repr
787             self.to_screen(u'[debug] rtmpdump command line: ' + shell_quote(args))
788         retval = subprocess.call(args)
789         while retval == 2 or retval == 1:
790             prevsize = os.path.getsize(encodeFilename(tmpfilename))
791             self.to_screen(u'\r[rtmpdump] %s bytes' % prevsize, skip_eol=True)
792             time.sleep(5.0) # This seems to be needed
793             retval = subprocess.call(basic_args + ['-e'] + [[], ['-k', '1']][retval == 1])
794             cursize = os.path.getsize(encodeFilename(tmpfilename))
795             if prevsize == cursize and retval == 1:
796                 break
797              # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those
798             if prevsize == cursize and retval == 2 and cursize > 1024:
799                 self.to_screen(u'\r[rtmpdump] Could not download the whole video. This can happen for some advertisements.')
800                 retval = 0
801                 break
802         if retval == 0:
803             fsize = os.path.getsize(encodeFilename(tmpfilename))
804             self.to_screen(u'\r[rtmpdump] %s bytes' % fsize)
805             self.try_rename(tmpfilename, filename)
806             self._hook_progress({
807                 'downloaded_bytes': fsize,
808                 'total_bytes': fsize,
809                 'filename': filename,
810                 'status': 'finished',
811             })
812             return True
813         else:
814             self.to_stderr(u"\n")
815             self.report_error(u'rtmpdump exited with code %d' % retval)
816             return False
817
818     def _download_with_mplayer(self, filename, url):
819         self.report_destination(filename)
820         tmpfilename = self.temp_name(filename)
821
822         args = ['mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy', '-dumpstream', '-dumpfile', tmpfilename, url]
823         # Check for mplayer first
824         try:
825             subprocess.call(['mplayer', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
826         except (OSError, IOError):
827             self.report_error(u'MMS or RTSP download detected but "%s" could not be run' % args[0] )
828             return False
829
830         # Download using mplayer. 
831         retval = subprocess.call(args)
832         if retval == 0:
833             fsize = os.path.getsize(encodeFilename(tmpfilename))
834             self.to_screen(u'\r[%s] %s bytes' % (args[0], fsize))
835             self.try_rename(tmpfilename, filename)
836             self._hook_progress({
837                 'downloaded_bytes': fsize,
838                 'total_bytes': fsize,
839                 'filename': filename,
840                 'status': 'finished',
841             })
842             return True
843         else:
844             self.to_stderr(u"\n")
845             self.report_error(u'mplayer exited with code %d' % retval)
846             return False
847
848
849     def _do_download(self, filename, info_dict):
850         url = info_dict['url']
851
852         # Check file already present
853         if self.params.get('continuedl', False) and os.path.isfile(encodeFilename(filename)) and not self.params.get('nopart', False):
854             self.report_file_already_downloaded(filename)
855             self._hook_progress({
856                 'filename': filename,
857                 'status': 'finished',
858             })
859             return True
860
861         # Attempt to download using rtmpdump
862         if url.startswith('rtmp'):
863             return self._download_with_rtmpdump(filename, url,
864                                                 info_dict.get('player_url', None),
865                                                 info_dict.get('page_url', None),
866                                                 info_dict.get('play_path', None),
867                                                 info_dict.get('tc_url', None))
868
869         # Attempt to download using mplayer
870         if url.startswith('mms') or url.startswith('rtsp'):
871             return self._download_with_mplayer(filename, url)
872
873         tmpfilename = self.temp_name(filename)
874         stream = None
875
876         # Do not include the Accept-Encoding header
877         headers = {'Youtubedl-no-compression': 'True'}
878         if 'user_agent' in info_dict:
879             headers['Youtubedl-user-agent'] = info_dict['user_agent']
880         basic_request = compat_urllib_request.Request(url, None, headers)
881         request = compat_urllib_request.Request(url, None, headers)
882
883         if self.params.get('test', False):
884             request.add_header('Range','bytes=0-10240')
885
886         # Establish possible resume length
887         if os.path.isfile(encodeFilename(tmpfilename)):
888             resume_len = os.path.getsize(encodeFilename(tmpfilename))
889         else:
890             resume_len = 0
891
892         open_mode = 'wb'
893         if resume_len != 0:
894             if self.params.get('continuedl', False):
895                 self.report_resuming_byte(resume_len)
896                 request.add_header('Range','bytes=%d-' % resume_len)
897                 open_mode = 'ab'
898             else:
899                 resume_len = 0
900
901         count = 0
902         retries = self.params.get('retries', 0)
903         while count <= retries:
904             # Establish connection
905             try:
906                 if count == 0 and 'urlhandle' in info_dict:
907                     data = info_dict['urlhandle']
908                 data = compat_urllib_request.urlopen(request)
909                 break
910             except (compat_urllib_error.HTTPError, ) as err:
911                 if (err.code < 500 or err.code >= 600) and err.code != 416:
912                     # Unexpected HTTP error
913                     raise
914                 elif err.code == 416:
915                     # Unable to resume (requested range not satisfiable)
916                     try:
917                         # Open the connection again without the range header
918                         data = compat_urllib_request.urlopen(basic_request)
919                         content_length = data.info()['Content-Length']
920                     except (compat_urllib_error.HTTPError, ) as err:
921                         if err.code < 500 or err.code >= 600:
922                             raise
923                     else:
924                         # Examine the reported length
925                         if (content_length is not None and
926                                 (resume_len - 100 < int(content_length) < resume_len + 100)):
927                             # The file had already been fully downloaded.
928                             # Explanation to the above condition: in issue #175 it was revealed that
929                             # YouTube sometimes adds or removes a few bytes from the end of the file,
930                             # changing the file size slightly and causing problems for some users. So
931                             # I decided to implement a suggested change and consider the file
932                             # completely downloaded if the file size differs less than 100 bytes from
933                             # the one in the hard drive.
934                             self.report_file_already_downloaded(filename)
935                             self.try_rename(tmpfilename, filename)
936                             self._hook_progress({
937                                 'filename': filename,
938                                 'status': 'finished',
939                             })
940                             return True
941                         else:
942                             # The length does not match, we start the download over
943                             self.report_unable_to_resume()
944                             open_mode = 'wb'
945                             break
946             # Retry
947             count += 1
948             if count <= retries:
949                 self.report_retry(count, retries)
950
951         if count > retries:
952             self.report_error(u'giving up after %s retries' % retries)
953             return False
954
955         data_len = data.info().get('Content-length', None)
956         if data_len is not None:
957             data_len = int(data_len) + resume_len
958             min_data_len = self.params.get("min_filesize", None)
959             max_data_len =  self.params.get("max_filesize", None)
960             if min_data_len is not None and data_len < min_data_len:
961                 self.to_screen(u'\r[download] File is smaller than min-filesize (%s bytes < %s bytes). Aborting.' % (data_len, min_data_len))
962                 return False
963             if max_data_len is not None and data_len > max_data_len:
964                 self.to_screen(u'\r[download] File is larger than max-filesize (%s bytes > %s bytes). Aborting.' % (data_len, max_data_len))
965                 return False
966
967         data_len_str = self.format_bytes(data_len)
968         byte_counter = 0 + resume_len
969         block_size = self.params.get('buffersize', 1024)
970         start = time.time()
971         while True:
972             # Download and write
973             before = time.time()
974             data_block = data.read(block_size)
975             after = time.time()
976             if len(data_block) == 0:
977                 break
978             byte_counter += len(data_block)
979
980             # Open file just in time
981             if stream is None:
982                 try:
983                     (stream, tmpfilename) = sanitize_open(tmpfilename, open_mode)
984                     assert stream is not None
985                     filename = self.undo_temp_name(tmpfilename)
986                     self.report_destination(filename)
987                 except (OSError, IOError) as err:
988                     self.report_error(u'unable to open for writing: %s' % str(err))
989                     return False
990             try:
991                 stream.write(data_block)
992             except (IOError, OSError) as err:
993                 self.to_stderr(u"\n")
994                 self.report_error(u'unable to write data: %s' % str(err))
995                 return False
996             if not self.params.get('noresizebuffer', False):
997                 block_size = self.best_block_size(after - before, len(data_block))
998
999             # Progress message
1000             speed_str = self.calc_speed(start, time.time(), byte_counter - resume_len)
1001             if data_len is None:
1002                 self.report_progress('Unknown %', data_len_str, speed_str, 'Unknown ETA')
1003             else:
1004                 percent_str = self.calc_percent(byte_counter, data_len)
1005                 eta_str = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
1006                 self.report_progress(percent_str, data_len_str, speed_str, eta_str)
1007
1008             self._hook_progress({
1009                 'downloaded_bytes': byte_counter,
1010                 'total_bytes': data_len,
1011                 'tmpfilename': tmpfilename,
1012                 'filename': filename,
1013                 'status': 'downloading',
1014             })
1015
1016             # Apply rate limit
1017             self.slow_down(start, byte_counter - resume_len)
1018
1019         if stream is None:
1020             self.to_stderr(u"\n")
1021             self.report_error(u'Did not get any data blocks')
1022             return False
1023         stream.close()
1024         self.report_finish()
1025         if data_len is not None and byte_counter != data_len:
1026             raise ContentTooShortError(byte_counter, int(data_len))
1027         self.try_rename(tmpfilename, filename)
1028
1029         # Update file modification time
1030         if self.params.get('updatetime', True):
1031             info_dict['filetime'] = self.try_utime(filename, data.info().get('last-modified', None))
1032
1033         self._hook_progress({
1034             'downloaded_bytes': byte_counter,
1035             'total_bytes': byte_counter,
1036             'filename': filename,
1037             'status': 'finished',
1038         })
1039
1040         return True
1041
1042     def _hook_progress(self, status):
1043         for ph in self._progress_hooks:
1044             ph(status)
1045
1046     def add_progress_hook(self, ph):
1047         """ ph gets called on download progress, with a dictionary with the entries
1048         * filename: The final filename
1049         * status: One of "downloading" and "finished"
1050
1051         It can also have some of the following entries:
1052
1053         * downloaded_bytes: Bytes on disks
1054         * total_bytes: Total bytes, None if unknown
1055         * tmpfilename: The filename we're currently writing to
1056
1057         Hooks are guaranteed to be called at least once (with status "finished")
1058         if the download is successful.
1059         """
1060         self._progress_hooks.append(ph)