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