]> jfr.im git - yt-dlp.git/blame - youtube_dl/FileDownloader.py
Merge pull request #1724 from rzhxeo/generic_youtube
[yt-dlp.git] / youtube_dl / FileDownloader.py
CommitLineData
d77c3dfd
FV
1import math
2import os
3import re
d77c3dfd
FV
4import subprocess
5import sys
6import time
59ce2019 7import traceback
d77c3dfd
FV
8
9if os.name == 'nt':
59ae15a5 10 import ctypes
3eec021a 11
9e8056d5 12from .utils import *
d77c3dfd
FV
13
14
15class FileDownloader(object):
59ae15a5
PH
16 """File Downloader class.
17
18 File downloader objects are the ones responsible of downloading the
8222d8de 19 actual video file and writing it to disk.
59ae15a5
PH
20
21 File downloaders accept a lot of parameters. In order not to saturate
22 the object constructor with arguments, it receives a dictionary of
8222d8de 23 options instead.
59ae15a5
PH
24
25 Available options:
26
8222d8de 27 verbose: Print additional info to stdout.
59ae15a5 28 quiet: Do not print messages to stdout.
59ae15a5 29 ratelimit: Download speed limit, in bytes/sec.
59ae15a5
PH
30 retries: Number of times to retry for HTTP error 5xx
31 buffersize: Size of download buffer in bytes.
32 noresizebuffer: Do not automatically resize the download buffer.
33 continuedl: Try to continue downloads if possible.
34 noprogress: Do not print the progress bar.
59ae15a5
PH
35 logtostderr: Log messages to stderr instead of stdout.
36 consoletitle: Display progress in console window's titlebar.
37 nopart: Do not use temporary .part files.
38 updatetime: Use the Last-modified header to set output file timestamps.
37c8fd48 39 test: Download only first bytes to test the downloader.
9e982f9e
JC
40 min_filesize: Skip files smaller than this size
41 max_filesize: Skip files larger than this size
59ae15a5
PH
42 """
43
44 params = None
59ae15a5 45
8222d8de 46 def __init__(self, ydl, params):
59ae15a5 47 """Create a FileDownloader object with the given options."""
8222d8de 48 self.ydl = ydl
bffbd5f0 49 self._progress_hooks = []
59ae15a5
PH
50 self.params = params
51
59ae15a5
PH
52 @staticmethod
53 def format_bytes(bytes):
54 if bytes is None:
55 return 'N/A'
56 if type(bytes) is str:
57 bytes = float(bytes)
58 if bytes == 0.0:
59 exponent = 0
60 else:
61 exponent = int(math.log(bytes, 1024.0))
b0936ef4 62 suffix = ['B','KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'][exponent]
59ae15a5
PH
63 converted = float(bytes) / float(1024 ** exponent)
64 return '%.2f%s' % (converted, suffix)
65
af8bd6a8
JMF
66 @staticmethod
67 def format_seconds(seconds):
68 (mins, secs) = divmod(seconds, 60)
061b2889 69 (hours, mins) = divmod(mins, 60)
af8bd6a8
JMF
70 if hours > 99:
71 return '--:--:--'
72 if hours == 0:
73 return '%02d:%02d' % (mins, secs)
74 else:
75 return '%02d:%02d:%02d' % (hours, mins, secs)
76
59ae15a5
PH
77 @staticmethod
78 def calc_percent(byte_counter, data_len):
79 if data_len is None:
4ae72004
JMF
80 return None
81 return float(byte_counter) / float(data_len) * 100.0
82
83 @staticmethod
84 def format_percent(percent):
85 if percent is None:
59ae15a5 86 return '---.-%'
4ae72004 87 return '%6s' % ('%3.1f%%' % percent)
59ae15a5
PH
88
89 @staticmethod
90 def calc_eta(start, now, total, current):
91 if total is None:
4ae72004 92 return None
59ae15a5
PH
93 dif = now - start
94 if current == 0 or dif < 0.001: # One millisecond
4ae72004 95 return None
59ae15a5 96 rate = float(current) / dif
4ae72004
JMF
97 return int((float(total) - float(current)) / rate)
98
99 @staticmethod
100 def format_eta(eta):
101 if eta is None:
102 return '--:--'
af8bd6a8 103 return FileDownloader.format_seconds(eta)
59ae15a5
PH
104
105 @staticmethod
106 def calc_speed(start, now, bytes):
107 dif = now - start
108 if bytes == 0 or dif < 0.001: # One millisecond
4ae72004
JMF
109 return None
110 return float(bytes) / dif
111
112 @staticmethod
113 def format_speed(speed):
114 if speed is None:
59ae15a5 115 return '%10s' % '---b/s'
4ae72004 116 return '%10s' % ('%s/s' % FileDownloader.format_bytes(speed))
59ae15a5
PH
117
118 @staticmethod
119 def best_block_size(elapsed_time, bytes):
120 new_min = max(bytes / 2.0, 1.0)
121 new_max = min(max(bytes * 2.0, 1.0), 4194304) # Do not surpass 4 MB
122 if elapsed_time < 0.001:
123 return int(new_max)
124 rate = bytes / elapsed_time
125 if rate > new_max:
126 return int(new_max)
127 if rate < new_min:
128 return int(new_min)
129 return int(rate)
130
131 @staticmethod
132 def parse_bytes(bytestr):
133 """Parse a string indicating a byte quantity into an integer."""
134 matchobj = re.match(r'(?i)^(\d+(?:\.\d+)?)([kMGTPEZY]?)$', bytestr)
135 if matchobj is None:
136 return None
137 number = float(matchobj.group(1))
138 multiplier = 1024.0 ** 'bkmgtpezy'.index(matchobj.group(2).lower())
139 return int(round(number * multiplier))
140
8222d8de
JMF
141 def to_screen(self, *args, **kargs):
142 self.ydl.to_screen(*args, **kargs)
59ae15a5
PH
143
144 def to_stderr(self, message):
8222d8de 145 self.ydl.to_screen(message)
59ae15a5
PH
146
147 def to_cons_title(self, message):
148 """Set console/terminal window title to message."""
149 if not self.params.get('consoletitle', False):
150 return
151 if os.name == 'nt' and ctypes.windll.kernel32.GetConsoleWindow():
152 # c_wchar_p() might not be necessary if `message` is
153 # already of type unicode()
154 ctypes.windll.kernel32.SetConsoleTitleW(ctypes.c_wchar_p(message))
155 elif 'TERM' in os.environ:
906417c7 156 self.to_screen('\033]0;%s\007' % message, skip_eol=True)
59ae15a5 157
8222d8de
JMF
158 def trouble(self, *args, **kargs):
159 self.ydl.trouble(*args, **kargs)
160
161 def report_warning(self, *args, **kargs):
162 self.ydl.report_warning(*args, **kargs)
163
164 def report_error(self, *args, **kargs):
2e325280 165 self.ydl.report_error(*args, **kargs)
4e1582f3 166
59ae15a5
PH
167 def slow_down(self, start_time, byte_counter):
168 """Sleep if the download speed is over the rate limit."""
169 rate_limit = self.params.get('ratelimit', None)
170 if rate_limit is None or byte_counter == 0:
171 return
172 now = time.time()
173 elapsed = now - start_time
174 if elapsed <= 0.0:
175 return
176 speed = float(byte_counter) / elapsed
177 if speed > rate_limit:
178 time.sleep((byte_counter - rate_limit * (now - start_time)) / rate_limit)
179
180 def temp_name(self, filename):
181 """Returns a temporary filename for the given filename."""
182 if self.params.get('nopart', False) or filename == u'-' or \
183 (os.path.exists(encodeFilename(filename)) and not os.path.isfile(encodeFilename(filename))):
184 return filename
185 return filename + u'.part'
186
187 def undo_temp_name(self, filename):
188 if filename.endswith(u'.part'):
189 return filename[:-len(u'.part')]
190 return filename
191
192 def try_rename(self, old_filename, new_filename):
193 try:
194 if old_filename == new_filename:
195 return
196 os.rename(encodeFilename(old_filename), encodeFilename(new_filename))
197 except (IOError, OSError) as err:
6622d22c 198 self.report_error(u'unable to rename file')
59ae15a5
PH
199
200 def try_utime(self, filename, last_modified_hdr):
201 """Try to set the last-modified time of the given file."""
202 if last_modified_hdr is None:
203 return
204 if not os.path.isfile(encodeFilename(filename)):
205 return
206 timestr = last_modified_hdr
207 if timestr is None:
208 return
209 filetime = timeconvert(timestr)
210 if filetime is None:
211 return filetime
bb474376
PH
212 # Ignore obviously invalid dates
213 if filetime == 0:
214 return
59ae15a5
PH
215 try:
216 os.utime(filename, (time.time(), filetime))
217 except:
218 pass
219 return filetime
220
59ae15a5
PH
221 def report_destination(self, filename):
222 """Report destination filename."""
223 self.to_screen(u'[download] Destination: ' + filename)
224
4ae72004 225 def report_progress(self, percent, data_len_str, speed, eta):
59ae15a5
PH
226 """Report download progress."""
227 if self.params.get('noprogress', False):
228 return
4ae9e558 229 clear_line = (u'\x1b[K' if sys.stderr.isatty() and os.name != 'nt' else u'')
4ae72004
JMF
230 eta_str = self.format_eta(eta)
231 percent_str = self.format_percent(percent)
232 speed_str = self.format_speed(speed)
5717d91a 233 if self.params.get('progress_with_newline', False):
1528d664 234 self.to_screen(u'[download] %s of %s at %s ETA %s' %
7311fef8 235 (percent_str, data_len_str, speed_str, eta_str))
5717d91a 236 else:
4ae9e558
PH
237 self.to_screen(u'\r%s[download] %s of %s at %s ETA %s' %
238 (clear_line, percent_str, data_len_str, speed_str, eta_str), skip_eol=True)
59ae15a5
PH
239 self.to_cons_title(u'youtube-dl - %s of %s at %s ETA %s' %
240 (percent_str.strip(), data_len_str.strip(), speed_str.strip(), eta_str.strip()))
241
242 def report_resuming_byte(self, resume_len):
243 """Report attempt to resume at given byte."""
244 self.to_screen(u'[download] Resuming download at byte %s' % resume_len)
245
246 def report_retry(self, count, retries):
247 """Report retry in case of HTTP error 5xx"""
248 self.to_screen(u'[download] Got server HTTP error. Retrying (attempt %d of %d)...' % (count, retries))
249
250 def report_file_already_downloaded(self, file_name):
251 """Report file has already been fully downloaded."""
252 try:
253 self.to_screen(u'[download] %s has already been downloaded' % file_name)
254 except (UnicodeEncodeError) as err:
255 self.to_screen(u'[download] The file has already been downloaded')
256
257 def report_unable_to_resume(self):
258 """Report it was impossible to resume download."""
259 self.to_screen(u'[download] Unable to resume')
260
968b5e01 261 def report_finish(self, data_len_str, tot_time):
59ae15a5
PH
262 """Report download finished."""
263 if self.params.get('noprogress', False):
264 self.to_screen(u'[download] Download completed')
265 else:
6d38616e 266 clear_line = (u'\x1b[K' if sys.stderr.isatty() and os.name != 'nt' else u'')
af8bd6a8
JMF
267 self.to_screen(u'\r%s[download] 100%% of %s in %s' %
268 (clear_line, data_len_str, self.format_seconds(tot_time)))
59ae15a5 269
8e4f8243 270 def _download_with_rtmpdump(self, filename, url, player_url, page_url, play_path, tc_url):
59ae15a5
PH
271 self.report_destination(filename)
272 tmpfilename = self.temp_name(filename)
9026dd38 273 test = self.params.get('test', False)
59ae15a5
PH
274
275 # Check for rtmpdump first
276 try:
967897fd 277 subprocess.call(['rtmpdump', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
59ae15a5 278 except (OSError, IOError):
6622d22c 279 self.report_error(u'RTMP download detected but "rtmpdump" could not be run')
59ae15a5 280 return False
8cd252f1 281 verbosity_option = '--verbose' if self.params.get('verbose', False) else '--quiet'
59ae15a5
PH
282
283 # Download using rtmpdump. rtmpdump returns exit code 2 when
284 # the connection was interrumpted and resuming appears to be
285 # possible. This is part of rtmpdump's normal usage, AFAIK.
8cd252f1 286 basic_args = ['rtmpdump', verbosity_option, '-r', url, '-o', tmpfilename]
f5ebb614 287 if player_url is not None:
8cd252f1 288 basic_args += ['--swfVfy', player_url]
f5ebb614
PH
289 if page_url is not None:
290 basic_args += ['--pageUrl', page_url]
adb029ed 291 if play_path is not None:
8cd252f1 292 basic_args += ['--playpath', play_path]
de5d66d4 293 if tc_url is not None:
294 basic_args += ['--tcUrl', url]
9026dd38 295 if test:
ad7a071a 296 basic_args += ['--stop', '1']
8cd252f1 297 args = basic_args + [[], ['--resume', '--skip', '1']][self.params.get('continuedl', False)]
59ae15a5
PH
298 if self.params.get('verbose', False):
299 try:
300 import pipes
301 shell_quote = lambda args: ' '.join(map(pipes.quote, args))
302 except ImportError:
303 shell_quote = repr
304 self.to_screen(u'[debug] rtmpdump command line: ' + shell_quote(args))
305 retval = subprocess.call(args)
9026dd38 306 while (retval == 2 or retval == 1) and not test:
59ae15a5
PH
307 prevsize = os.path.getsize(encodeFilename(tmpfilename))
308 self.to_screen(u'\r[rtmpdump] %s bytes' % prevsize, skip_eol=True)
309 time.sleep(5.0) # This seems to be needed
310 retval = subprocess.call(basic_args + ['-e'] + [[], ['-k', '1']][retval == 1])
311 cursize = os.path.getsize(encodeFilename(tmpfilename))
312 if prevsize == cursize and retval == 1:
313 break
314 # Some rtmp streams seem abort after ~ 99.8%. Don't complain for those
315 if prevsize == cursize and retval == 2 and cursize > 1024:
316 self.to_screen(u'\r[rtmpdump] Could not download the whole video. This can happen for some advertisements.')
317 retval = 0
318 break
9026dd38 319 if retval == 0 or (test and retval == 2):
bffbd5f0
PH
320 fsize = os.path.getsize(encodeFilename(tmpfilename))
321 self.to_screen(u'\r[rtmpdump] %s bytes' % fsize)
59ae15a5 322 self.try_rename(tmpfilename, filename)
bffbd5f0
PH
323 self._hook_progress({
324 'downloaded_bytes': fsize,
325 'total_bytes': fsize,
326 'filename': filename,
327 'status': 'finished',
328 })
59ae15a5
PH
329 return True
330 else:
6622d22c
JMF
331 self.to_stderr(u"\n")
332 self.report_error(u'rtmpdump exited with code %d' % retval)
59ae15a5
PH
333 return False
334
f2cd958c 335 def _download_with_mplayer(self, filename, url):
336 self.report_destination(filename)
337 tmpfilename = self.temp_name(filename)
338
f2cd958c 339 args = ['mplayer', '-really-quiet', '-vo', 'null', '-vc', 'dummy', '-dumpstream', '-dumpfile', tmpfilename, url]
340 # Check for mplayer first
341 try:
3054ff0c 342 subprocess.call(['mplayer', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
f2cd958c 343 except (OSError, IOError):
344 self.report_error(u'MMS or RTSP download detected but "%s" could not be run' % args[0] )
345 return False
346
347 # Download using mplayer.
348 retval = subprocess.call(args)
349 if retval == 0:
350 fsize = os.path.getsize(encodeFilename(tmpfilename))
351 self.to_screen(u'\r[%s] %s bytes' % (args[0], fsize))
352 self.try_rename(tmpfilename, filename)
353 self._hook_progress({
354 'downloaded_bytes': fsize,
355 'total_bytes': fsize,
356 'filename': filename,
357 'status': 'finished',
358 })
359 return True
360 else:
361 self.to_stderr(u"\n")
3054ff0c 362 self.report_error(u'mplayer exited with code %d' % retval)
f2cd958c 363 return False
364
b15d4f62
JMF
365 def _download_m3u8_with_ffmpeg(self, filename, url):
366 self.report_destination(filename)
367 tmpfilename = self.temp_name(filename)
368
369 args = ['ffmpeg', '-y', '-i', url, '-f', 'mp4', tmpfilename]
370 # Check for ffmpeg first
371 try:
372 subprocess.call(['ffmpeg', '-h'], stdout=(open(os.path.devnull, 'w')), stderr=subprocess.STDOUT)
373 except (OSError, IOError):
374 self.report_error(u'm3u8 download detected but "%s" could not be run' % args[0] )
375 return False
376
377 retval = subprocess.call(args)
378 if retval == 0:
379 fsize = os.path.getsize(encodeFilename(tmpfilename))
380 self.to_screen(u'\r[%s] %s bytes' % (args[0], fsize))
381 self.try_rename(tmpfilename, filename)
382 self._hook_progress({
383 'downloaded_bytes': fsize,
384 'total_bytes': fsize,
385 'filename': filename,
386 'status': 'finished',
387 })
388 return True
389 else:
390 self.to_stderr(u"\n")
391 self.report_error(u'ffmpeg exited with code %d' % retval)
392 return False
393
f2cd958c 394
59ae15a5
PH
395 def _do_download(self, filename, info_dict):
396 url = info_dict['url']
59ae15a5
PH
397
398 # Check file already present
399 if self.params.get('continuedl', False) and os.path.isfile(encodeFilename(filename)) and not self.params.get('nopart', False):
400 self.report_file_already_downloaded(filename)
bffbd5f0
PH
401 self._hook_progress({
402 'filename': filename,
403 'status': 'finished',
dd5d2eb0 404 'total_bytes': os.path.getsize(encodeFilename(filename)),
bffbd5f0 405 })
59ae15a5
PH
406 return True
407
408 # Attempt to download using rtmpdump
409 if url.startswith('rtmp'):
f5ebb614
PH
410 return self._download_with_rtmpdump(filename, url,
411 info_dict.get('player_url', None),
adb029ed 412 info_dict.get('page_url', None),
de5d66d4 413 info_dict.get('play_path', None),
8e4f8243 414 info_dict.get('tc_url', None))
59ae15a5 415
f2cd958c 416 # Attempt to download using mplayer
417 if url.startswith('mms') or url.startswith('rtsp'):
418 return self._download_with_mplayer(filename, url)
419
b15d4f62
JMF
420 # m3u8 manifest are downloaded with ffmpeg
421 if determine_ext(url) == u'm3u8':
422 return self._download_m3u8_with_ffmpeg(filename, url)
423
59ae15a5
PH
424 tmpfilename = self.temp_name(filename)
425 stream = None
426
427 # Do not include the Accept-Encoding header
428 headers = {'Youtubedl-no-compression': 'True'}
3446dfb7
PH
429 if 'user_agent' in info_dict:
430 headers['Youtubedl-user-agent'] = info_dict['user_agent']
59ae15a5
PH
431 basic_request = compat_urllib_request.Request(url, None, headers)
432 request = compat_urllib_request.Request(url, None, headers)
433
37c8fd48
FV
434 if self.params.get('test', False):
435 request.add_header('Range','bytes=0-10240')
436
59ae15a5
PH
437 # Establish possible resume length
438 if os.path.isfile(encodeFilename(tmpfilename)):
439 resume_len = os.path.getsize(encodeFilename(tmpfilename))
440 else:
441 resume_len = 0
442
443 open_mode = 'wb'
444 if resume_len != 0:
445 if self.params.get('continuedl', False):
446 self.report_resuming_byte(resume_len)
447 request.add_header('Range','bytes=%d-' % resume_len)
448 open_mode = 'ab'
449 else:
450 resume_len = 0
451
452 count = 0
453 retries = self.params.get('retries', 0)
454 while count <= retries:
455 # Establish connection
456 try:
457 if count == 0 and 'urlhandle' in info_dict:
458 data = info_dict['urlhandle']
459 data = compat_urllib_request.urlopen(request)
460 break
461 except (compat_urllib_error.HTTPError, ) as err:
462 if (err.code < 500 or err.code >= 600) and err.code != 416:
463 # Unexpected HTTP error
464 raise
465 elif err.code == 416:
466 # Unable to resume (requested range not satisfiable)
467 try:
468 # Open the connection again without the range header
469 data = compat_urllib_request.urlopen(basic_request)
470 content_length = data.info()['Content-Length']
471 except (compat_urllib_error.HTTPError, ) as err:
472 if err.code < 500 or err.code >= 600:
473 raise
474 else:
475 # Examine the reported length
476 if (content_length is not None and
477 (resume_len - 100 < int(content_length) < resume_len + 100)):
478 # The file had already been fully downloaded.
479 # Explanation to the above condition: in issue #175 it was revealed that
480 # YouTube sometimes adds or removes a few bytes from the end of the file,
481 # changing the file size slightly and causing problems for some users. So
482 # I decided to implement a suggested change and consider the file
483 # completely downloaded if the file size differs less than 100 bytes from
484 # the one in the hard drive.
485 self.report_file_already_downloaded(filename)
486 self.try_rename(tmpfilename, filename)
bffbd5f0
PH
487 self._hook_progress({
488 'filename': filename,
489 'status': 'finished',
490 })
59ae15a5
PH
491 return True
492 else:
493 # The length does not match, we start the download over
494 self.report_unable_to_resume()
495 open_mode = 'wb'
496 break
497 # Retry
498 count += 1
499 if count <= retries:
500 self.report_retry(count, retries)
501
502 if count > retries:
6622d22c 503 self.report_error(u'giving up after %s retries' % retries)
59ae15a5
PH
504 return False
505
506 data_len = data.info().get('Content-length', None)
507 if data_len is not None:
508 data_len = int(data_len) + resume_len
9e982f9e
JC
509 min_data_len = self.params.get("min_filesize", None)
510 max_data_len = self.params.get("max_filesize", None)
511 if min_data_len is not None and data_len < min_data_len:
512 self.to_screen(u'\r[download] File is smaller than min-filesize (%s bytes < %s bytes). Aborting.' % (data_len, min_data_len))
513 return False
514 if max_data_len is not None and data_len > max_data_len:
515 self.to_screen(u'\r[download] File is larger than max-filesize (%s bytes > %s bytes). Aborting.' % (data_len, max_data_len))
516 return False
517
59ae15a5
PH
518 data_len_str = self.format_bytes(data_len)
519 byte_counter = 0 + resume_len
520 block_size = self.params.get('buffersize', 1024)
521 start = time.time()
522 while True:
523 # Download and write
524 before = time.time()
525 data_block = data.read(block_size)
526 after = time.time()
527 if len(data_block) == 0:
528 break
529 byte_counter += len(data_block)
530
531 # Open file just in time
532 if stream is None:
533 try:
534 (stream, tmpfilename) = sanitize_open(tmpfilename, open_mode)
535 assert stream is not None
536 filename = self.undo_temp_name(tmpfilename)
537 self.report_destination(filename)
538 except (OSError, IOError) as err:
6622d22c 539 self.report_error(u'unable to open for writing: %s' % str(err))
59ae15a5
PH
540 return False
541 try:
542 stream.write(data_block)
543 except (IOError, OSError) as err:
6622d22c
JMF
544 self.to_stderr(u"\n")
545 self.report_error(u'unable to write data: %s' % str(err))
59ae15a5
PH
546 return False
547 if not self.params.get('noresizebuffer', False):
548 block_size = self.best_block_size(after - before, len(data_block))
549
550 # Progress message
4ae72004 551 speed = self.calc_speed(start, time.time(), byte_counter - resume_len)
59ae15a5
PH
552 if data_len is None:
553 self.report_progress('Unknown %', data_len_str, speed_str, 'Unknown ETA')
4ae72004 554 eta = None
59ae15a5 555 else:
4ae72004
JMF
556 percent = self.calc_percent(byte_counter, data_len)
557 eta = self.calc_eta(start, time.time(), data_len - resume_len, byte_counter - resume_len)
558 self.report_progress(percent, data_len_str, speed, eta)
59ae15a5 559
bffbd5f0
PH
560 self._hook_progress({
561 'downloaded_bytes': byte_counter,
562 'total_bytes': data_len,
563 'tmpfilename': tmpfilename,
564 'filename': filename,
565 'status': 'downloading',
4ae72004
JMF
566 'eta': eta,
567 'speed': speed,
bffbd5f0
PH
568 })
569
59ae15a5
PH
570 # Apply rate limit
571 self.slow_down(start, byte_counter - resume_len)
572
573 if stream is None:
6622d22c
JMF
574 self.to_stderr(u"\n")
575 self.report_error(u'Did not get any data blocks')
59ae15a5
PH
576 return False
577 stream.close()
968b5e01 578 self.report_finish(data_len_str, (time.time() - start))
59ae15a5
PH
579 if data_len is not None and byte_counter != data_len:
580 raise ContentTooShortError(byte_counter, int(data_len))
581 self.try_rename(tmpfilename, filename)
582
583 # Update file modification time
584 if self.params.get('updatetime', True):
585 info_dict['filetime'] = self.try_utime(filename, data.info().get('last-modified', None))
586
bffbd5f0
PH
587 self._hook_progress({
588 'downloaded_bytes': byte_counter,
589 'total_bytes': byte_counter,
590 'filename': filename,
591 'status': 'finished',
592 })
593
59ae15a5 594 return True
bffbd5f0
PH
595
596 def _hook_progress(self, status):
597 for ph in self._progress_hooks:
598 ph(status)
599
600 def add_progress_hook(self, ph):
601 """ ph gets called on download progress, with a dictionary with the entries
602 * filename: The final filename
603 * status: One of "downloading" and "finished"
604
605 It can also have some of the following entries:
606
607 * downloaded_bytes: Bytes on disks
608 * total_bytes: Total bytes, None if unknown
609 * tmpfilename: The filename we're currently writing to
4ae72004
JMF
610 * eta: The estimated time in seconds, None if unknown
611 * speed: The download speed in bytes/second, None if unknown
bffbd5f0
PH
612
613 Hooks are guaranteed to be called at least once (with status "finished")
614 if the download is successful.
615 """
616 self._progress_hooks.append(ph)