|
@@ -7,10 +7,14 @@ import time
|
|
import re
|
|
import re
|
|
|
|
|
|
from .common import FileDownloader
|
|
from .common import FileDownloader
|
|
-from ..compat import compat_urllib_error
|
|
|
|
|
|
+from ..compat import (
|
|
|
|
+ compat_str,
|
|
|
|
+ compat_urllib_error,
|
|
|
|
+)
|
|
from ..utils import (
|
|
from ..utils import (
|
|
ContentTooShortError,
|
|
ContentTooShortError,
|
|
encodeFilename,
|
|
encodeFilename,
|
|
|
|
+ int_or_none,
|
|
sanitize_open,
|
|
sanitize_open,
|
|
sanitized_Request,
|
|
sanitized_Request,
|
|
write_xattr,
|
|
write_xattr,
|
|
@@ -42,17 +46,22 @@ class HttpFD(FileDownloader):
|
|
request = sanitized_Request(url, None, headers)
|
|
request = sanitized_Request(url, None, headers)
|
|
|
|
|
|
is_test = self.params.get('test', False)
|
|
is_test = self.params.get('test', False)
|
|
-
|
|
|
|
- if is_test:
|
|
|
|
- request.add_header('Range', 'bytes=0-%s' % str(self._TEST_FILE_SIZE - 1))
|
|
|
|
|
|
+ chunk_size = self._TEST_FILE_SIZE if is_test else (
|
|
|
|
+ self.params.get('http_chunk_size') or 0)
|
|
|
|
|
|
ctx.open_mode = 'wb'
|
|
ctx.open_mode = 'wb'
|
|
ctx.resume_len = 0
|
|
ctx.resume_len = 0
|
|
|
|
+ ctx.data_len = None
|
|
|
|
+ ctx.block_size = self.params.get('buffersize', 1024)
|
|
|
|
+ ctx.start_time = time.time()
|
|
|
|
|
|
if self.params.get('continuedl', True):
|
|
if self.params.get('continuedl', True):
|
|
# Establish possible resume length
|
|
# Establish possible resume length
|
|
if os.path.isfile(encodeFilename(ctx.tmpfilename)):
|
|
if os.path.isfile(encodeFilename(ctx.tmpfilename)):
|
|
- ctx.resume_len = os.path.getsize(encodeFilename(ctx.tmpfilename))
|
|
|
|
|
|
+ ctx.resume_len = os.path.getsize(
|
|
|
|
+ encodeFilename(ctx.tmpfilename))
|
|
|
|
+
|
|
|
|
+ ctx.is_resume = ctx.resume_len > 0
|
|
|
|
|
|
count = 0
|
|
count = 0
|
|
retries = self.params.get('retries', 0)
|
|
retries = self.params.get('retries', 0)
|
|
@@ -64,11 +73,33 @@ class HttpFD(FileDownloader):
|
|
def __init__(self, source_error):
|
|
def __init__(self, source_error):
|
|
self.source_error = source_error
|
|
self.source_error = source_error
|
|
|
|
|
|
|
|
+ class NextFragment(Exception):
|
|
|
|
+ pass
|
|
|
|
+
|
|
|
|
+ def set_range(req, start, end):
|
|
|
|
+ range_header = 'bytes=%d-' % start
|
|
|
|
+ if end:
|
|
|
|
+ range_header += compat_str(end)
|
|
|
|
+ req.add_header('Range', range_header)
|
|
|
|
+
|
|
def establish_connection():
|
|
def establish_connection():
|
|
- if ctx.resume_len != 0:
|
|
|
|
- self.report_resuming_byte(ctx.resume_len)
|
|
|
|
- request.add_header('Range', 'bytes=%d-' % ctx.resume_len)
|
|
|
|
|
|
+ if ctx.resume_len > 0:
|
|
|
|
+ range_start = ctx.resume_len
|
|
|
|
+ if ctx.is_resume:
|
|
|
|
+ self.report_resuming_byte(ctx.resume_len)
|
|
ctx.open_mode = 'ab'
|
|
ctx.open_mode = 'ab'
|
|
|
|
+ elif chunk_size > 0:
|
|
|
|
+ range_start = 0
|
|
|
|
+ else:
|
|
|
|
+ range_start = None
|
|
|
|
+ ctx.is_resume = False
|
|
|
|
+ range_end = range_start + chunk_size - 1 if chunk_size else None
|
|
|
|
+ if range_end and ctx.data_len is not None and range_end >= ctx.data_len:
|
|
|
|
+ range_end = ctx.data_len - 1
|
|
|
|
+ has_range = range_start is not None
|
|
|
|
+ ctx.has_range = has_range
|
|
|
|
+ if has_range:
|
|
|
|
+ set_range(request, range_start, range_end)
|
|
# Establish connection
|
|
# Establish connection
|
|
try:
|
|
try:
|
|
ctx.data = self.ydl.urlopen(request)
|
|
ctx.data = self.ydl.urlopen(request)
|
|
@@ -77,25 +108,35 @@ class HttpFD(FileDownloader):
|
|
# that don't support resuming and serve a whole file with no Content-Range
|
|
# that don't support resuming and serve a whole file with no Content-Range
|
|
# set in response despite of requested Range (see
|
|
# set in response despite of requested Range (see
|
|
# https://github.com/rg3/youtube-dl/issues/6057#issuecomment-126129799)
|
|
# https://github.com/rg3/youtube-dl/issues/6057#issuecomment-126129799)
|
|
- if ctx.resume_len > 0:
|
|
|
|
|
|
+ if has_range:
|
|
content_range = ctx.data.headers.get('Content-Range')
|
|
content_range = ctx.data.headers.get('Content-Range')
|
|
if content_range:
|
|
if content_range:
|
|
- content_range_m = re.search(r'bytes (\d+)-', content_range)
|
|
|
|
|
|
+ content_range_m = re.search(r'bytes (\d+)-(\d+)?(?:/(\d+))?', content_range)
|
|
# Content-Range is present and matches requested Range, resume is possible
|
|
# Content-Range is present and matches requested Range, resume is possible
|
|
- if content_range_m and ctx.resume_len == int(content_range_m.group(1)):
|
|
|
|
- return
|
|
|
|
|
|
+ if content_range_m:
|
|
|
|
+ if range_start == int(content_range_m.group(1)):
|
|
|
|
+ content_range_end = int_or_none(content_range_m.group(2))
|
|
|
|
+ content_len = int_or_none(content_range_m.group(3))
|
|
|
|
+ accept_content_len = (
|
|
|
|
+ # Non-chunked download
|
|
|
|
+ not chunk_size or
|
|
|
|
+ # Chunked download and requested piece or
|
|
|
|
+ # its part is promised to be served
|
|
|
|
+ content_range_end == range_end or
|
|
|
|
+ content_len < range_end)
|
|
|
|
+ if accept_content_len:
|
|
|
|
+ ctx.data_len = content_len
|
|
|
|
+ return
|
|
# Content-Range is either not present or invalid. Assuming remote webserver is
|
|
# Content-Range is either not present or invalid. Assuming remote webserver is
|
|
# trying to send the whole file, resume is not possible, so wiping the local file
|
|
# trying to send the whole file, resume is not possible, so wiping the local file
|
|
# and performing entire redownload
|
|
# and performing entire redownload
|
|
self.report_unable_to_resume()
|
|
self.report_unable_to_resume()
|
|
ctx.resume_len = 0
|
|
ctx.resume_len = 0
|
|
ctx.open_mode = 'wb'
|
|
ctx.open_mode = 'wb'
|
|
|
|
+ ctx.data_len = int_or_none(ctx.data.info().get('Content-length', None))
|
|
return
|
|
return
|
|
except (compat_urllib_error.HTTPError, ) as err:
|
|
except (compat_urllib_error.HTTPError, ) as err:
|
|
- if (err.code < 500 or err.code >= 600) and err.code != 416:
|
|
|
|
- # Unexpected HTTP error
|
|
|
|
- raise
|
|
|
|
- elif err.code == 416:
|
|
|
|
|
|
+ if err.code == 416:
|
|
# Unable to resume (requested range not satisfiable)
|
|
# Unable to resume (requested range not satisfiable)
|
|
try:
|
|
try:
|
|
# Open the connection again without the range header
|
|
# Open the connection again without the range header
|
|
@@ -130,6 +171,15 @@ class HttpFD(FileDownloader):
|
|
ctx.resume_len = 0
|
|
ctx.resume_len = 0
|
|
ctx.open_mode = 'wb'
|
|
ctx.open_mode = 'wb'
|
|
return
|
|
return
|
|
|
|
+ elif err.code == 302:
|
|
|
|
+ if not chunk_size:
|
|
|
|
+ raise
|
|
|
|
+ # HTTP Error 302: The HTTP server returned a redirect error that would lead to an infinite loop.
|
|
|
|
+ # may happen during chunk downloading. This is usually fixed
|
|
|
|
+ # with a retry.
|
|
|
|
+ elif err.code < 500 or err.code >= 600:
|
|
|
|
+ # Unexpected HTTP error
|
|
|
|
+ raise
|
|
raise RetryDownload(err)
|
|
raise RetryDownload(err)
|
|
except socket.error as err:
|
|
except socket.error as err:
|
|
if err.errno != errno.ECONNRESET:
|
|
if err.errno != errno.ECONNRESET:
|
|
@@ -160,7 +210,7 @@ class HttpFD(FileDownloader):
|
|
return False
|
|
return False
|
|
|
|
|
|
byte_counter = 0 + ctx.resume_len
|
|
byte_counter = 0 + ctx.resume_len
|
|
- block_size = self.params.get('buffersize', 1024)
|
|
|
|
|
|
+ block_size = ctx.block_size
|
|
start = time.time()
|
|
start = time.time()
|
|
|
|
|
|
# measure time over whole while-loop, so slow_down() and best_block_size() work together properly
|
|
# measure time over whole while-loop, so slow_down() and best_block_size() work together properly
|
|
@@ -233,25 +283,30 @@ class HttpFD(FileDownloader):
|
|
|
|
|
|
# Progress message
|
|
# Progress message
|
|
speed = self.calc_speed(start, now, byte_counter - ctx.resume_len)
|
|
speed = self.calc_speed(start, now, byte_counter - ctx.resume_len)
|
|
- if data_len is None:
|
|
|
|
|
|
+ if ctx.data_len is None:
|
|
eta = None
|
|
eta = None
|
|
else:
|
|
else:
|
|
- eta = self.calc_eta(start, time.time(), data_len - ctx.resume_len, byte_counter - ctx.resume_len)
|
|
|
|
|
|
+ eta = self.calc_eta(start, time.time(), ctx.data_len - ctx.resume_len, byte_counter - ctx.resume_len)
|
|
|
|
|
|
self._hook_progress({
|
|
self._hook_progress({
|
|
'status': 'downloading',
|
|
'status': 'downloading',
|
|
'downloaded_bytes': byte_counter,
|
|
'downloaded_bytes': byte_counter,
|
|
- 'total_bytes': data_len,
|
|
|
|
|
|
+ 'total_bytes': ctx.data_len,
|
|
'tmpfilename': ctx.tmpfilename,
|
|
'tmpfilename': ctx.tmpfilename,
|
|
'filename': ctx.filename,
|
|
'filename': ctx.filename,
|
|
'eta': eta,
|
|
'eta': eta,
|
|
'speed': speed,
|
|
'speed': speed,
|
|
- 'elapsed': now - start,
|
|
|
|
|
|
+ 'elapsed': now - ctx.start_time,
|
|
})
|
|
})
|
|
|
|
|
|
if is_test and byte_counter == data_len:
|
|
if is_test and byte_counter == data_len:
|
|
break
|
|
break
|
|
|
|
|
|
|
|
+ if not is_test and chunk_size and ctx.data_len is not None and byte_counter < ctx.data_len:
|
|
|
|
+ ctx.resume_len = byte_counter
|
|
|
|
+ # ctx.block_size = block_size
|
|
|
|
+ raise NextFragment()
|
|
|
|
+
|
|
if ctx.stream is None:
|
|
if ctx.stream is None:
|
|
self.to_stderr('\n')
|
|
self.to_stderr('\n')
|
|
self.report_error('Did not get any data blocks')
|
|
self.report_error('Did not get any data blocks')
|
|
@@ -276,7 +331,7 @@ class HttpFD(FileDownloader):
|
|
'total_bytes': byte_counter,
|
|
'total_bytes': byte_counter,
|
|
'filename': ctx.filename,
|
|
'filename': ctx.filename,
|
|
'status': 'finished',
|
|
'status': 'finished',
|
|
- 'elapsed': time.time() - start,
|
|
|
|
|
|
+ 'elapsed': time.time() - ctx.start_time,
|
|
})
|
|
})
|
|
|
|
|
|
return True
|
|
return True
|
|
@@ -290,6 +345,8 @@ class HttpFD(FileDownloader):
|
|
if count <= retries:
|
|
if count <= retries:
|
|
self.report_retry(e.source_error, count, retries)
|
|
self.report_retry(e.source_error, count, retries)
|
|
continue
|
|
continue
|
|
|
|
+ except NextFragment:
|
|
|
|
+ continue
|
|
except SucceedDownload:
|
|
except SucceedDownload:
|
|
return True
|
|
return True
|
|
|
|
|