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