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