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