]>
Raphaƫl G. Git Repositories - youtubedl/blob - youtube_dl/FileDownloader.py
   2 # -*- coding: utf-8 -*- 
  20 class FileDownloader(object): 
  21         """File Downloader class. 
  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. 
  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. 
  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". 
  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         ignoreerrors:     Do not stop on download errors. 
  61         ratelimit:        Download speed limit, in bytes/sec. 
  62         nooverwrites:     Prevent overwriting files. 
  63         retries:          Number of times to retry for HTTP error 5xx 
  64         continuedl:       Try to continue downloads if possible. 
  65         noprogress:       Do not print the progress bar. 
  66         playliststart:    Playlist item to start at. 
  67         playlistend:      Playlist item to end at. 
  68         matchtitle:       Download only matching titles. 
  69         rejecttitle:      Reject downloads for matching titles. 
  70         logtostderr:      Log messages to stderr instead of stdout. 
  71         consoletitle:     Display progress in console window's titlebar. 
  72         nopart:           Do not use temporary .part files. 
  73         updatetime:       Use the Last-modified header to set output file timestamps. 
  74         writedescription: Write the video description to a .description file 
  75         writeinfojson:    Write the video description to a .info.json file 
  76         writesubtitles:   Write the video subtitles to a .srt file 
  77         subtitleslang:    Language of the subtitles to download 
  83         _download_retcode 
= None 
  87         def __init__(self
, params
): 
  88                 """Create a FileDownloader object with the given options.""" 
  91                 self
._download
_retcode 
= 0 
  92                 self
._num
_downloads 
= 0 
  93                 self
._screen
_file 
= [sys
.stdout
, sys
.stderr
][params
.get('logtostderr', False)] 
  97         def format_bytes(bytes): 
 100                 if type(bytes) is str: 
 105                         exponent 
= long(math
.log(bytes, 1024.0)) 
 106                 suffix 
= 'bkMGTPEZY'[exponent
] 
 107                 converted 
= float(bytes) / float(1024 ** exponent
) 
 108                 return '%.2f%s' % (converted
, suffix
) 
 111         def calc_percent(byte_counter
, data_len
): 
 114                 return '%6s' % ('%3.1f%%' % (float(byte_counter
) / float(data_len
) * 100.0)) 
 117         def calc_eta(start
, now
, total
, current
): 
 121                 if current 
== 0 or dif 
< 0.001: # One millisecond 
 123                 rate 
= float(current
) / dif
 
 124                 eta 
= long((float(total
) - float(current
)) / rate
) 
 125                 (eta_mins
, eta_secs
) = divmod(eta
, 60) 
 128                 return '%02d:%02d' % (eta_mins
, eta_secs
) 
 131         def calc_speed(start
, now
, bytes): 
 133                 if bytes == 0 or dif 
< 0.001: # One millisecond 
 134                         return '%10s' % '---b/s' 
 135                 return '%10s' % ('%s/s' % FileDownloader
.format_bytes(float(bytes) / dif
)) 
 138         def best_block_size(elapsed_time
, bytes): 
 139                 new_min 
= max(bytes / 2.0, 1.0) 
 140                 new_max 
= min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB 
 141                 if elapsed_time 
< 0.001: 
 143                 rate 
= bytes / elapsed_time
 
 151         def parse_bytes(bytestr
): 
 152                 """Parse a string indicating a byte quantity into a long integer.""" 
 153                 matchobj 
= re
.match(r
'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr
) 
 156                 number 
= float(matchobj
.group(1)) 
 157                 multiplier 
= 1024.0 ** 'bkmgtpezy'.index(matchobj
.group(2).lower()) 
 158                 return long(round(number 
* multiplier
)) 
 160         def add_info_extractor(self
, ie
): 
 161                 """Add an InfoExtractor object to the end of the list.""" 
 163                 ie
.set_downloader(self
) 
 165         def add_post_processor(self
, pp
): 
 166                 """Add a PostProcessor object to the end of the chain.""" 
 168                 pp
.set_downloader(self
) 
 170         def to_screen(self
, message
, skip_eol
=False): 
 171                 """Print message to stdout if not in quiet mode.""" 
 172                 assert type(message
) == type(u
'') 
 173                 if not self
.params
.get('quiet', False): 
 174                         terminator 
= [u
'\n', u
''][skip_eol
] 
 175                         output 
= message 
+ terminator
 
 177                         if 'b' not in self
._screen
_file
.mode 
or sys
.version_info
[0] < 3: # Python 2 lies about the mode of sys.stdout/sys.stderr 
 178                                 output 
= output
.encode(preferredencoding(), 'ignore') 
 179                         self
._screen
_file
.write(output
) 
 180                         self
._screen
_file
.flush() 
 182         def to_stderr(self
, message
): 
 183                 """Print message to stderr.""" 
 184                 print >>sys
.stderr
, message
.encode(preferredencoding()) 
 186         def to_cons_title(self
, message
): 
 187                 """Set console/terminal window title to message.""" 
 188                 if not self
.params
.get('consoletitle', False): 
 190                 if os
.name 
== 'nt' and ctypes
.windll
.kernel32
.GetConsoleWindow(): 
 191                         # c_wchar_p() might not be necessary if `message` is 
 192                         # already of type unicode() 
 193                         ctypes
.windll
.kernel32
.SetConsoleTitleW(ctypes
.c_wchar_p(message
)) 
 194                 elif 'TERM' in os
.environ
: 
 195                         sys
.stderr
.write('\033]0;%s\007' % message
.encode(preferredencoding())) 
 197         def fixed_template(self
): 
 198                 """Checks if the output template is fixed.""" 
 199                 return (re
.search(ur
'(?u)%\(.+?\)s', self
.params
['outtmpl']) is None) 
 201         def trouble(self
, message
=None): 
 202                 """Determine action to take when a download problem appears. 
 204                 Depending on if the downloader has been configured to ignore 
 205                 download errors or not, this method may throw an exception or 
 206                 not when errors are found, after printing the message. 
 208                 if message 
is not None: 
 209                         self
.to_stderr(message
) 
 210                 if not self
.params
.get('ignoreerrors', False): 
 211                         raise DownloadError(message
) 
 212                 self
._download
_retcode 
= 1 
 214         def slow_down(self
, start_time
, byte_counter
): 
 215                 """Sleep if the download speed is over the rate limit.""" 
 216                 rate_limit 
= self
.params
.get('ratelimit', None) 
 217                 if rate_limit 
is None or byte_counter 
== 0: 
 220                 elapsed 
= now 
- start_time
 
 223                 speed 
= float(byte_counter
) / elapsed
 
 224                 if speed 
> rate_limit
: 
 225                         time
.sleep((byte_counter 
- rate_limit 
* (now 
- start_time
)) / rate_limit
) 
 227         def temp_name(self
, filename
): 
 228                 """Returns a temporary filename for the given filename.""" 
 229                 if self
.params
.get('nopart', False) or filename 
== u
'-' or \
 
 230                                 (os
.path
.exists(encodeFilename(filename
)) and not os
.path
.isfile(encodeFilename(filename
))): 
 232                 return filename 
+ u
'.part' 
 234         def undo_temp_name(self
, filename
): 
 235                 if filename
.endswith(u
'.part'): 
 236                         return filename
[:-len(u
'.part')] 
 239         def try_rename(self
, old_filename
, new_filename
): 
 241                         if old_filename 
== new_filename
: 
 243                         os
.rename(encodeFilename(old_filename
), encodeFilename(new_filename
)) 
 244                 except (IOError, OSError), err
: 
 245                         self
.trouble(u
'ERROR: unable to rename file') 
 247         def try_utime(self
, filename
, last_modified_hdr
): 
 248                 """Try to set the last-modified time of the given file.""" 
 249                 if last_modified_hdr 
is None: 
 251                 if not os
.path
.isfile(encodeFilename(filename
)): 
 253                 timestr 
= last_modified_hdr
 
 256                 filetime 
= timeconvert(timestr
) 
 260                         os
.utime(filename
, (time
.time(), filetime
)) 
 265         def report_writedescription(self
, descfn
): 
 266                 """ Report that the description file is being written """ 
 267                 self
.to_screen(u
'[info] Writing video description to: ' + descfn
) 
 269         def report_writesubtitles(self
, srtfn
): 
 270                 """ Report that the subtitles file is being written """ 
 271                 self
.to_screen(u
'[info] Writing video subtitles to: ' + srtfn
) 
 273         def report_writeinfojson(self
, infofn
): 
 274                 """ Report that the metadata file has been written """ 
 275                 self
.to_screen(u
'[info] Video description metadata as JSON to: ' + infofn
) 
 277         def report_destination(self
, filename
): 
 278                 """Report destination filename.""" 
 279                 self
.to_screen(u
'[download] Destination: ' + filename
) 
 281         def report_progress(self
, percent_str
, data_len_str
, speed_str
, eta_str
): 
 282                 """Report download progress.""" 
 283                 if self
.params
.get('noprogress', False): 
 285                 self
.to_screen(u
'\r[download] %s of %s at %s ETA %s' % 
 286                                 (percent_str
, data_len_str
, speed_str
, eta_str
), skip_eol
=True) 
 287                 self
.to_cons_title(u
'youtube-dl - %s of %s at %s ETA %s' % 
 288                                 (percent_str
.strip(), data_len_str
.strip(), speed_str
.strip(), eta_str
.strip())) 
 290         def report_resuming_byte(self
, resume_len
): 
 291                 """Report attempt to resume at given byte.""" 
 292                 self
.to_screen(u
'[download] Resuming download at byte %s' % resume_len
) 
 294         def report_retry(self
, count
, retries
): 
 295                 """Report retry in case of HTTP error 5xx""" 
 296                 self
.to_screen(u
'[download] Got server HTTP error. Retrying (attempt %d of %d)...' % (count
, retries
)) 
 298         def report_file_already_downloaded(self
, file_name
): 
 299                 """Report file has already been fully downloaded.""" 
 301                         self
.to_screen(u
'[download] %s has already been downloaded' % file_name
) 
 302                 except (UnicodeEncodeError), err
: 
 303                         self
.to_screen(u
'[download] The file has already been downloaded') 
 305         def report_unable_to_resume(self
): 
 306                 """Report it was impossible to resume download.""" 
 307                 self
.to_screen(u
'[download] Unable to resume') 
 309         def report_finish(self
): 
 310                 """Report download finished.""" 
 311                 if self
.params
.get('noprogress', False): 
 312                         self
.to_screen(u
'[download] Download completed') 
 316         def increment_downloads(self
): 
 317                 """Increment the ordinal that assigns a number to each file.""" 
 318                 self
._num
_downloads 
+= 1 
 320         def prepare_filename(self
, info_dict
): 
 321                 """Generate the output filename.""" 
 323                         template_dict 
= dict(info_dict
) 
 324                         template_dict
['epoch'] = unicode(long(time
.time())) 
 325                         template_dict
['autonumber'] = unicode('%05d' % self
._num
_downloads
) 
 326                         filename 
= self
.params
['outtmpl'] % template_dict
 
 328                 except (ValueError, KeyError), err
: 
 329                         self
.trouble(u
'ERROR: invalid system charset or erroneous output template') 
 332         def _match_entry(self
, info_dict
): 
 333                 """ Returns None iff the file should be downloaded """ 
 335                 title 
= info_dict
['title'] 
 336                 matchtitle 
= self
.params
.get('matchtitle', False) 
 337                 if matchtitle 
and not re
.search(matchtitle
, title
, re
.IGNORECASE
): 
 338                         return u
'[download] "' + title 
+ '" title did not match pattern "' + matchtitle 
+ '"' 
 339                 rejecttitle 
= self
.params
.get('rejecttitle', False) 
 340                 if rejecttitle 
and re
.search(rejecttitle
, title
, re
.IGNORECASE
): 
 341                         return u
'"' + title 
+ '" title matched reject pattern "' + rejecttitle 
+ '"' 
 344         def process_info(self
, info_dict
): 
 345                 """Process a single dictionary returned by an InfoExtractor.""" 
 347                 info_dict
['stitle'] = sanitize_filename(info_dict
['title']) 
 349                 reason 
= self
._match
_entry
(info_dict
) 
 350                 if reason 
is not None: 
 351                         self
.to_screen(u
'[download] ' + reason
) 
 354                 max_downloads 
= self
.params
.get('max_downloads') 
 355                 if max_downloads 
is not None: 
 356                         if self
._num
_downloads 
> int(max_downloads
): 
 357                                 raise MaxDownloadsReached() 
 359                 filename 
= self
.prepare_filename(info_dict
) 
 362                 if self
.params
.get('forcetitle', False): 
 363                         print info_dict
['title'].encode(preferredencoding(), 'xmlcharrefreplace') 
 364                 if self
.params
.get('forceurl', False): 
 365                         print info_dict
['url'].encode(preferredencoding(), 'xmlcharrefreplace') 
 366                 if self
.params
.get('forcethumbnail', False) and 'thumbnail' in info_dict
: 
 367                         print info_dict
['thumbnail'].encode(preferredencoding(), 'xmlcharrefreplace') 
 368                 if self
.params
.get('forcedescription', False) and 'description' in info_dict
: 
 369                         print info_dict
['description'].encode(preferredencoding(), 'xmlcharrefreplace') 
 370                 if self
.params
.get('forcefilename', False) and filename 
is not None: 
 371                         print filename
.encode(preferredencoding(), 'xmlcharrefreplace') 
 372                 if self
.params
.get('forceformat', False): 
 373                         print info_dict
['format'].encode(preferredencoding(), 'xmlcharrefreplace') 
 375                 # Do nothing else if in simulate mode 
 376                 if self
.params
.get('simulate', False): 
 383                         dn 
= os
.path
.dirname(encodeFilename(filename
)) 
 384                         if dn 
!= '' and not os
.path
.exists(dn
): # dn is already encoded 
 386                 except (OSError, IOError), err
: 
 387                         self
.trouble(u
'ERROR: unable to create directory ' + unicode(err
)) 
 390                 if self
.params
.get('writedescription', False): 
 392                                 descfn 
= filename 
+ u
'.description' 
 393                                 self
.report_writedescription(descfn
) 
 394                                 descfile 
= open(encodeFilename(descfn
), 'wb') 
 396                                         descfile
.write(info_dict
['description'].encode('utf-8')) 
 399                         except (OSError, IOError): 
 400                                 self
.trouble(u
'ERROR: Cannot write description file ' + descfn
) 
 403                 if self
.params
.get('writesubtitles', False) and 'subtitles' in info_dict 
and info_dict
['subtitles']: 
 404                         # subtitles download errors are already managed as troubles in relevant IE 
 405                         # that way it will silently go on when used with unsupporting IE  
 407                                 srtfn 
= filename
.rsplit('.', 1)[0] + u
'.srt' 
 408                                 self
.report_writesubtitles(srtfn
) 
 409                                 srtfile 
= open(encodeFilename(srtfn
), 'wb') 
 411                                         srtfile
.write(info_dict
['subtitles'].encode('utf-8')) 
 414                         except (OSError, IOError): 
 415                                 self
.trouble(u
'ERROR: Cannot write subtitles file ' + descfn
) 
 418                 if self
.params
.get('writeinfojson', False): 
 419                         infofn 
= filename 
+ u
'.info.json' 
 420                         self
.report_writeinfojson(infofn
) 
 423                         except (NameError,AttributeError): 
 424                                 self
.trouble(u
'ERROR: No JSON encoder found. Update to Python 2.6+, setup a json module, or leave out --write-info-json.') 
 427                                 infof 
= open(encodeFilename(infofn
), 'wb') 
 429                                         json_info_dict 
= dict((k
,v
) for k
,v 
in info_dict
.iteritems() if not k 
in ('urlhandle',)) 
 430                                         json
.dump(json_info_dict
, infof
) 
 433                         except (OSError, IOError): 
 434                                 self
.trouble(u
'ERROR: Cannot write metadata to JSON file ' + infofn
) 
 437                 if not self
.params
.get('skip_download', False): 
 438                         if self
.params
.get('nooverwrites', False) and os
.path
.exists(encodeFilename(filename
)): 
 442                                         success 
= self
._do
_download
(filename
, info_dict
) 
 443                                 except (OSError, IOError), err
: 
 444                                         raise UnavailableVideoError
 
 445                                 except (urllib2
.URLError
, httplib
.HTTPException
, socket
.error
), err
: 
 446                                         self
.trouble(u
'ERROR: unable to download video data: %s' % str(err
)) 
 448                                 except (ContentTooShortError
, ), err
: 
 449                                         self
.trouble(u
'ERROR: content too short (expected %s bytes and served %s)' % (err
.expected
, err
.downloaded
)) 
 454                                         self
.post_process(filename
, info_dict
) 
 455                                 except (PostProcessingError
), err
: 
 456                                         self
.trouble(u
'ERROR: postprocessing: %s' % str(err
)) 
 459         def download(self
, url_list
): 
 460                 """Download a given list of URLs.""" 
 461                 if len(url_list
) > 1 and self
.fixed_template(): 
 462                         raise SameFileError(self
.params
['outtmpl']) 
 465                         suitable_found 
= False 
 467                                 # Go to next InfoExtractor if not suitable 
 468                                 if not ie
.suitable(url
): 
 471                                 # Suitable InfoExtractor found 
 472                                 suitable_found 
= True 
 474                                 # Extract information from URL and process it 
 475                                 videos 
= ie
.extract(url
) 
 476                                 for video 
in videos 
or []: 
 478                                                 self
.increment_downloads() 
 479                                                 self
.process_info(video
) 
 480                                         except UnavailableVideoError
: 
 481                                                 self
.trouble(u
'\nERROR: unable to download video') 
 483                                 # Suitable InfoExtractor had been found; go to next URL 
 486                         if not suitable_found
: 
 487                                 self
.trouble(u
'ERROR: no suitable InfoExtractor: %s' % url
) 
 489                 return self
._download
_retcode
 
 491         def post_process(self
, filename
, ie_info
): 
 492                 """Run the postprocessing chain on the given file.""" 
 494                 info
['filepath'] = filename
 
 500         def _download_with_rtmpdump(self
, filename
, url
, player_url
): 
 501                 self
.report_destination(filename
) 
 502                 tmpfilename 
= self
.temp_name(filename
) 
 504                 # Check for rtmpdump first 
 506                         subprocess
.call(['rtmpdump', '-h'], stdout
=(file(os
.path
.devnull
, 'w')), stderr
=subprocess
.STDOUT
) 
 507                 except (OSError, IOError): 
 508                         self
.trouble(u
'ERROR: RTMP download detected but "rtmpdump" could not be run') 
 511                 # Download using rtmpdump. rtmpdump returns exit code 2 when 
 512                 # the connection was interrumpted and resuming appears to be 
 513                 # possible. This is part of rtmpdump's normal usage, AFAIK. 
 514                 basic_args 
= ['rtmpdump', '-q'] + [[], ['-W', player_url
]][player_url 
is not None] + ['-r', url
, '-o', tmpfilename
] 
 515                 args 
= basic_args 
+ [[], ['-e', '-k', '1']][self
.params
.get('continuedl', False)] 
 516                 if self
.params
.get('verbose', False): 
 519                                 shell_quote 
= lambda args
: ' '.join(map(pipes
.quote
, args
)) 
 522                         self
.to_screen(u
'[debug] rtmpdump command line: ' + shell_quote(args
)) 
 523                 retval 
= subprocess
.call(args
) 
 524                 while retval 
== 2 or retval 
== 1: 
 525                         prevsize 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 526                         self
.to_screen(u
'\r[rtmpdump] %s bytes' % prevsize
, skip_eol
=True) 
 527                         time
.sleep(5.0) # This seems to be needed 
 528                         retval 
= subprocess
.call(basic_args 
+ ['-e'] + [[], ['-k', '1']][retval 
== 1]) 
 529                         cursize 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 530                         if prevsize 
== cursize 
and retval 
== 1: 
 532                          # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those 
 533                         if prevsize 
== cursize 
and retval 
== 2 and cursize 
> 1024: 
 534                                 self
.to_screen(u
'\r[rtmpdump] Could not download the whole video. This can happen for some advertisements.') 
 538                         self
.to_screen(u
'\r[rtmpdump] %s bytes' % os
.path
.getsize(encodeFilename(tmpfilename
))) 
 539                         self
.try_rename(tmpfilename
, filename
) 
 542                         self
.trouble(u
'\nERROR: rtmpdump exited with code %d' % retval
) 
 545         def _do_download(self
, filename
, info_dict
): 
 546                 url 
= info_dict
['url'] 
 547                 player_url 
= info_dict
.get('player_url', None) 
 549                 # Check file already present 
 550                 if self
.params
.get('continuedl', False) and os
.path
.isfile(encodeFilename(filename
)) and not self
.params
.get('nopart', False): 
 551                         self
.report_file_already_downloaded(filename
) 
 554                 # Attempt to download using rtmpdump 
 555                 if url
.startswith('rtmp'): 
 556                         return self
._download
_with
_rtmpdump
(filename
, url
, player_url
) 
 558                 tmpfilename 
= self
.temp_name(filename
) 
 561                 # Do not include the Accept-Encoding header 
 562                 headers 
= {'Youtubedl-no-compression': 'True'} 
 563                 basic_request 
= urllib2
.Request(url
, None, headers
) 
 564                 request 
= urllib2
.Request(url
, None, headers
) 
 566                 # Establish possible resume length 
 567                 if os
.path
.isfile(encodeFilename(tmpfilename
)): 
 568                         resume_len 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 574                         if self
.params
.get('continuedl', False): 
 575                                 self
.report_resuming_byte(resume_len
) 
 576                                 request
.add_header('Range','bytes=%d-' % resume_len
) 
 582                 retries 
= self
.params
.get('retries', 0) 
 583                 while count 
<= retries
: 
 584                         # Establish connection 
 586                                 if count 
== 0 and 'urlhandle' in info_dict
: 
 587                                         data 
= info_dict
['urlhandle'] 
 588                                 data 
= urllib2
.urlopen(request
) 
 590                         except (urllib2
.HTTPError
, ), err
: 
 591                                 if (err
.code 
< 500 or err
.code 
>= 600) and err
.code 
!= 416: 
 592                                         # Unexpected HTTP error 
 594                                 elif err
.code 
== 416: 
 595                                         # Unable to resume (requested range not satisfiable) 
 597                                                 # Open the connection again without the range header 
 598                                                 data 
= urllib2
.urlopen(basic_request
) 
 599                                                 content_length 
= data
.info()['Content-Length'] 
 600                                         except (urllib2
.HTTPError
, ), err
: 
 601                                                 if err
.code 
< 500 or err
.code 
>= 600: 
 604                                                 # Examine the reported length 
 605                                                 if (content_length 
is not None and 
 606                                                                 (resume_len 
- 100 < long(content_length
) < resume_len 
+ 100)): 
 607                                                         # The file had already been fully downloaded. 
 608                                                         # Explanation to the above condition: in issue #175 it was revealed that 
 609                                                         # YouTube sometimes adds or removes a few bytes from the end of the file, 
 610                                                         # changing the file size slightly and causing problems for some users. So 
 611                                                         # I decided to implement a suggested change and consider the file 
 612                                                         # completely downloaded if the file size differs less than 100 bytes from 
 613                                                         # the one in the hard drive. 
 614                                                         self
.report_file_already_downloaded(filename
) 
 615                                                         self
.try_rename(tmpfilename
, filename
) 
 618                                                         # The length does not match, we start the download over 
 619                                                         self
.report_unable_to_resume() 
 625                                 self
.report_retry(count
, retries
) 
 628                         self
.trouble(u
'ERROR: giving up after %s retries' % retries
) 
 631                 data_len 
= data
.info().get('Content-length', None) 
 632                 if data_len 
is not None: 
 633                         data_len 
= long(data_len
) + resume_len
 
 634                 data_len_str 
= self
.format_bytes(data_len
) 
 635                 byte_counter 
= 0 + resume_len
 
 641                         data_block 
= data
.read(block_size
) 
 643                         if len(data_block
) == 0: 
 645                         byte_counter 
+= len(data_block
) 
 647                         # Open file just in time 
 650                                         (stream
, tmpfilename
) = sanitize_open(tmpfilename
, open_mode
) 
 651                                         assert stream 
is not None 
 652                                         filename 
= self
.undo_temp_name(tmpfilename
) 
 653                                         self
.report_destination(filename
) 
 654                                 except (OSError, IOError), err
: 
 655                                         self
.trouble(u
'ERROR: unable to open for writing: %s' % str(err
)) 
 658                                 stream
.write(data_block
) 
 659                         except (IOError, OSError), err
: 
 660                                 self
.trouble(u
'\nERROR: unable to write data: %s' % str(err
)) 
 662                         block_size 
= self
.best_block_size(after 
- before
, len(data_block
)) 
 665                         speed_str 
= self
.calc_speed(start
, time
.time(), byte_counter 
- resume_len
) 
 667                                 self
.report_progress('Unknown %', data_len_str
, speed_str
, 'Unknown ETA') 
 669                                 percent_str 
= self
.calc_percent(byte_counter
, data_len
) 
 670                                 eta_str 
= self
.calc_eta(start
, time
.time(), data_len 
- resume_len
, byte_counter 
- resume_len
) 
 671                                 self
.report_progress(percent_str
, data_len_str
, speed_str
, eta_str
) 
 674                         self
.slow_down(start
, byte_counter 
- resume_len
) 
 677                         self
.trouble(u
'\nERROR: Did not get any data blocks') 
 681                 if data_len 
is not None and byte_counter 
!= data_len
: 
 682                         raise ContentTooShortError(byte_counter
, long(data_len
)) 
 683                 self
.try_rename(tmpfilename
, filename
) 
 685                 # Update file modification time 
 686                 if self
.params
.get('updatetime', True): 
 687                         info_dict
['filetime'] = self
.try_utime(filename
, data
.info().get('last-modified', None))