19 class FileDownloader(object): 
  20     """File Downloader class. 
  22     File downloader objects are the ones responsible of downloading the 
  23     actual video file and writing it to disk. 
  25     File downloaders accept a lot of parameters. In order not to saturate 
  26     the object constructor with arguments, it receives a dictionary of 
  31     verbose:           Print additional info to stdout. 
  32     quiet:             Do not print messages to stdout. 
  33     ratelimit:         Download speed limit, in bytes/sec. 
  34     retries:           Number of times to retry for HTTP error 5xx 
  35     buffersize:        Size of download buffer in bytes. 
  36     noresizebuffer:    Do not automatically resize the download buffer. 
  37     continuedl:        Try to continue downloads if possible. 
  38     noprogress:        Do not print the progress bar. 
  39     logtostderr:       Log messages to stderr instead of stdout. 
  40     consoletitle:      Display progress in console window's titlebar. 
  41     nopart:            Do not use temporary .part files. 
  42     updatetime:        Use the Last-modified header to set output file timestamps. 
  43     test:              Download only first bytes to test the downloader. 
  44     min_filesize:      Skip files smaller than this size 
  45     max_filesize:      Skip files larger than this size 
  50     def __init__(self
, ydl
, params
): 
  51         """Create a FileDownloader object with the given options.""" 
  53         self
._progress
_hooks 
= [] 
  57     def format_seconds(seconds
): 
  58         (mins
, secs
) = divmod(seconds
, 60) 
  59         (hours
, mins
) = divmod(mins
, 60) 
  63             return '%02d:%02d' % (mins
, secs
) 
  65             return '%02d:%02d:%02d' % (hours
, mins
, secs
) 
  68     def calc_percent(byte_counter
, data_len
): 
  71         return float(byte_counter
) / float(data_len
) * 100.0 
  74     def format_percent(percent
): 
  77         return '%6s' % ('%3.1f%%' % percent
) 
  80     def calc_eta(start
, now
, total
, current
): 
  84         if current 
== 0 or dif 
< 0.001: # One millisecond 
  86         rate 
= float(current
) / dif
 
  87         return int((float(total
) - float(current
)) / rate
) 
  93         return FileDownloader
.format_seconds(eta
) 
  96     def calc_speed(start
, now
, bytes): 
  98         if bytes == 0 or dif 
< 0.001: # One millisecond 
 100         return float(bytes) / dif
 
 103     def format_speed(speed
): 
 105             return '%10s' % '---b/s' 
 106         return '%10s' % ('%s/s' % format_bytes(speed
)) 
 109     def best_block_size(elapsed_time
, bytes): 
 110         new_min 
= max(bytes / 2.0, 1.0) 
 111         new_max 
= min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB 
 112         if elapsed_time 
< 0.001: 
 114         rate 
= bytes / elapsed_time
 
 122     def parse_bytes(bytestr
): 
 123         """Parse a string indicating a byte quantity into an integer.""" 
 124         matchobj 
= re
.match(r
'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr
) 
 127         number 
= float(matchobj
.group(1)) 
 128         multiplier 
= 1024.0 ** 'bkmgtpezy'.index(matchobj
.group(2).lower()) 
 129         return int(round(number 
* multiplier
)) 
 131     def to_screen(self
, *args
, **kargs
): 
 132         self
.ydl
.to_screen(*args
, **kargs
) 
 134     def to_stderr(self
, message
): 
 135         self
.ydl
.to_screen(message
) 
 137     def to_console_title(self
, message
): 
 138         self
.ydl
.to_console_title(message
) 
 140     def trouble(self
, *args
, **kargs
): 
 141         self
.ydl
.trouble(*args
, **kargs
) 
 143     def report_warning(self
, *args
, **kargs
): 
 144         self
.ydl
.report_warning(*args
, **kargs
) 
 146     def report_error(self
, *args
, **kargs
): 
 147         self
.ydl
.report_error(*args
, **kargs
) 
 149     def slow_down(self
, start_time
, byte_counter
): 
 150         """Sleep if the download speed is over the rate limit.""" 
 151         rate_limit 
= self
.params
.get('ratelimit', None) 
 152         if rate_limit 
is None or byte_counter 
== 0: 
 155         elapsed 
= now 
- start_time
 
 158         speed 
= float(byte_counter
) / elapsed
 
 159         if speed 
> rate_limit
: 
 160             time
.sleep((byte_counter 
- rate_limit 
* (now 
- start_time
)) / rate_limit
) 
 162     def temp_name(self
, filename
): 
 163         """Returns a temporary filename for the given filename.""" 
 164         if self
.params
.get('nopart', False) or filename 
== u
'-' or \
 
 165                 (os
.path
.exists(encodeFilename(filename
)) and not os
.path
.isfile(encodeFilename(filename
))): 
 167         return filename 
+ u
'.part' 
 169     def undo_temp_name(self
, filename
): 
 170         if filename
.endswith(u
'.part'): 
 171             return filename
[:-len(u
'.part')] 
 174     def try_rename(self
, old_filename
, new_filename
): 
 176             if old_filename 
== new_filename
: 
 178             os
.rename(encodeFilename(old_filename
), encodeFilename(new_filename
)) 
 179         except (IOError, OSError): 
 180             self
.report_error(u
'unable to rename file') 
 182     def try_utime(self
, filename
, last_modified_hdr
): 
 183         """Try to set the last-modified time of the given file.""" 
 184         if last_modified_hdr 
is None: 
 186         if not os
.path
.isfile(encodeFilename(filename
)): 
 188         timestr 
= last_modified_hdr
 
 191         filetime 
= timeconvert(timestr
) 
 194         # Ignore obviously invalid dates 
 198             os
.utime(filename
, (time
.time(), filetime
)) 
 203     def report_destination(self
, filename
): 
 204         """Report destination filename.""" 
 205         self
.to_screen(u
'[download] Destination: ' + filename
) 
 207     def _report_progress_status(self
, msg
, is_last_line
=False): 
 208         fullmsg 
= u
'[download] ' + msg
 
 209         if self
.params
.get('progress_with_newline', False): 
 210             self
.to_screen(fullmsg
) 
 213                 prev_len 
= getattr(self
, '_report_progress_prev_line_length', 
 215                 if prev_len 
> len(fullmsg
): 
 216                     fullmsg 
+= u
' ' * (prev_len 
- len(fullmsg
)) 
 217                 self
._report
_progress
_prev
_line
_length 
= len(fullmsg
) 
 220                 clear_line 
= (u
'\r\x1b[K' if sys
.stderr
.isatty() else u
'\r') 
 221             self
.to_screen(clear_line 
+ fullmsg
, skip_eol
=not is_last_line
) 
 222         self
.to_console_title(u
'youtube-dl ' + msg
) 
 224     def report_progress(self
, percent
, data_len_str
, speed
, eta
): 
 225         """Report download progress.""" 
 226         if self
.params
.get('noprogress', False): 
 229             eta_str 
= self
.format_eta(eta
) 
 231             eta_str 
= 'Unknown ETA' 
 232         if percent 
is not None: 
 233             percent_str 
= self
.format_percent(percent
) 
 235             percent_str 
= 'Unknown %' 
 236         speed_str 
= self
.format_speed(speed
) 
 238         msg 
= (u
'%s of %s at %s ETA %s' % 
 239                (percent_str
, data_len_str
, speed_str
, eta_str
)) 
 240         self
._report
_progress
_status
(msg
) 
 242     def report_progress_live_stream(self
, downloaded_data_len
, speed
, elapsed
): 
 243         if self
.params
.get('noprogress', False): 
 245         downloaded_str 
= format_bytes(downloaded_data_len
) 
 246         speed_str 
= self
.format_speed(speed
) 
 247         elapsed_str 
= FileDownloader
.format_seconds(elapsed
) 
 248         msg 
= u
'%s at %s (%s)' % (downloaded_str
, speed_str
, elapsed_str
) 
 249         self
._report
_progress
_status
(msg
) 
 251     def report_finish(self
, data_len_str
, tot_time
): 
 252         """Report download finished.""" 
 253         if self
.params
.get('noprogress', False): 
 254             self
.to_screen(u
'[download] Download completed') 
 256             self
._report
_progress
_status
( 
 257                 (u
'100%% of %s in %s' % 
 258                  (data_len_str
, self
.format_seconds(tot_time
))), 
 261     def report_resuming_byte(self
, resume_len
): 
 262         """Report attempt to resume at given byte.""" 
 263         self
.to_screen(u
'[download] Resuming download at byte %s' % resume_len
) 
 265     def report_retry(self
, count
, retries
): 
 266         """Report retry in case of HTTP error 5xx""" 
 267         self
.to_screen(u
'[download] Got server HTTP error. Retrying (attempt %d of %d)...' % (count
, retries
)) 
 269     def report_file_already_downloaded(self
, file_name
): 
 270         """Report file has already been fully downloaded.""" 
 272             self
.to_screen(u
'[download] %s has already been downloaded' % file_name
) 
 273         except UnicodeEncodeError: 
 274             self
.to_screen(u
'[download] The file has already been downloaded') 
 276     def report_unable_to_resume(self
): 
 277         """Report it was impossible to resume download.""" 
 278         self
.to_screen(u
'[download] Unable to resume') 
 280     def _download_with_rtmpdump(self
, filename
, url
, player_url
, page_url
, play_path
, tc_url
, live
, conn
): 
 281         def run_rtmpdump(args
): 
 283             resume_percent 
= None 
 284             resume_downloaded_data_len 
= None 
 285             proc 
= subprocess
.Popen(args
, stderr
=subprocess
.PIPE
) 
 286             cursor_in_new_line 
= True 
 287             proc_stderr_closed 
= False 
 288             while not proc_stderr_closed
: 
 289                 # read line from stderr 
 292                     char 
= proc
.stderr
.read(1) 
 294                         proc_stderr_closed 
= True 
 296                     if char 
in [b
'\r', b
'\n']: 
 298                     line 
+= char
.decode('ascii', 'replace') 
 300                     # proc_stderr_closed is True 
 302                 mobj 
= re
.search(r
'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec \(([0-9]{1,2}\.[0-9])%\)', line
) 
 304                     downloaded_data_len 
= int(float(mobj
.group(1))*1024) 
 305                     percent 
= float(mobj
.group(2)) 
 306                     if not resume_percent
: 
 307                         resume_percent 
= percent
 
 308                         resume_downloaded_data_len 
= downloaded_data_len
 
 309                     eta 
= self
.calc_eta(start
, time
.time(), 100-resume_percent
, percent
-resume_percent
) 
 310                     speed 
= self
.calc_speed(start
, time
.time(), downloaded_data_len
-resume_downloaded_data_len
) 
 313                         data_len 
= int(downloaded_data_len 
* 100 / percent
) 
 314                     data_len_str 
= u
'~' + format_bytes(data_len
) 
 315                     self
.report_progress(percent
, data_len_str
, speed
, eta
) 
 316                     cursor_in_new_line 
= False 
 317                     self
._hook
_progress
({ 
 318                         'downloaded_bytes': downloaded_data_len
, 
 319                         'total_bytes': data_len
, 
 320                         'tmpfilename': tmpfilename
, 
 321                         'filename': filename
, 
 322                         'status': 'downloading', 
 327                     # no percent for live streams 
 328                     mobj 
= re
.search(r
'([0-9]+\.[0-9]{3}) kB / [0-9]+\.[0-9]{2} sec', line
) 
 330                         downloaded_data_len 
= int(float(mobj
.group(1))*1024) 
 331                         time_now 
= time
.time() 
 332                         speed 
= self
.calc_speed(start
, time_now
, downloaded_data_len
) 
 333                         self
.report_progress_live_stream(downloaded_data_len
, speed
, time_now 
- start
) 
 334                         cursor_in_new_line 
= False 
 335                         self
._hook
_progress
({ 
 336                             'downloaded_bytes': downloaded_data_len
, 
 337                             'tmpfilename': tmpfilename
, 
 338                             'filename': filename
, 
 339                             'status': 'downloading', 
 342                     elif self
.params
.get('verbose', False): 
 343                         if not cursor_in_new_line
: 
 345                         cursor_in_new_line 
= True 
 346                         self
.to_screen(u
'[rtmpdump] '+line
) 
 348             if not cursor_in_new_line
: 
 350             return proc
.returncode
 
 352         self
.report_destination(filename
) 
 353         tmpfilename 
= self
.temp_name(filename
) 
 354         test 
= self
.params
.get('test', False) 
 356         # Check for rtmpdump first 
 358             subprocess
.call(['rtmpdump', '-h'], stdout
=(open(os
.path
.devnull
, 'w')), stderr
=subprocess
.STDOUT
) 
 359         except (OSError, IOError): 
 360             self
.report_error(u
'RTMP download detected but "rtmpdump" could not be run') 
 363         # Download using rtmpdump. rtmpdump returns exit code 2 when 
 364         # the connection was interrumpted and resuming appears to be 
 365         # possible. This is part of rtmpdump's normal usage, AFAIK. 
 366         basic_args 
= ['rtmpdump', '--verbose', '-r', url
, '-o', tmpfilename
] 
 367         if player_url 
is not None: 
 368             basic_args 
+= ['--swfVfy', player_url
] 
 369         if page_url 
is not None: 
 370             basic_args 
+= ['--pageUrl', page_url
] 
 371         if play_path 
is not None: 
 372             basic_args 
+= ['--playpath', play_path
] 
 373         if tc_url 
is not None: 
 374             basic_args 
+= ['--tcUrl', url
] 
 376             basic_args 
+= ['--stop', '1'] 
 378             basic_args 
+= ['--live'] 
 380             basic_args 
+= ['--conn', conn
] 
 381         args 
= basic_args 
+ [[], ['--resume', '--skip', '1']][self
.params
.get('continuedl', False)] 
 383         if sys
.platform 
== 'win32' and sys
.version_info 
< (3, 0): 
 384             # Windows subprocess module does not actually support Unicode 
 386             # See http://stackoverflow.com/a/9951851/35070 
 387             subprocess_encoding 
= sys
.getfilesystemencoding() 
 388             args 
= [a
.encode(subprocess_encoding
, 'ignore') for a 
in args
] 
 390             subprocess_encoding 
= None 
 392         if self
.params
.get('verbose', False): 
 393             if subprocess_encoding
: 
 395                     a
.decode(subprocess_encoding
) if isinstance(a
, bytes) else a
 
 401                 shell_quote 
= lambda args
: ' '.join(map(pipes
.quote
, str_args
)) 
 404             self
.to_screen(u
'[debug] rtmpdump command line: ' + shell_quote(str_args
)) 
 406         retval 
= run_rtmpdump(args
) 
 408         while (retval 
== 2 or retval 
== 1) and not test
: 
 409             prevsize 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 410             self
.to_screen(u
'[rtmpdump] %s bytes' % prevsize
) 
 411             time
.sleep(5.0) # This seems to be needed 
 412             retval 
= run_rtmpdump(basic_args 
+ ['-e'] + [[], ['-k', '1']][retval 
== 1]) 
 413             cursize 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 414             if prevsize 
== cursize 
and retval 
== 1: 
 416              # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those 
 417             if prevsize 
== cursize 
and retval 
== 2 and cursize 
> 1024: 
 418                 self
.to_screen(u
'[rtmpdump] Could not download the whole video. This can happen for some advertisements.') 
 421         if retval 
== 0 or (test 
and retval 
== 2): 
 422             fsize 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 423             self
.to_screen(u
'[rtmpdump] %s bytes' % fsize
) 
 424             self
.try_rename(tmpfilename
, filename
) 
 425             self
._hook
_progress
({ 
 426                 'downloaded_bytes': fsize
, 
 427                 'total_bytes': fsize
, 
 428                 'filename': filename
, 
 429                 'status': 'finished', 
 433             self
.to_stderr(u
"\n") 
 434             self
.report_error(u
'rtmpdump exited with code %d' % retval
) 
 437     def _download_with_mplayer(self
, filename
, url
): 
 438         self
.report_destination(filename
) 
 439         tmpfilename 
= self
.temp_name(filename
) 
 441         args 
= ['mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy', '-dumpstream', '-dumpfile', tmpfilename
, url
] 
 442         # Check for mplayer first 
 444             subprocess
.call(['mplayer', '-h'], stdout
=(open(os
.path
.devnull
, 'w')), stderr
=subprocess
.STDOUT
) 
 445         except (OSError, IOError): 
 446             self
.report_error(u
'MMS or RTSP download detected but "%s" could not be run' % args
[0] ) 
 449         # Download using mplayer.  
 450         retval 
= subprocess
.call(args
) 
 452             fsize 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 453             self
.to_screen(u
'\r[%s] %s bytes' % (args
[0], fsize
)) 
 454             self
.try_rename(tmpfilename
, filename
) 
 455             self
._hook
_progress
({ 
 456                 'downloaded_bytes': fsize
, 
 457                 'total_bytes': fsize
, 
 458                 'filename': filename
, 
 459                 'status': 'finished', 
 463             self
.to_stderr(u
"\n") 
 464             self
.report_error(u
'mplayer exited with code %d' % retval
) 
 467     def _download_m3u8_with_ffmpeg(self
, filename
, url
): 
 468         self
.report_destination(filename
) 
 469         tmpfilename 
= self
.temp_name(filename
) 
 471         args 
= ['-y', '-i', url
, '-f', 'mp4', '-c', 'copy', 
 472             '-bsf:a', 'aac_adtstoasc', tmpfilename
] 
 474         for program 
in ['avconv', 'ffmpeg']: 
 476                 subprocess
.call([program
, '-version'], stdout
=(open(os
.path
.devnull
, 'w')), stderr
=subprocess
.STDOUT
) 
 478             except (OSError, IOError): 
 481             self
.report_error(u
'm3u8 download detected but ffmpeg or avconv could not be found') 
 482         cmd 
= [program
] + args
 
 484         retval 
= subprocess
.call(cmd
) 
 486             fsize 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 487             self
.to_screen(u
'\r[%s] %s bytes' % (args
[0], fsize
)) 
 488             self
.try_rename(tmpfilename
, filename
) 
 489             self
._hook
_progress
({ 
 490                 'downloaded_bytes': fsize
, 
 491                 'total_bytes': fsize
, 
 492                 'filename': filename
, 
 493                 'status': 'finished', 
 497             self
.to_stderr(u
"\n") 
 498             self
.report_error(u
'ffmpeg exited with code %d' % retval
) 
 502     def _do_download(self
, filename
, info_dict
): 
 503         url 
= info_dict
['url'] 
 505         # Check file already present 
 506         if self
.params
.get('continuedl', False) and os
.path
.isfile(encodeFilename(filename
)) and not self
.params
.get('nopart', False): 
 507             self
.report_file_already_downloaded(filename
) 
 508             self
._hook
_progress
({ 
 509                 'filename': filename
, 
 510                 'status': 'finished', 
 511                 'total_bytes': os
.path
.getsize(encodeFilename(filename
)), 
 515         # Attempt to download using rtmpdump 
 516         if url
.startswith('rtmp'): 
 517             return self
._download
_with
_rtmpdump
(filename
, url
, 
 518                                                 info_dict
.get('player_url', None), 
 519                                                 info_dict
.get('page_url', None), 
 520                                                 info_dict
.get('play_path', None), 
 521                                                 info_dict
.get('tc_url', None), 
 522                                                 info_dict
.get('rtmp_live', False), 
 523                                                 info_dict
.get('rtmp_conn', None)) 
 525         # Attempt to download using mplayer 
 526         if url
.startswith('mms') or url
.startswith('rtsp'): 
 527             return self
._download
_with
_mplayer
(filename
, url
) 
 529         # m3u8 manifest are downloaded with ffmpeg 
 530         if determine_ext(url
) == u
'm3u8': 
 531             return self
._download
_m
3u8_with
_ffmpeg
(filename
, url
) 
 533         tmpfilename 
= self
.temp_name(filename
) 
 536         # Do not include the Accept-Encoding header 
 537         headers 
= {'Youtubedl-no-compression': 'True'} 
 538         if 'user_agent' in info_dict
: 
 539             headers
['Youtubedl-user-agent'] = info_dict
['user_agent'] 
 540         basic_request 
= compat_urllib_request
.Request(url
, None, headers
) 
 541         request 
= compat_urllib_request
.Request(url
, None, headers
) 
 543         if self
.params
.get('test', False): 
 544             request
.add_header('Range','bytes=0-10240') 
 546         # Establish possible resume length 
 547         if os
.path
.isfile(encodeFilename(tmpfilename
)): 
 548             resume_len 
= os
.path
.getsize(encodeFilename(tmpfilename
)) 
 554             if self
.params
.get('continuedl', False): 
 555                 self
.report_resuming_byte(resume_len
) 
 556                 request
.add_header('Range','bytes=%d-' % resume_len
) 
 562         retries 
= self
.params
.get('retries', 0) 
 563         while count 
<= retries
: 
 564             # Establish connection 
 566                 if count 
== 0 and 'urlhandle' in info_dict
: 
 567                     data 
= info_dict
['urlhandle'] 
 568                 data 
= compat_urllib_request
.urlopen(request
) 
 570             except (compat_urllib_error
.HTTPError
, ) as err
: 
 571                 if (err
.code 
< 500 or err
.code 
>= 600) and err
.code 
!= 416: 
 572                     # Unexpected HTTP error 
 574                 elif err
.code 
== 416: 
 575                     # Unable to resume (requested range not satisfiable) 
 577                         # Open the connection again without the range header 
 578                         data 
= compat_urllib_request
.urlopen(basic_request
) 
 579                         content_length 
= data
.info()['Content-Length'] 
 580                     except (compat_urllib_error
.HTTPError
, ) as err
: 
 581                         if err
.code 
< 500 or err
.code 
>= 600: 
 584                         # Examine the reported length 
 585                         if (content_length 
is not None and 
 586                                 (resume_len 
- 100 < int(content_length
) < resume_len 
+ 100)): 
 587                             # The file had already been fully downloaded. 
 588                             # Explanation to the above condition: in issue #175 it was revealed that 
 589                             # YouTube sometimes adds or removes a few bytes from the end of the file, 
 590                             # changing the file size slightly and causing problems for some users. So 
 591                             # I decided to implement a suggested change and consider the file 
 592                             # completely downloaded if the file size differs less than 100 bytes from 
 593                             # the one in the hard drive. 
 594                             self
.report_file_already_downloaded(filename
) 
 595                             self
.try_rename(tmpfilename
, filename
) 
 596                             self
._hook
_progress
({ 
 597                                 'filename': filename
, 
 598                                 'status': 'finished', 
 602                             # The length does not match, we start the download over 
 603                             self
.report_unable_to_resume() 
 609                 self
.report_retry(count
, retries
) 
 612             self
.report_error(u
'giving up after %s retries' % retries
) 
 615         data_len 
= data
.info().get('Content-length', None) 
 616         if data_len 
is not None: 
 617             data_len 
= int(data_len
) + resume_len
 
 618             min_data_len 
= self
.params
.get("min_filesize", None) 
 619             max_data_len 
=  self
.params
.get("max_filesize", None) 
 620             if min_data_len 
is not None and data_len 
< min_data_len
: 
 621                 self
.to_screen(u
'\r[download] File is smaller than min-filesize (%s bytes < %s bytes). Aborting.' % (data_len
, min_data_len
)) 
 623             if max_data_len 
is not None and data_len 
> max_data_len
: 
 624                 self
.to_screen(u
'\r[download] File is larger than max-filesize (%s bytes > %s bytes). Aborting.' % (data_len
, max_data_len
)) 
 627         data_len_str 
= format_bytes(data_len
) 
 628         byte_counter 
= 0 + resume_len
 
 629         block_size 
= self
.params
.get('buffersize', 1024) 
 634             data_block 
= data
.read(block_size
) 
 636             if len(data_block
) == 0: 
 638             byte_counter 
+= len(data_block
) 
 640             # Open file just in time 
 643                     (stream
, tmpfilename
) = sanitize_open(tmpfilename
, open_mode
) 
 644                     assert stream 
is not None 
 645                     filename 
= self
.undo_temp_name(tmpfilename
) 
 646                     self
.report_destination(filename
) 
 647                 except (OSError, IOError) as err
: 
 648                     self
.report_error(u
'unable to open for writing: %s' % str(err
)) 
 651                 stream
.write(data_block
) 
 652             except (IOError, OSError) as err
: 
 653                 self
.to_stderr(u
"\n") 
 654                 self
.report_error(u
'unable to write data: %s' % str(err
)) 
 656             if not self
.params
.get('noresizebuffer', False): 
 657                 block_size 
= self
.best_block_size(after 
- before
, len(data_block
)) 
 660             speed 
= self
.calc_speed(start
, time
.time(), byte_counter 
- resume_len
) 
 664                 percent 
= self
.calc_percent(byte_counter
, data_len
) 
 665                 eta 
= self
.calc_eta(start
, time
.time(), data_len 
- resume_len
, byte_counter 
- resume_len
) 
 666             self
.report_progress(percent
, data_len_str
, speed
, eta
) 
 668             self
._hook
_progress
({ 
 669                 'downloaded_bytes': byte_counter
, 
 670                 'total_bytes': data_len
, 
 671                 'tmpfilename': tmpfilename
, 
 672                 'filename': filename
, 
 673                 'status': 'downloading', 
 679             self
.slow_down(start
, byte_counter 
- resume_len
) 
 682             self
.to_stderr(u
"\n") 
 683             self
.report_error(u
'Did not get any data blocks') 
 686         self
.report_finish(data_len_str
, (time
.time() - start
)) 
 687         if data_len 
is not None and byte_counter 
!= data_len
: 
 688             raise ContentTooShortError(byte_counter
, int(data_len
)) 
 689         self
.try_rename(tmpfilename
, filename
) 
 691         # Update file modification time 
 692         if self
.params
.get('updatetime', True): 
 693             info_dict
['filetime'] = self
.try_utime(filename
, data
.info().get('last-modified', None)) 
 695         self
._hook
_progress
({ 
 696             'downloaded_bytes': byte_counter
, 
 697             'total_bytes': byte_counter
, 
 698             'filename': filename
, 
 699             'status': 'finished', 
 704     def _hook_progress(self
, status
): 
 705         for ph 
in self
._progress
_hooks
: 
 708     def add_progress_hook(self
, ph
): 
 709         """ ph gets called on download progress, with a dictionary with the entries 
 710         * filename: The final filename 
 711         * status: One of "downloading" and "finished" 
 713         It can also have some of the following entries: 
 715         * downloaded_bytes: Bytes on disks 
 716         * total_bytes: Total bytes, None if unknown 
 717         * tmpfilename: The filename we're currently writing to 
 718         * eta: The estimated time in seconds, None if unknown 
 719         * speed: The download speed in bytes/second, None if unknown 
 721         Hooks are guaranteed to be called at least once (with status "finished") 
 722         if the download is successful. 
 724         self
._progress
_hooks
.append(ph
)