Merge 'rg3/master' into fork_master
[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             sanitize = lambda k,v: sanitize_filename(
338                 u'NA' if v is None else compat_str(v),
339                 restricted=self.params.get('restrictfilenames'),
340                 is_id=(k==u'id'))
341             template_dict = dict((k, sanitize(k, v)) for k,v in template_dict.items())
342
343             filename = self.params['outtmpl'] % template_dict
344             return filename
345         except (ValueError, KeyError) as err:
346             self.trouble(u'ERROR: invalid system charset or erroneous output template')
347             return None
348
349     def _match_entry(self, info_dict):
350         """ Returns None iff the file should be downloaded """
351
352         title = info_dict['title']
353         matchtitle = self.params.get('matchtitle', False)
354         if matchtitle:
355             matchtitle = matchtitle.decode('utf8')
356             if not re.search(matchtitle, title, re.IGNORECASE):
357                 return u'[download] "' + title + '" title did not match pattern "' + matchtitle + '"'
358         rejecttitle = self.params.get('rejecttitle', False)
359         if rejecttitle:
360             rejecttitle = rejecttitle.decode('utf8')
361             if re.search(rejecttitle, title, re.IGNORECASE):
362                 return u'"' + title + '" title matched reject pattern "' + rejecttitle + '"'
363         return None
364
365     def process_info(self, info_dict):
366         """Process a single dictionary returned by an InfoExtractor."""
367
368         # Keep for backwards compatibility
369         info_dict['stitle'] = info_dict['title']
370
371         if not 'format' in info_dict:
372             info_dict['format'] = info_dict['ext']
373
374         reason = self._match_entry(info_dict)
375         if reason is not None:
376             self.to_screen(u'[download] ' + reason)
377             return
378
379         max_downloads = self.params.get('max_downloads')
380         if max_downloads is not None:
381             if self._num_downloads > int(max_downloads):
382                 raise MaxDownloadsReached()
383
384         filename = self.prepare_filename(info_dict)
385
386         # Forced printings
387         if self.params.get('forcetitle', False):
388             compat_print(info_dict['title'])
389         if self.params.get('forceurl', False):
390             compat_print(info_dict['url'])
391         if self.params.get('forcethumbnail', False) and 'thumbnail' in info_dict:
392             compat_print(info_dict['thumbnail'])
393         if self.params.get('forcedescription', False) and 'description' in info_dict:
394             compat_print(info_dict['description'])
395         if self.params.get('forcefilename', False) and filename is not None:
396             compat_print(filename)
397         if self.params.get('forceformat', False):
398             compat_print(info_dict['format'])
399
400         # Do nothing else if in simulate mode
401         if self.params.get('simulate', False):
402             return
403
404         if filename is None:
405             return
406
407         try:
408             dn = os.path.dirname(encodeFilename(filename))
409             if dn != '' and not os.path.exists(dn): # dn is already encoded
410                 os.makedirs(dn)
411         except (OSError, IOError) as err:
412             self.trouble(u'ERROR: unable to create directory ' + compat_str(err))
413             return
414
415         if self.params.get('writedescription', False):
416             try:
417                 descfn = filename + u'.description'
418                 self.report_writedescription(descfn)
419                 descfile = open(encodeFilename(descfn), 'wb')
420                 try:
421                     descfile.write(info_dict['description'].encode('utf-8'))
422                 finally:
423                     descfile.close()
424             except (OSError, IOError):
425                 self.trouble(u'ERROR: Cannot write description file ' + descfn)
426                 return
427
428         if self.params.get('writesubtitles', False) and 'subtitles' in info_dict and info_dict['subtitles']:
429             # subtitles download errors are already managed as troubles in relevant IE
430             # that way it will silently go on when used with unsupporting IE
431             try:
432                 srtfn = filename.rsplit('.', 1)[0] + u'.srt'
433                 self.report_writesubtitles(srtfn)
434                 srtfile = open(encodeFilename(srtfn), 'wb')
435                 try:
436                     srtfile.write(info_dict['subtitles'].encode('utf-8'))
437                 finally:
438                     srtfile.close()
439             except (OSError, IOError):
440                 self.trouble(u'ERROR: Cannot write subtitles file ' + descfn)
441                 return
442
443         if self.params.get('writeinfojson', False):
444             infofn = filename + u'.info.json'
445             self.report_writeinfojson(infofn)
446             try:
447                 json.dump
448             except (NameError,AttributeError):
449                 self.trouble(u'ERROR: No JSON encoder found. Update to Python 2.6+, setup a json module, or leave out --write-info-json.')
450                 return
451             try:
452                 infof = open(encodeFilename(infofn), 'wb')
453                 try:
454                     json_info_dict = dict((k,v) for k,v in info_dict.iteritems() if not k in ('urlhandle',))
455                     json.dump(json_info_dict, infof)
456                 finally:
457                     infof.close()
458             except (OSError, IOError):
459                 self.trouble(u'ERROR: Cannot write metadata to JSON file ' + infofn)
460                 return
461
462         if not self.params.get('skip_download', False):
463             if self.params.get('nooverwrites', False) and os.path.exists(encodeFilename(filename)):
464                 success = True
465             else:
466                 try:
467                     success = self._do_download(filename, info_dict)
468                 except (OSError, IOError) as err:
469                     raise UnavailableVideoError()
470                 except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
471                     self.trouble(u'ERROR: unable to download video data: %s' % str(err))
472                     return
473                 except (ContentTooShortError, ) as err:
474                     self.trouble(u'ERROR: content too short (expected %s bytes and served %s)' % (err.expected, err.downloaded))
475                     return
476
477             if success:
478                 try:
479                     self.post_process(filename, info_dict)
480                 except (PostProcessingError) as err:
481                     self.trouble(u'ERROR: postprocessing: %s' % str(err))
482                     return
483
484     def download(self, url_list):
485         """Download a given list of URLs."""
486         if len(url_list) > 1 and self.fixed_template():
487             raise SameFileError(self.params['outtmpl'])
488
489         for url in url_list:
490             suitable_found = False
491             for ie in self._ies:
492                 # Go to next InfoExtractor if not suitable
493                 if not ie.suitable(url):
494                     continue
495
496                 # Warn if the _WORKING attribute is False
497                 if not ie.working():
498                     self.trouble(u'WARNING: the program functionality for this site has been marked as broken, '
499                                  u'and will probably not work. If you want to go on, use the -i option.')
500
501                 # Suitable InfoExtractor found
502                 suitable_found = True
503
504                 # Extract information from URL and process it
505                 videos = ie.extract(url)
506                 for video in videos or []:
507                     video['extractor'] = ie.IE_NAME
508                     try:
509                         self.increment_downloads()
510                         self.process_info(video)
511                     except UnavailableVideoError:
512                         self.trouble(u'\nERROR: unable to download video')
513
514                 # Suitable InfoExtractor had been found; go to next URL
515                 break
516
517             if not suitable_found:
518                 self.trouble(u'ERROR: no suitable InfoExtractor: %s' % url)
519
520         return self._download_retcode
521
522     def post_process(self, filename, ie_info):
523         """Run the postprocessing chain on the given file."""
524         info = dict(ie_info)
525         info['filepath'] = filename
526         for pp in self._pps:
527             info = pp.run(info)
528             if info is None:
529                 break
530
531     def _download_with_rtmpdump(self, filename, url, player_url):
532         self.report_destination(filename)
533         tmpfilename = self.temp_name(filename)
534
535         # Check for rtmpdump first
536         try:
537             subprocess.call(['rtmpdump', '-h'], stdout=(file(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
538         except (OSError, IOError):
539             self.trouble(u'ERROR: RTMP download detected but "rtmpdump" could not be run')
540             return False
541
542         # Download using rtmpdump. rtmpdump returns exit code 2 when
543         # the connection was interrumpted and resuming appears to be
544         # possible. This is part of rtmpdump's normal usage, AFAIK.
545         basic_args = ['rtmpdump', '-q'] + [[], ['-W', player_url]][player_url is not None] + ['-r', url, '-o', tmpfilename]
546         args = basic_args + [[], ['-e', '-k', '1']][self.params.get('continuedl', False)]
547         if self.params.get('verbose', False):
548             try:
549                 import pipes
550                 shell_quote = lambda args: ' '.join(map(pipes.quote, args))
551             except ImportError:
552                 shell_quote = repr
553             self.to_screen(u'[debug] rtmpdump command line: ' + shell_quote(args))
554         retval = subprocess.call(args)
555         while retval == 2 or retval == 1:
556             prevsize = os.path.getsize(encodeFilename(tmpfilename))
557             self.to_screen(u'\r[rtmpdump] %s bytes' % prevsize, skip_eol=True)
558             time.sleep(5.0) # This seems to be needed
559             retval = subprocess.call(basic_args + ['-e'] + [[], ['-k', '1']][retval == 1])
560             cursize = os.path.getsize(encodeFilename(tmpfilename))
561             if prevsize == cursize and retval == 1:
562                 break
563              # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those
564             if prevsize == cursize and retval == 2 and cursize > 1024:
565                 self.to_screen(u'\r[rtmpdump] Could not download the whole video. This can happen for some advertisements.')
566                 retval = 0
567                 break
568         if retval == 0:
569             self.to_screen(u'\r[rtmpdump] %s bytes' % os.path.getsize(encodeFilename(tmpfilename)))
570             self.try_rename(tmpfilename, filename)
571             return True
572         else:
573             self.trouble(u'\nERROR: rtmpdump exited with code %d' % retval)
574             return False
575
576     def _do_download(self, filename, info_dict):
577         url = info_dict['url']
578         player_url = info_dict.get('player_url', None)
579
580         # Check file already present
581         if self.params.get('continuedl', False) and os.path.isfile(encodeFilename(filename)) and not self.params.get('nopart', False):
582             self.report_file_already_downloaded(filename)
583             return True
584
585         # Attempt to download using rtmpdump
586         if url.startswith('rtmp'):
587             return self._download_with_rtmpdump(filename, url, player_url)
588
589         tmpfilename = self.temp_name(filename)
590         stream = None
591
592         # Do not include the Accept-Encoding header
593         headers = {'Youtubedl-no-compression': 'True'}
594         basic_request = compat_urllib_request.Request(url, None, headers)
595         request = compat_urllib_request.Request(url, None, headers)
596
597         # Establish possible resume length
598         if os.path.isfile(encodeFilename(tmpfilename)):
599             resume_len = os.path.getsize(encodeFilename(tmpfilename))
600         else:
601             resume_len = 0
602
603         open_mode = 'wb'
604         if resume_len != 0:
605             if self.params.get('continuedl', False):
606                 self.report_resuming_byte(resume_len)
607                 request.add_header('Range','bytes=%d-' % resume_len)
608                 open_mode = 'ab'
609             else:
610                 resume_len = 0
611
612         count = 0
613         retries = self.params.get('retries', 0)
614         while count <= retries:
615             # Establish connection
616             try:
617                 if count == 0 and 'urlhandle' in info_dict:
618                     data = info_dict['urlhandle']
619                 data = compat_urllib_request.urlopen(request)
620                 break
621             except (compat_urllib_error.HTTPError, ) as err:
622                 if (err.code < 500 or err.code >= 600) and err.code != 416:
623                     # Unexpected HTTP error
624                     raise
625                 elif err.code == 416:
626                     # Unable to resume (requested range not satisfiable)
627                     try:
628                         # Open the connection again without the range header
629                         data = compat_urllib_request.urlopen(basic_request)
630                         content_length = data.info()['Content-Length']
631                     except (compat_urllib_error.HTTPError, ) as err:
632                         if err.code < 500 or err.code >= 600:
633                             raise
634                     else:
635                         # Examine the reported length
636                         if (content_length is not None and
637                                 (resume_len - 100 < int(content_length) < resume_len + 100)):
638                             # The file had already been fully downloaded.
639                             # Explanation to the above condition: in issue #175 it was revealed that
640                             # YouTube sometimes adds or removes a few bytes from the end of the file,
641                             # changing the file size slightly and causing problems for some users. So
642                             # I decided to implement a suggested change and consider the file
643                             # completely downloaded if the file size differs less than 100 bytes from
644                             # the one in the hard drive.
645                             self.report_file_already_downloaded(filename)
646                             self.try_rename(tmpfilename, filename)
647                             return True
648                         else:
649                             # The length does not match, we start the download over
650                             self.report_unable_to_resume()
651                             open_mode = 'wb'
652                             break
653             # Retry
654             count += 1
655             if count <= retries:
656                 self.report_retry(count, retries)
657
658         if count > retries:
659             self.trouble(u'ERROR: giving up after %s retries' % retries)
660             return False
661
662         data_len = data.info().get('Content-length', None)
663         if data_len is not None:
664             data_len = int(data_len) + resume_len
665         data_len_str = self.format_bytes(data_len)
666         byte_counter = 0 + resume_len
667         block_size = self.params.get('buffersize', 1024)
668         start = time.time()
669         while True:
670             # Download and write
671             before = time.time()
672             data_block = data.read(block_size)
673             after = time.time()
674             if len(data_block) == 0:
675                 break
676             byte_counter += len(data_block)
677
678             # Open file just in time
679             if stream is None:
680                 try:
681                     (stream, tmpfilename) = sanitize_open(tmpfilename, open_mode)
682                     assert stream is not None
683                     filename = self.undo_temp_name(tmpfilename)
684                     self.report_destination(filename)
685                 except (OSError, IOError) as err:
686                     self.trouble(u'ERROR: unable to open for writing: %s' % str(err))
687                     return False
688             try:
689                 stream.write(data_block)
690             except (IOError, OSError) as err:
691                 self.trouble(u'\nERROR: unable to write data: %s' % str(err))
692                 return False
693             if not self.params.get('noresizebuffer', False):
694                 block_size = self.best_block_size(after - before, len(data_block))
695
696             # Progress message
697             speed_str = self.calc_speed(start, time.time(), byte_counter - resume_len)
698             if data_len is None:
699                 self.report_progress('Unknown %', data_len_str, speed_str, 'Unknown ETA')
700             else:
701                 percent_str = self.calc_percent(byte_counter, data_len)
702                 eta_str = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
703                 self.report_progress(percent_str, data_len_str, speed_str, eta_str)
704
705             # Apply rate limit
706             self.slow_down(start, byte_counter - resume_len)
707
708         if stream is None:
709             self.trouble(u'\nERROR: Did not get any data blocks')
710             return False
711         stream.close()
712         self.report_finish()
713         if data_len is not None and byte_counter != data_len:
714             raise ContentTooShortError(byte_counter, int(data_len))
715         self.try_rename(tmpfilename, filename)
716
717         # Update file modification time
718         if self.params.get('updatetime', True):
719             info_dict['filetime'] = self.try_utime(filename, data.info().get('last-modified', None))
720
721         return True