@ -7,6 +7,11 @@ try:
can_decrypt_frag = True
can_decrypt_frag = True
except ImportError :
except ImportError :
can_decrypt_frag = False
can_decrypt_frag = False
try :
import concurrent . futures
can_threaded_download = True
except ImportError :
can_threaded_download = False
from . . downloader import _get_real_downloader
from . . downloader import _get_real_downloader
from . fragment import FragmentFD
from . fragment import FragmentFD
@ -19,6 +24,7 @@ from ..compat import (
)
)
from . . utils import (
from . . utils import (
parse_m3u8_attributes ,
parse_m3u8_attributes ,
sanitize_open ,
update_url_query ,
update_url_query ,
)
)
@ -151,7 +157,6 @@ class HlsFD(FragmentFD):
ad_frag_next = False
ad_frag_next = False
for line in s . splitlines ( ) :
for line in s . splitlines ( ) :
line = line . strip ( )
line = line . strip ( )
download_frag = False
if line :
if line :
if not line . startswith ( ' # ' ) :
if not line . startswith ( ' # ' ) :
if format_index and discontinuity_count != format_index :
if format_index and discontinuity_count != format_index :
@ -168,13 +173,13 @@ class HlsFD(FragmentFD):
if extra_query :
if extra_query :
frag_url = update_url_query ( frag_url , extra_query )
frag_url = update_url_query ( frag_url , extra_query )
if real_downloader :
fragments . append ( {
fragments . append ( {
' frag_index ' : frag_index ,
' url ' : frag_url ,
' url ' : frag_url ,
' decrypt_info ' : decrypt_info ,
' decrypt_info ' : decrypt_info ,
} )
' byte_range ' : byte_range ,
continue
' media_sequence ' : media_sequence ,
download_frag = True
} )
elif line . startswith ( ' #EXT-X-MAP ' ) :
elif line . startswith ( ' #EXT-X-MAP ' ) :
if format_index and discontinuity_count != format_index :
if format_index and discontinuity_count != format_index :
@ -191,12 +196,14 @@ class HlsFD(FragmentFD):
else compat_urlparse . urljoin ( man_url , map_info . get ( ' URI ' ) ) )
else compat_urlparse . urljoin ( man_url , map_info . get ( ' URI ' ) ) )
if extra_query :
if extra_query :
frag_url = update_url_query ( frag_url , extra_query )
frag_url = update_url_query ( frag_url , extra_query )
if real_downloader :
fragments . append ( {
fragments . append ( {
' url ' : frag_url ,
' frag_index ' : frag_index ,
' decrypt_info ' : decrypt_info ,
' url ' : frag_url ,
} )
' decrypt_info ' : decrypt_info ,
continue
' byte_range ' : byte_range ,
' media_sequence ' : media_sequence
} )
if map_info . get ( ' BYTERANGE ' ) :
if map_info . get ( ' BYTERANGE ' ) :
splitted_byte_range = map_info . get ( ' BYTERANGE ' ) . split ( ' @ ' )
splitted_byte_range = map_info . get ( ' BYTERANGE ' ) . split ( ' @ ' )
@ -205,7 +212,6 @@ class HlsFD(FragmentFD):
' start ' : sub_range_start ,
' start ' : sub_range_start ,
' end ' : sub_range_start + int ( splitted_byte_range [ 0 ] ) ,
' end ' : sub_range_start + int ( splitted_byte_range [ 0 ] ) ,
}
}
download_frag = True
elif line . startswith ( ' #EXT-X-KEY ' ) :
elif line . startswith ( ' #EXT-X-KEY ' ) :
decrypt_url = decrypt_info . get ( ' URI ' )
decrypt_url = decrypt_info . get ( ' URI ' )
@ -236,53 +242,12 @@ class HlsFD(FragmentFD):
ad_frag_next = False
ad_frag_next = False
elif line . startswith ( ' #EXT-X-DISCONTINUITY ' ) :
elif line . startswith ( ' #EXT-X-DISCONTINUITY ' ) :
discontinuity_count + = 1
discontinuity_count + = 1
i + = 1
media_sequence + = 1
if download_frag :
# We only download the first fragment during the test
count = 0
if test :
headers = info_dict . get ( ' http_headers ' , { } )
fragments = [ fragments [ 0 ] if fragments else None ]
if byte_range :
headers [ ' Range ' ] = ' bytes= %d - %d ' % ( byte_range [ ' start ' ] , byte_range [ ' end ' ] - 1 )
while count < = fragment_retries :
try :
success , frag_content = self . _download_fragment (
ctx , frag_url , info_dict , headers )
if not success :
return False
break
except compat_urllib_error . HTTPError as err :
# Unavailable (possibly temporary) fragments may be served.
# First we try to retry then either skip or abort.
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
# https://github.com/ytdl-org/youtube-dl/issues/10448).
count + = 1
if count < = fragment_retries :
self . report_retry_fragment ( err , frag_index , count , fragment_retries )
if count > fragment_retries :
if skip_unavailable_fragments :
i + = 1
media_sequence + = 1
self . report_skip_fragment ( frag_index )
continue
self . report_error (
' giving up after %s fragment retries ' % fragment_retries )
return False
if decrypt_info [ ' METHOD ' ] == ' AES-128 ' :
iv = decrypt_info . get ( ' IV ' ) or compat_struct_pack ( ' >8xq ' , media_sequence )
decrypt_info [ ' KEY ' ] = decrypt_info . get ( ' KEY ' ) or self . ydl . urlopen (
self . _prepare_url ( info_dict , info_dict . get ( ' _decryption_key_url ' ) or decrypt_info [ ' URI ' ] ) ) . read ( )
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
# not what it decrypts to.
if not test :
frag_content = AES . new (
decrypt_info [ ' KEY ' ] , AES . MODE_CBC , iv ) . decrypt ( frag_content )
self . _append_fragment ( ctx , frag_content )
# We only download the first fragment during the test
if test :
break
i + = 1
media_sequence + = 1
if real_downloader :
if real_downloader :
info_copy = info_dict . copy ( )
info_copy = info_dict . copy ( )
@ -295,5 +260,106 @@ class HlsFD(FragmentFD):
if not success :
if not success :
return False
return False
else :
else :
def download_fragment ( fragment ) :
frag_index = fragment [ ' frag_index ' ]
frag_url = fragment [ ' url ' ]
decrypt_info = fragment [ ' decrypt_info ' ]
byte_range = fragment [ ' byte_range ' ]
media_sequence = fragment [ ' media_sequence ' ]
ctx [ ' fragment_index ' ] = frag_index
count = 0
headers = info_dict . get ( ' http_headers ' , { } )
if byte_range :
headers [ ' Range ' ] = ' bytes= %d - %d ' % ( byte_range [ ' start ' ] , byte_range [ ' end ' ] - 1 )
while count < = fragment_retries :
try :
success , frag_content = self . _download_fragment (
ctx , frag_url , info_dict , headers )
if not success :
return False , frag_index
break
except compat_urllib_error . HTTPError as err :
# Unavailable (possibly temporary) fragments may be served.
# First we try to retry then either skip or abort.
# See https://github.com/ytdl-org/youtube-dl/issues/10165,
# https://github.com/ytdl-org/youtube-dl/issues/10448).
count + = 1
if count < = fragment_retries :
self . report_retry_fragment ( err , frag_index , count , fragment_retries )
if count > fragment_retries :
return False , frag_index
if decrypt_info [ ' METHOD ' ] == ' AES-128 ' :
iv = decrypt_info . get ( ' IV ' ) or compat_struct_pack ( ' >8xq ' , media_sequence )
decrypt_info [ ' KEY ' ] = decrypt_info . get ( ' KEY ' ) or self . ydl . urlopen (
self . _prepare_url ( info_dict , info_dict . get ( ' _decryption_key_url ' ) or decrypt_info [ ' URI ' ] ) ) . read ( )
# Don't decrypt the content in tests since the data is explicitly truncated and it's not to a valid block
# size (see https://github.com/ytdl-org/youtube-dl/pull/27660). Tests only care that the correct data downloaded,
# not what it decrypts to.
if not test :
frag_content = AES . new (
decrypt_info [ ' KEY ' ] , AES . MODE_CBC , iv ) . decrypt ( frag_content )
return frag_content , frag_index
def append_fragment ( frag_content , frag_index ) :
if frag_content :
fragment_filename = ' %s -Frag %d ' % ( ctx [ ' tmpfilename ' ] , frag_index )
try :
file , frag_sanitized = sanitize_open ( fragment_filename , ' rb ' )
ctx [ ' fragment_filename_sanitized ' ] = frag_sanitized
file . close ( )
self . _append_fragment ( ctx , frag_content )
return True
except FileNotFoundError :
if skip_unavailable_fragments :
self . report_skip_fragment ( frag_index )
return True
else :
self . report_error (
' fragment %s not found, unable to continue ' % frag_index )
return False
else :
if skip_unavailable_fragments :
self . report_skip_fragment ( frag_index )
return True
else :
self . report_error (
' fragment %s not found, unable to continue ' % frag_index )
return False
max_workers = self . params . get ( ' concurrent_fragment_downloads ' , 1 )
if can_threaded_download and max_workers > 1 :
self . report_warning ( ' The download speed shown is only of one thread. This is a known issue ' )
with concurrent . futures . ThreadPoolExecutor ( max_workers ) as pool :
futures = [ pool . submit ( download_fragment , fragment ) for fragment in fragments ]
# timeout must be 0 to return instantly
done , not_done = concurrent . futures . wait ( futures , timeout = 0 )
try :
while not_done :
# Check every 1 second for KeyboardInterrupt
freshly_done , not_done = concurrent . futures . wait ( not_done , timeout = 1 )
done | = freshly_done
except KeyboardInterrupt :
for future in not_done :
future . cancel ( )
# timeout must be none to cancel
concurrent . futures . wait ( not_done , timeout = None )
raise KeyboardInterrupt
results = [ future . result ( ) for future in futures ]
for frag_content , frag_index in results :
result = append_fragment ( frag_content , frag_index )
if not result :
return False
else :
for fragment in fragments :
frag_content , frag_index = download_fragment ( fragment )
result = append_fragment ( frag_content , frag_index )
if not result :
return False
self . _finish_frag_download ( ctx )
self . _finish_frag_download ( ctx )
return True
return True