adding the script hook
[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 os
8 import re
9 import socket
10 import subprocess
11 import sys
12 import time
13
14 if os.name == 'nt':
15     import ctypes
16
17 from .utils import *
18
19
20 class FileDownloader(object):
21     """File Downloader class.
22
23     File downloader objects are the ones responsible of downloading the
24     actual video file and writing it to disk if the user has requested
25     it, among some other tasks. In most cases there should be one per
26     program. As, given a video URL, the downloader doesn't know how to
27     extract all the needed information, task that InfoExtractors do, it
28     has to pass the URL to one of them.
29
30     For this, file downloader objects have a method that allows
31     InfoExtractors to be registered in a given order. When it is passed
32     a URL, the file downloader handles it to the first InfoExtractor it
33     finds that reports being able to handle it. The InfoExtractor extracts
34     all the information about the video or videos the URL refers to, and
35     asks the FileDownloader to process the video information, possibly
36     downloading the video.
37
38     File downloaders accept a lot of parameters. In order not to saturate
39     the object constructor with arguments, it receives a dictionary of
40     options instead. These options are available through the params
41     attribute for the InfoExtractors to use. The FileDownloader also
42     registers itself as the downloader in charge for the InfoExtractors
43     that are added to it, so this is a "mutual registration".
44
45     Available options:
46
47     username:          Username for authentication purposes.
48     password:          Password for authentication purposes.
49     usenetrc:          Use netrc for authentication instead.
50     quiet:             Do not print messages to stdout.
51     forceurl:          Force printing final URL.
52     forcetitle:        Force printing title.
53     forcethumbnail:    Force printing thumbnail URL.
54     forcedescription:  Force printing description.
55     forcefilename:     Force printing final filename.
56     simulate:          Do not download the video files.
57     format:            Video format code.
58     format_limit:      Highest quality format to try.
59     outtmpl:           Template for output names.
60     restrictfilenames: Do not allow "&" and spaces in file names
61     ignoreerrors:      Do not stop on download errors.
62     ratelimit:         Download speed limit, in bytes/sec.
63     nooverwrites:      Prevent overwriting files.
64     retries:           Number of times to retry for HTTP error 5xx
65     buffersize:        Size of download buffer in bytes.
66     noresizebuffer:    Do not automatically resize the download buffer.
67     continuedl:        Try to continue downloads if possible.
68     noprogress:        Do not print the progress bar.
69     playliststart:     Playlist item to start at.
70     playlistend:       Playlist item to end at.
71     matchtitle:        Download only matching titles.
72     rejecttitle:       Reject downloads for matching titles.
73     logtostderr:       Log messages to stderr instead of stdout.
74     consoletitle:      Display progress in console window's titlebar.
75     nopart:            Do not use temporary .part files.
76     updatetime:        Use the Last-modified header to set output file timestamps.
77     writedescription:  Write the video description to a .description file
78     writeinfojson:     Write the video description to a .info.json file
79     writesubtitles:    Write the video subtitles to a .srt file
80     subtitleslang:     Language of the subtitles to download
81     """
82
83     params = None
84     _ies = []
85     _pps = []
86     _download_retcode = None
87     _num_downloads = None
88     _screen_file = None
89
90     def __init__(self, params):
91         """Create a FileDownloader object with the given options."""
92         self._ies = []
93         self._pps = []
94         self._download_retcode = 0
95         self._num_downloads = 0
96         self._screen_file = [sys.stdout, sys.stderr][params.get('logtostderr', False)]
97         self.params = params
98
99         if '%(stitle)s' in self.params['outtmpl']:
100             self.to_stderr(u'WARNING: %(stitle)s is deprecated. Use the %(title)s and the --restrict-filenames flag(which also secures %(uploader)s et al) instead.')
101
102     @staticmethod
103     def format_bytes(bytes):
104         if bytes is None:
105             return 'N/A'
106         if type(bytes) is str:
107             bytes = float(bytes)
108         if bytes == 0.0:
109             exponent = 0
110         else:
111             exponent = int(math.log(bytes, 1024.0))
112         suffix = 'bkMGTPEZY'[exponent]
113         converted = float(bytes) / float(1024 ** exponent)
114         return '%.2f%s' % (converted, suffix)
115
116     @staticmethod
117     def calc_percent(byte_counter, data_len):
118         if data_len is None:
119             return '---.-%'
120         return '%6s' % ('%3.1f%%' % (float(byte_counter) / float(data_len) * 100.0))
121
122     @staticmethod
123     def calc_eta(start, now, total, current):
124         if total is None:
125             return '--:--'
126         dif = now - start
127         if current == 0 or dif < 0.001: # One millisecond
128             return '--:--'
129         rate = float(current) / dif
130         eta = int((float(total) - float(current)) / rate)
131         (eta_mins, eta_secs) = divmod(eta, 60)
132         if eta_mins > 99:
133             return '--:--'
134         return '%02d:%02d' % (eta_mins, eta_secs)
135
136     @staticmethod
137     def calc_speed(start, now, bytes):
138         dif = now - start
139         if bytes == 0 or dif < 0.001: # One millisecond
140             return '%10s' % '---b/s'
141         return '%10s' % ('%s/s' % FileDownloader.format_bytes(float(bytes) / dif))
142
143     @staticmethod
144     def best_block_size(elapsed_time, bytes):
145         new_min = max(bytes / 2.0, 1.0)
146         new_max = min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB
147         if elapsed_time < 0.001:
148             return int(new_max)
149         rate = bytes / elapsed_time
150         if rate > new_max:
151             return int(new_max)
152         if rate < new_min:
153             return int(new_min)
154         return int(rate)
155
156     @staticmethod
157     def parse_bytes(bytestr):
158         """Parse a string indicating a byte quantity into an integer."""
159         matchobj = re.match(r'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr)
160         if matchobj is None:
161             return None
162         number = float(matchobj.group(1))
163         multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
164         return int(round(number * multiplier))
165
166     def add_info_extractor(self, ie):
167         """Add an InfoExtractor object to the end of the list."""
168         self._ies.append(ie)
169         ie.set_downloader(self)
170
171     def add_post_processor(self, pp):
172         """Add a PostProcessor object to the end of the chain."""
173         self._pps.append(pp)
174         pp.set_downloader(self)
175
176     def to_screen(self, message, skip_eol=False):
177         """Print message to stdout if not in quiet mode."""
178         assert type(message) == type(u'')
179         if not self.params.get('quiet', False):
180             terminator = [u'\n', u''][skip_eol]
181             output = message + terminator
182             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
183                 output = output.encode(preferredencoding(), 'ignore')
184             self._screen_file.write(output)
185             self._screen_file.flush()
186
187     def to_stderr(self, message):
188         """Print message to stderr."""
189         assert type(message) == type(u'')
190         output = message + u'\n'
191         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
192             output = output.encode(preferredencoding())
193         sys.stderr.write(output)
194
195     def to_cons_title(self, message):
196         """Set console/terminal window title to message."""
197         if not self.params.get('consoletitle', False):
198             return
199         if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
200             # c_wchar_p() might not be necessary if `message` is
201             # already of type unicode()
202             ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
203         elif 'TERM' in os.environ:
204             sys.stderr.write('\033]0;%s\007' % message.encode(preferredencoding()))
205
206     def fixed_template(self):
207         """Checks if the output template is fixed."""
208         return (re.search(u'(?u)%\\(.+?\\)s', self.params['outtmpl']) is None)
209
210     def trouble(self, message=None):
211         """Determine action to take when a download problem appears.
212
213         Depending on if the downloader has been configured to ignore
214         download errors or not, this method may throw an exception or
215         not when errors are found, after printing the message.
216         """
217         if message is not None:
218             self.to_stderr(message)
219         if not self.params.get('ignoreerrors', False):
220             raise DownloadError(message)
221         self._download_retcode = 1
222
223     def slow_down(self, start_time, byte_counter):
224         """Sleep if the download speed is over the rate limit."""
225         rate_limit = self.params.get('ratelimit', None)
226         if rate_limit is None or byte_counter == 0:
227             return
228         now = time.time()
229         elapsed = now - start_time
230         if elapsed <= 0.0:
231             return
232         speed = float(byte_counter) / elapsed
233         if speed > rate_limit:
234             time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
235
236     def temp_name(self, filename):
237         """Returns a temporary filename for the given filename."""
238         if self.params.get('nopart', False) or filename == u'-' or \
239                 (os.path.exists(encodeFilename(filename)) and not os.path.isfile(encodeFilename(filename))):
240             return filename
241         return filename + u'.part'
242
243     def undo_temp_name(self, filename):
244         if filename.endswith(u'.part'):
245             return filename[:-len(u'.part')]
246         return filename
247
248     def try_rename(self, old_filename, new_filename):
249         try:
250             if old_filename == new_filename:
251                 return
252             os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
253         except (IOError, OSError) as err:
254             self.trouble(u'ERROR: unable to rename file')
255
256     def try_utime(self, filename, last_modified_hdr):
257         """Try to set the last-modified time of the given file."""
258         if last_modified_hdr is None:
259             return
260         if not os.path.isfile(encodeFilename(filename)):
261             return
262         timestr = last_modified_hdr
263         if timestr is None:
264             return
265         filetime = timeconvert(timestr)
266         if filetime is None:
267             return filetime
268         try:
269             os.utime(filename, (time.time(), filetime))
270         except:
271             pass
272         return filetime
273
274     def report_writedescription(self, descfn):
275         """ Report that the description file is being written """
276         self.to_screen(u'[info] Writing video description to: ' + descfn)
277
278     def report_writesubtitles(self, srtfn):
279         """ Report that the subtitles file is being written """
280         self.to_screen(u'[info] Writing video subtitles to: ' + srtfn)
281
282     def report_writeinfojson(self, infofn):
283         """ Report that the metadata file has been written """
284         self.to_screen(u'[info] Video description metadata as JSON to: ' + infofn)
285
286     def report_destination(self, filename):
287         """Report destination filename."""
288         self.to_screen(u'[download] Destination: ' + filename)
289
290     def report_progress(self, percent_str, data_len_str, speed_str, eta_str):
291         """Report download progress."""
292         if self.params.get('noprogress', False):
293             return
294         self.to_screen(u'\r[download] %s of %s at %s ETA %s' %
295                 (percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
296         self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' %
297                 (percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))
298
299     def report_resuming_byte(self, resume_len):
300         """Report attempt to resume at given byte."""
301         self.to_screen(u'[download] Resuming download at byte %s' % resume_len)
302
303     def report_retry(self, count, retries):
304         """Report retry in case of HTTP error 5xx"""
305         self.to_screen(u'[download] Got server HTTP error. Retrying (attempt %d of %d)...' % (count, retries))
306
307     def report_file_already_downloaded(self, file_name):
308         """Report file has already been fully downloaded."""
309         try:
310             self.to_screen(u'[download] %s has already been downloaded' % file_name)
311         except (UnicodeEncodeError) as err:
312             self.to_screen(u'[download] The file has already been downloaded')
313
314     def report_unable_to_resume(self):
315         """Report it was impossible to resume download."""
316         self.to_screen(u'[download] Unable to resume')
317
318     def report_finish(self):
319         """Report download finished."""
320         if self.params.get('noprogress', False):
321             self.to_screen(u'[download] Download completed')
322         else:
323             self.to_screen(u'')
324
325     def increment_downloads(self):
326         """Increment the ordinal that assigns a number to each file."""
327         self._num_downloads += 1
328
329     def prepare_filename(self, info_dict):
330         """Generate the output filename."""
331         try:
332             template_dict = dict(info_dict)
333
334             template_dict['epoch'] = int(time.time())
335             template_dict['autonumber'] = u'%05d' % self._num_downloads
336
337             template_dict = dict((key, u'NA' if val is None else val) for key, val in template_dict.items())
338             template_dict = dict((k, sanitize_filename(compat_str(v), self.params.get('restrictfilenames'))) for k,v in template_dict.items())
339
340             filename = self.params['outtmpl'] % template_dict
341             return filename
342         except (ValueError, KeyError) as err:
343             self.trouble(u'ERROR: invalid system charset or erroneous output template')
344             return None
345
346     def _match_entry(self, info_dict):
347         """ Returns None iff the file should be downloaded """
348
349         title = info_dict['title']
350         matchtitle = self.params.get('matchtitle', False)
351         if matchtitle:
352             matchtitle = matchtitle.decode('utf8')
353             if not re.search(matchtitle, title, re.IGNORECASE):
354                 return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
355         rejecttitle = self.params.get('rejecttitle', False)
356         if rejecttitle:
357             rejecttitle = rejecttitle.decode('utf8')
358             if re.search(rejecttitle, title, re.IGNORECASE):
359                 return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
360         return None
361
362     def process_info(self, info_dict):
363         """Process a single dictionary returned by an InfoExtractor."""
364
365         # Keep for backwards compatibility
366         info_dict['stitle'] = info_dict['title']
367
368         if not 'format' in info_dict:
369             info_dict['format'] = info_dict['ext']
370
371         reason = self._match_entry(info_dict)
372         if reason is not None:
373             self.to_screen(u'[download] ' + reason)
374             return
375
376         max_downloads = self.params.get('max_downloads')
377         if max_downloads is not None:
378             if self._num_downloads > int(max_downloads):
379                 raise MaxDownloadsReached()
380
381         filename = self.prepare_filename(info_dict)
382
383         # Forced printings
384         if self.params.get('forcetitle', False):
385             compat_print(info_dict['title'])
386         if self.params.get('forceurl', False):
387             compat_print(info_dict['url'])
388         if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
389             compat_print(info_dict['thumbnail'])
390         if self.params.get('forcedescription', False) and 'description' in info_dict:
391             compat_print(info_dict['description'])
392         if self.params.get('forcefilename', False) and filename is not None:
393             compat_print(filename)
394         if self.params.get('forceformat', False):
395             compat_print(info_dict['format'])
396
397         # Do nothing else if in simulate mode
398         if self.params.get('simulate', False):
399             return
400
401         if filename is None:
402             return
403
404         try:
405             dn = os.path.dirname(encodeFilename(filename))
406             if dn != '' and not os.path.exists(dn): # dn is already encoded
407                 os.makedirs(dn)
408         except (OSError, IOError) as err:
409             self.trouble(u'ERROR: unable to create directory ' + compat_str(err))
410             return
411
412         if self.params.get('writedescription', False):
413             try:
414                 descfn = filename + u'.description'
415                 self.report_writedescription(descfn)
416                 descfile = open(encodeFilename(descfn), 'wb')
417                 try:
418                     descfile.write(info_dict['description'].encode('utf-8'))
419                 finally:
420                     descfile.close()
421             except (OSError, IOError):
422                 self.trouble(u'ERROR: Cannot write description file ' + descfn)
423                 return
424
425         if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
426             # subtitles download errors are already managed as troubles in relevant IE
427             # that way it will silently go on when used with unsupporting IE
428             try:
429                 srtfn = filename.rsplit('.', 1)[0] + u'.srt'
430                 self.report_writesubtitles(srtfn)
431                 srtfile = open(encodeFilename(srtfn), 'wb')
432                 try:
433                     srtfile.write(info_dict['subtitles'].encode('utf-8'))
434                 finally:
435                     srtfile.close()
436             except (OSError, IOError):
437                 self.trouble(u'ERROR: Cannot write subtitles file ' + descfn)
438                 return
439
440         if self.params.get('writeinfojson', False):
441             infofn = filename + u'.info.json'
442             self.report_writeinfojson(infofn)
443             try:
444                 json.dump
445             except (NameError,AttributeError):
446                 self.trouble(u'ERROR: No JSON encoder found. Update to Python 2.6+, setup a json module, or leave out --write-info-json.')
447                 return
448             try:
449                 infof = open(encodeFilename(infofn), 'wb')
450                 try:
451                     json_info_dict = dict((k,v) for k,v in info_dict.iteritems() if not k in ('urlhandle',))
452                     json.dump(json_info_dict, infof)
453                 finally:
454                     infof.close()
455             except (OSError, IOError):
456                 self.trouble(u'ERROR: Cannot write metadata to JSON file ' + infofn)
457                 return
458
459         if not self.params.get('skip_download', False):
460             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
461                 success = True
462             else:
463                 try:
464                     success = self._do_download(filename, info_dict)
465                 except (OSError, IOError) as err:
466                     raise UnavailableVideoError
467                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
468                     self.trouble(u'ERROR: unable to download video data: %s' % str(err))
469                     return
470                 except (ContentTooShortError, ) as err:
471                     self.trouble(u'ERROR: content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
472                     return
473
474             if success:
475                 try:
476                     self.post_process(filename, info_dict)
477                 except (PostProcessingError) as err:
478                     self.trouble(u'ERROR: postprocessing: %s' % str(err))
479                     return
480
481     def download(self, url_list):
482         """Download a given list of URLs."""
483         if len(url_list) > 1 and self.fixed_template():
484             raise SameFileError(self.params['outtmpl'])
485
486         for url in url_list:
487             suitable_found = False
488             for ie in self._ies:
489                 # Go to next InfoExtractor if not suitable
490                 if not ie.suitable(url):
491                     continue
492
493                 # Warn if the _WORKING attribute is False
494                 if not ie.working():
495                     self.trouble(u'WARNING: the program functionality for this site has been marked as broken, '
496                                  u'and will probably not work. If you want to go on, use the -i option.')
497
498                 # Suitable InfoExtractor found
499                 suitable_found = True
500
501                 # Extract information from URL and process it
502                 videos = ie.extract(url)
503                 for video in videos or []:
504                     video['extractor'] = ie.IE_NAME
505                     try:
506                         self.increment_downloads()
507                         self.process_info(video)
508                     except UnavailableVideoError:
509                         self.trouble(u'\nERROR: unable to download video')
510
511                 # Suitable InfoExtractor had been found; go to next URL
512                 break
513
514             if not suitable_found:
515                 self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url)
516
517         return self._download_retcode
518
519     def post_process(self, filename, ie_info):
520         """Run the postprocessing chain on the given file."""
521         info = dict(ie_info)
522         info['filepath'] = filename
523         for pp in self._pps:
524             info = pp.run(info)
525             if info is None:
526                 break
527
528     def _download_with_rtmpdump(self, filename, url, player_url):
529         self.report_destination(filename)
530         tmpfilename = self.temp_name(filename)
531
532         # Check for rtmpdump first
533         try:
534             subprocess.call(['rtmpdump', '-h'], stdout=(file(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
535         except (OSError, IOError):
536             self.trouble(u'ERROR: RTMP download detected but "rtmpdump" could not be run')
537             return False
538
539         # Download using rtmpdump. rtmpdump returns exit code 2 when
540         # the connection was interrumpted and resuming appears to be
541         # possible. This is part of rtmpdump's normal usage, AFAIK.
542         basic_args = ['rtmpdump', '-q'] + [[], ['-W', player_url]][player_url is not None] + ['-r', url, '-o', tmpfilename]
543         args = basic_args + [[], ['-e', '-k', '1']][self.params.get('continuedl', False)]
544         if self.params.get('verbose', False):
545             try:
546                 import pipes
547                 shell_quote = lambda args: ' '.join(map(pipes.quote, args))
548             except ImportError:
549                 shell_quote = repr
550             self.to_screen(u'[debug] rtmpdump command line: ' + shell_quote(args))
551         retval = subprocess.call(args)
552         while retval == 2 or retval == 1:
553             prevsize = os.path.getsize(encodeFilename(tmpfilename))
554             self.to_screen(u'\r[rtmpdump] %s bytes' % prevsize, skip_eol=True)
555             time.sleep(5.0) # This seems to be needed
556             retval = subprocess.call(basic_args + ['-e'] + [[], ['-k', '1']][retval == 1])
557             cursize = os.path.getsize(encodeFilename(tmpfilename))
558             if prevsize == cursize and retval == 1:
559                 break
560              # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those
561             if prevsize == cursize and retval == 2 and cursize > 1024:
562                 self.to_screen(u'\r[rtmpdump] Could not download the whole video. This can happen for some advertisements.')
563                 retval = 0
564                 break
565         if retval == 0:
566             self.to_screen(u'\r[rtmpdump] %s bytes' % os.path.getsize(encodeFilename(tmpfilename)))
567             self.try_rename(tmpfilename, filename)
568             return True
569         else:
570             self.trouble(u'\nERROR: rtmpdump exited with code %d' % retval)
571             return False
572
573     def _do_download(self, filename, info_dict):
574         url = info_dict['url']
575         player_url = info_dict.get('player_url', None)
576
577         # Check file already present
578         if self.params.get('continuedl', False) and os.path.isfile(encodeFilename(filename)) and not self.params.get('nopart', False):
579             self.report_file_already_downloaded(filename)
580             return True
581
582         # Attempt to download using rtmpdump
583         if url.startswith('rtmp'):
584             return self._download_with_rtmpdump(filename, url, player_url)
585
586         tmpfilename = self.temp_name(filename)
587         stream = None
588
589         # Do not include the Accept-Encoding header
590         headers = {'Youtubedl-no-compression': 'True'}
591         basic_request = compat_urllib_request.Request(url, None, headers)
592         request = compat_urllib_request.Request(url, None, headers)
593
594         # Establish possible resume length
595         if os.path.isfile(encodeFilename(tmpfilename)):
596             resume_len = os.path.getsize(encodeFilename(tmpfilename))
597         else:
598             resume_len = 0
599
600         open_mode = 'wb'
601         if resume_len != 0:
602             if self.params.get('continuedl', False):
603                 self.report_resuming_byte(resume_len)
604                 request.add_header('Range','bytes=%d-' % resume_len)
605                 open_mode = 'ab'
606             else:
607                 resume_len = 0
608
609         count = 0
610         retries = self.params.get('retries', 0)
611         while count <= retries:
612             # Establish connection
613             try:
614                 if count == 0 and 'urlhandle' in info_dict:
615                     data = info_dict['urlhandle']
616                 data = compat_urllib_request.urlopen(request)
617                 break
618             except (compat_urllib_error.HTTPError, ) as err:
619                 if (err.code < 500 or err.code >= 600) and err.code != 416:
620                     # Unexpected HTTP error
621                     raise
622                 elif err.code == 416:
623                     # Unable to resume (requested range not satisfiable)
624                     try:
625                         # Open the connection again without the range header
626                         data = compat_urllib_request.urlopen(basic_request)
627                         content_length = data.info()['Content-Length']
628                     except (compat_urllib_error.HTTPError, ) as err:
629                         if err.code < 500 or err.code >= 600:
630                             raise
631                     else:
632                         # Examine the reported length
633                         if (content_length is not None and
634                                 (resume_len - 100 < int(content_length) < resume_len + 100)):
635                             # The file had already been fully downloaded.
636                             # Explanation to the above condition: in issue #175 it was revealed that
637                             # YouTube sometimes adds or removes a few bytes from the end of the file,
638                             # changing the file size slightly and causing problems for some users. So
639                             # I decided to implement a suggested change and consider the file
640                             # completely downloaded if the file size differs less than 100 bytes from
641                             # the one in the hard drive.
642                             self.report_file_already_downloaded(filename)
643                             self.try_rename(tmpfilename, filename)
644                             return True
645                         else:
646                             # The length does not match, we start the download over
647                             self.report_unable_to_resume()
648                             open_mode = 'wb'
649                             break
650             # Retry
651             count += 1
652             if count <= retries:
653                 self.report_retry(count, retries)
654
655         if count > retries:
656             self.trouble(u'ERROR: giving up after %s retries' % retries)
657             return False
658
659         data_len = data.info().get('Content-length', None)
660         if data_len is not None:
661             data_len = int(data_len) + resume_len
662         data_len_str = self.format_bytes(data_len)
663         byte_counter = 0 + resume_len
664         block_size = self.params.get('buffersize', 1024)
665         start = time.time()
666         while True:
667             # Download and write
668             before = time.time()
669             data_block = data.read(block_size)
670             after = time.time()
671             if len(data_block) == 0:
672                 break
673             byte_counter += len(data_block)
674
675             # Open file just in time
676             if stream is None:
677                 try:
678                     (stream, tmpfilename) = sanitize_open(tmpfilename, open_mode)
679                     assert stream is not None
680                     filename = self.undo_temp_name(tmpfilename)
681                     self.report_destination(filename)
682                 except (OSError, IOError) as err:
683                     self.trouble(u'ERROR: unable to open for writing: %s' % str(err))
684                     return False
685             try:
686                 stream.write(data_block)
687             except (IOError, OSError) as err:
688                 self.trouble(u'\nERROR: unable to write data: %s' % str(err))
689                 return False
690             if not self.params.get('noresizebuffer', False):
691                 block_size = self.best_block_size(after - before, len(data_block))
692
693             # Progress message
694             speed_str = self.calc_speed(start, time.time(), byte_counter - resume_len)
695             if data_len is None:
696                 self.report_progress('Unknown %', data_len_str, speed_str, 'Unknown ETA')
697             else:
698                 percent_str = self.calc_percent(byte_counter, data_len)
699                 eta_str = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
700                 self.report_progress(percent_str, data_len_str, speed_str, eta_str)
701
702             # Apply rate limit
703             self.slow_down(start, byte_counter - resume_len)
704
705         if stream is None:
706             self.trouble(u'\nERROR: Did not get any data blocks')
707             return False
708         stream.close()
709         self.report_finish()
710         if data_len is not None and byte_counter != data_len:
711             raise ContentTooShortError(byte_counter, int(data_len))
712         self.try_rename(tmpfilename, filename)
713
714         # Update file modification time
715         if self.params.get('updatetime', True):
716             info_dict['filetime'] = self.try_utime(filename, data.info().get('last-modified', None))
717
718         return True