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