Compare commits

...

9 Commits

Author SHA1 Message Date
dirkf
9f4d83ff42 [options] Add --mtime option, unsets default --no-mtime
* resolves #1709 (!)
2023-04-05 19:05:16 +01:00
dirkf
25124bd640 [devscripts] Improve hack to convert command-line options to API options
* define equality for DateRange
* don't show default DateRange
2023-04-05 19:05:16 +01:00
dirkf
78da22489b [compat] Add and use compat_open() like Py3 open()
* resolves FIXME: ytdl-org/youtube-dl/commit/dfe5fa4
2023-04-05 18:57:37 +01:00
dirkf
557dbac173 [FragmentFD] Fix iteration with infinite limit
* fixes ytdl-org/youtube-dl/baa6c5e
* resolves #31885
2023-04-05 18:55:41 +01:00
dirkf
cdf40b6aa6 [test] Update tests for Ubuntu 20.04
* 18.04 test runner was withdrawn
* for now, disable Py 3.3/3.4 tests
2023-04-05 18:54:30 +01:00
pukkandan
3f6d2bd76f [extractor/youtube] Bypass throttling for -f17
and related cleanup

Thanks @AudricV for the finding

Ref: yt-dlp/yt-dlp/commit/c9abebb
2023-03-19 02:29:00 +00:00
pukkandan
88f28f620b [extractor/youtube] Construct fragment list lazily
Ref: yt-dlp/yt-dlp/commit/e389d17
See: yt-dlp/yt-dlp#6517
2023-03-19 02:29:00 +00:00
dirkf
f35b757c82 [utils] Ensure allow_types for variadic() is a tuple 2023-03-19 02:29:00 +00:00
dirkf
45495228b7 [downloader/http] Only check for resumability when actually resuming 2023-03-19 02:15:41 +00:00
10 changed files with 90 additions and 47 deletions

View File

@ -7,9 +7,10 @@ jobs:
strategy: strategy:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-18.04] os: [ubuntu-20.04]
# TODO: python 2.6 # TODO: python 2.6
python-version: [2.7, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy-2.7, pypy-3.6, pypy-3.7] # TODO: restore support for 3.3, 3.4
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy-2.7, pypy-3.6, pypy-3.7]
python-impl: [cpython] python-impl: [cpython]
ytdl-test-set: [core, download] ytdl-test-set: [core, download]
run-tests-ext: [sh] run-tests-ext: [sh]
@ -26,26 +27,27 @@ jobs:
ytdl-test-set: download ytdl-test-set: download
run-tests-ext: bat run-tests-ext: bat
# jython # jython
- os: ubuntu-18.04 - os: ubuntu-20.04
python-impl: jython python-impl: jython
ytdl-test-set: core ytdl-test-set: core
run-tests-ext: sh run-tests-ext: sh
- os: ubuntu-18.04 - os: ubuntu-20.04
python-impl: jython python-impl: jython
ytdl-test-set: download ytdl-test-set: download
run-tests-ext: sh run-tests-ext: sh
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up supported Python ${{ matrix.python-version }}
uses: actions/setup-python@v2 uses: actions/setup-python@v4
if: ${{ matrix.python-impl == 'cpython' }} if: ${{ matrix.python-impl == 'cpython' && ! contains(fromJSON('["3.3", "3.4"]'), matrix.python-version) }}
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Set up Java 8 - name: Set up Java 8
if: ${{ matrix.python-impl == 'jython' }} if: ${{ matrix.python-impl == 'jython' }}
uses: actions/setup-java@v1 uses: actions/setup-java@v2
with: with:
java-version: 8 java-version: 8
distribution: 'zulu'
- name: Install Jython - name: Install Jython
if: ${{ matrix.python-impl == 'jython' }} if: ${{ matrix.python-impl == 'jython' }}
run: | run: |
@ -70,9 +72,9 @@ jobs:
name: Linter name: Linter
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: 3.9 python-version: 3.9
- name: Install flake8 - name: Install flake8

View File

@ -49,15 +49,34 @@ def cli_to_api(*opts):
# from https://github.com/yt-dlp/yt-dlp/issues/5859#issuecomment-1363938900 # from https://github.com/yt-dlp/yt-dlp/issues/5859#issuecomment-1363938900
default = parsed_options([]) default = parsed_options([])
diff = dict((k, v) for k, v in parsed_options(opts).items() if default[k] != v)
def neq_opt(a, b):
if a == b:
return False
if a is None and repr(type(object)).endswith(".utils.DateRange'>"):
return '0001-01-01 - 9999-12-31' != '{0}'.format(b)
return a != b
diff = dict((k, v) for k, v in parsed_options(opts).items() if neq_opt(default[k], v))
if 'postprocessors' in diff: if 'postprocessors' in diff:
diff['postprocessors'] = [pp for pp in diff['postprocessors'] if pp not in default['postprocessors']] diff['postprocessors'] = [pp for pp in diff['postprocessors'] if pp not in default['postprocessors']]
return diff return diff
def main(): def main():
from pprint import pprint from pprint import PrettyPrinter
pprint(cli_to_api(*sys.argv))
pprint = PrettyPrinter()
super_format = pprint.format
def format(object, context, maxlevels, level):
if repr(type(object)).endswith(".utils.DateRange'>"):
return '{0}: {1}>'.format(repr(object)[:-2], object), True, False
return super_format(object, context, maxlevels, level)
pprint.format = format
pprint.pprint(cli_to_api(*sys.argv))
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -88,7 +88,7 @@ class TestHttpFD(unittest.TestCase):
self.assertTrue(downloader.real_download(filename, { self.assertTrue(downloader.real_download(filename, {
'url': 'http://127.0.0.1:%d/%s' % (self.port, ep), 'url': 'http://127.0.0.1:%d/%s' % (self.port, ep),
})) }))
self.assertEqual(os.path.getsize(encodeFilename(filename)), TEST_SIZE) self.assertEqual(os.path.getsize(encodeFilename(filename)), TEST_SIZE, ep)
try_rm(encodeFilename(filename)) try_rm(encodeFilename(filename))
def download_all(self, params): def download_all(self, params):

View File

@ -1563,6 +1563,7 @@ Line 1
self.assertEqual(variadic(None), (None, )) self.assertEqual(variadic(None), (None, ))
self.assertEqual(variadic('spam'), ('spam', )) self.assertEqual(variadic('spam'), ('spam', ))
self.assertEqual(variadic('spam', allowed_types=dict), 'spam') self.assertEqual(variadic('spam', allowed_types=dict), 'spam')
self.assertEqual(variadic('spam', allowed_types=[dict]), 'spam')
def test_traverse_obj(self): def test_traverse_obj(self):
_TEST_DATA = { _TEST_DATA = {

View File

@ -3127,6 +3127,16 @@ else:
return ctypes.WINFUNCTYPE(*args, **kwargs) return ctypes.WINFUNCTYPE(*args, **kwargs)
if sys.version_info < (3, 0):
# open(file, mode='r', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True) not: opener=None
def compat_open(file_, *args, **kwargs):
if len(args) > 6 or 'opener' in kwargs:
raise ValueError('open: unsupported argument "opener"')
return io.open(file_, *args, **kwargs)
else:
compat_open = open
legacy = [ legacy = [
'compat_HTMLParseError', 'compat_HTMLParseError',
'compat_HTMLParser', 'compat_HTMLParser',
@ -3185,6 +3195,7 @@ __all__ = [
'compat_kwargs', 'compat_kwargs',
'compat_map', 'compat_map',
'compat_numeric_types', 'compat_numeric_types',
'compat_open',
'compat_ord', 'compat_ord',
'compat_os_name', 'compat_os_name',
'compat_os_path_expanduser', 'compat_os_path_expanduser',

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import itertools
from .fragment import FragmentFD from .fragment import FragmentFD
from ..compat import compat_urllib_error from ..compat import compat_urllib_error
from ..utils import ( from ..utils import (
@ -30,15 +32,13 @@ class DashSegmentsFD(FragmentFD):
fragment_retries = self.params.get('fragment_retries', 0) fragment_retries = self.params.get('fragment_retries', 0)
skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True) skip_unavailable_fragments = self.params.get('skip_unavailable_fragments', True)
frag_index = 0 for frag_index, fragment in enumerate(fragments, 1):
for i, fragment in enumerate(fragments):
frag_index += 1
if frag_index <= ctx['fragment_index']: if frag_index <= ctx['fragment_index']:
continue continue
# In DASH, the first segment contains necessary headers to # In DASH, the first segment contains necessary headers to
# generate a valid MP4 file, so always abort for the first segment # generate a valid MP4 file, so always abort for the first segment
fatal = i == 0 or not skip_unavailable_fragments fatal = frag_index == 1 or not skip_unavailable_fragments
for count in range(fragment_retries + 1): for count in itertools.count():
try: try:
fragment_url = fragment.get('url') fragment_url = fragment.get('url')
if not fragment_url: if not fragment_url:
@ -48,7 +48,6 @@ class DashSegmentsFD(FragmentFD):
if not success: if not success:
return False return False
self._append_fragment(ctx, frag_content) self._append_fragment(ctx, frag_content)
break
except compat_urllib_error.HTTPError as err: except compat_urllib_error.HTTPError as err:
# YouTube may often return 404 HTTP error for a fragment causing the # YouTube may often return 404 HTTP error for a fragment causing the
# whole download to fail. However if the same fragment is immediately # whole download to fail. However if the same fragment is immediately
@ -58,13 +57,14 @@ class DashSegmentsFD(FragmentFD):
# HTTP error. # HTTP error.
if count < fragment_retries: if count < fragment_retries:
self.report_retry_fragment(err, frag_index, count + 1, fragment_retries) self.report_retry_fragment(err, frag_index, count + 1, fragment_retries)
continue
except DownloadError: except DownloadError:
# Don't retry fragment if error occurred during HTTP downloading # Don't retry fragment if error occurred during HTTP downloading
# itself since it has own retry settings # itself since it has its own retry settings
if not fatal: if fatal:
self.report_skip_fragment(frag_index) raise
break self.report_skip_fragment(frag_index)
raise break
if count >= fragment_retries: if count >= fragment_retries:
if not fatal: if not fatal:

View File

@ -141,7 +141,8 @@ class HttpFD(FileDownloader):
# 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() if range_start > 0:
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)) ctx.data_len = int_or_none(ctx.data.info().get('Content-length', None))

View File

@ -31,6 +31,7 @@ from ..utils import (
get_element_by_attribute, get_element_by_attribute,
int_or_none, int_or_none,
js_to_json, js_to_json,
LazyList,
merge_dicts, merge_dicts,
mimetype2ext, mimetype2ext,
parse_codecs, parse_codecs,
@ -1986,9 +1987,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
itags = [] itags = []
itag_qualities = {} itag_qualities = {}
q = qualities(['tiny', 'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres']) q = qualities(['tiny', 'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres'])
CHUNK_SIZE = 10 << 20
streaming_data = player_response.get('streamingData') or {} streaming_data = player_response.get('streamingData') or {}
streaming_formats = streaming_data.get('formats') or [] streaming_formats = streaming_data.get('formats') or []
streaming_formats.extend(streaming_data.get('adaptiveFormats') or []) streaming_formats.extend(streaming_data.get('adaptiveFormats') or [])
def build_fragments(f):
return LazyList({
'url': update_url_query(f['url'], {
'range': '{0}-{1}'.format(range_start, min(range_start + CHUNK_SIZE - 1, f['filesize']))
})
} for range_start in range(0, f['filesize'], CHUNK_SIZE))
for fmt in streaming_formats: for fmt in streaming_formats:
if fmt.get('targetDurationSec') or fmt.get('drmFamilies'): if fmt.get('targetDurationSec') or fmt.get('drmFamilies'):
continue continue
@ -2041,28 +2052,18 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
if mobj: if mobj:
dct['ext'] = mimetype2ext(mobj.group(1)) dct['ext'] = mimetype2ext(mobj.group(1))
dct.update(parse_codecs(mobj.group(2))) dct.update(parse_codecs(mobj.group(2)))
no_audio = dct.get('acodec') == 'none' single_stream = 'none' in (dct.get(c) for c in ('acodec', 'vcodec'))
no_video = dct.get('vcodec') == 'none' if single_stream and dct.get('ext'):
if no_audio: dct['container'] = dct['ext'] + '_dash'
dct['vbr'] = tbr if single_stream or itag == '17':
if no_video:
dct['abr'] = tbr
if no_audio or no_video:
CHUNK_SIZE = 10 << 20
# avoid Youtube throttling # avoid Youtube throttling
dct.update({ dct.update({
'protocol': 'http_dash_segments', 'protocol': 'http_dash_segments',
'fragments': [{ 'fragments': build_fragments(dct),
'url': update_url_query(dct['url'], {
'range': '{0}-{1}'.format(range_start, min(range_start + CHUNK_SIZE - 1, dct['filesize']))
})
} for range_start in range(0, dct['filesize'], CHUNK_SIZE)]
} if dct['filesize'] else { } if dct['filesize'] else {
'downloader_options': {'http_chunk_size': CHUNK_SIZE} # No longer useful? 'downloader_options': {'http_chunk_size': CHUNK_SIZE} # No longer useful?
}) })
if dct.get('ext'):
dct['container'] = dct['ext'] + '_dash'
formats.append(dct) formats.append(dct)
hls_manifest_url = streaming_data.get('hlsManifestUrl') hls_manifest_url = streaming_data.get('hlsManifestUrl')

View File

@ -11,6 +11,7 @@ from .compat import (
compat_get_terminal_size, compat_get_terminal_size,
compat_getenv, compat_getenv,
compat_kwargs, compat_kwargs,
compat_open as open,
compat_shlex_split, compat_shlex_split,
) )
from .utils import ( from .utils import (
@ -41,14 +42,11 @@ def _hide_login_info(opts):
def parseOpts(overrideArguments=None): def parseOpts(overrideArguments=None):
def _readOptions(filename_bytes, default=[]): def _readOptions(filename_bytes, default=[]):
try: try:
optionf = open(filename_bytes) optionf = open(filename_bytes, encoding=preferredencoding())
except IOError: except IOError:
return default # silently skip if file is not present return default # silently skip if file is not present
try: try:
# FIXME: https://github.com/ytdl-org/youtube-dl/commit/dfe5fa49aed02cf36ba9f743b11b0903554b5e56
contents = optionf.read() contents = optionf.read()
if sys.version_info < (3,):
contents = contents.decode(preferredencoding())
res = compat_shlex_split(contents, comments=True) res = compat_shlex_split(contents, comments=True)
finally: finally:
optionf.close() optionf.close()
@ -733,9 +731,13 @@ def parseOpts(overrideArguments=None):
'--no-part', '--no-part',
action='store_true', dest='nopart', default=False, action='store_true', dest='nopart', default=False,
help='Do not use .part files - write directly into output file') help='Do not use .part files - write directly into output file')
filesystem.add_option(
'--mtime',
action='store_true', dest='updatetime', default=True,
help='Use the Last-modified header to set the file modification time (default)')
filesystem.add_option( filesystem.add_option(
'--no-mtime', '--no-mtime',
action='store_false', dest='updatetime', default=True, action='store_false', dest='updatetime',
help='Do not use the Last-modified header to set the file modification time') help='Do not use the Last-modified header to set the file modification time')
filesystem.add_option( filesystem.add_option(
'--write-description', '--write-description',

View File

@ -3190,6 +3190,10 @@ class DateRange(object):
def __str__(self): def __str__(self):
return '%s - %s' % (self.start.isoformat(), self.end.isoformat()) return '%s - %s' % (self.start.isoformat(), self.end.isoformat())
def __eq__(self, other):
return (isinstance(other, DateRange)
and self.start == other.start and self.end == other.end)
def platform_name(): def platform_name():
""" Returns the platform name as a compat_str """ """ Returns the platform name as a compat_str """
@ -4213,6 +4217,8 @@ def multipart_encode(data, boundary=None):
def variadic(x, allowed_types=(compat_str, bytes, dict)): def variadic(x, allowed_types=(compat_str, bytes, dict)):
if not isinstance(allowed_types, tuple) and isinstance(allowed_types, compat_collections_abc.Iterable):
allowed_types = tuple(allowed_types)
return x if isinstance(x, compat_collections_abc.Iterable) and not isinstance(x, allowed_types) else (x,) return x if isinstance(x, compat_collections_abc.Iterable) and not isinstance(x, allowed_types) else (x,)