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