mirror of
https://github.com/ytdl-org/youtube-dl.git
synced 2026-09-03 01:05:49 +00:00
Merge branch 'master' into fix-npo-support
This commit is contained in:
commit
0ed52e0fd3
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -122,12 +122,12 @@ jobs:
|
|||
ytdl-test-set: ${{ fromJSON(needs.select.outputs.test-set) }}
|
||||
run-tests-ext: [sh]
|
||||
include:
|
||||
- os: windows-2019
|
||||
- os: windows-2022
|
||||
python-version: 3.4
|
||||
python-impl: cpython
|
||||
ytdl-test-set: ${{ contains(needs.select.outputs.test-set, 'core') && 'core' || 'nocore' }}
|
||||
run-tests-ext: bat
|
||||
- os: windows-2019
|
||||
- os: windows-2022
|
||||
python-version: 3.4
|
||||
python-impl: cpython
|
||||
ytdl-test-set: ${{ contains(needs.select.outputs.test-set, 'download') && 'download' || 'nodownload' }}
|
||||
|
|
@ -365,7 +365,7 @@ jobs:
|
|||
python -m ensurepip || python -m pip --version || { \
|
||||
get_pip="${{ contains(needs.select.outputs.own-pip-versions, matrix.python-version) && format('{0}/', matrix.python-version) || '' }}"; \
|
||||
curl -L -O "https://bootstrap.pypa.io/pip/${get_pip}get-pip.py"; \
|
||||
python get-pip.py; }
|
||||
python get-pip.py --no-setuptools --no-wheel; }
|
||||
- name: Set up Python 2.6 pip
|
||||
if: ${{ matrix.python-version == '2.6' }}
|
||||
shell: bash
|
||||
|
|
|
|||
|
|
@ -85,10 +85,10 @@ class FakeYDL(YoutubeDL):
|
|||
# Silence an expected warning matching a regex
|
||||
old_report_warning = self.report_warning
|
||||
|
||||
def report_warning(self, message):
|
||||
def report_warning(self, message, *args, **kwargs):
|
||||
if re.match(regex, message):
|
||||
return
|
||||
old_report_warning(message)
|
||||
old_report_warning(message, *args, **kwargs)
|
||||
self.report_warning = types.MethodType(report_warning, self)
|
||||
|
||||
|
||||
|
|
@ -265,11 +265,11 @@ def assertRegexpMatches(self, text, regexp, msg=None):
|
|||
def expect_warnings(ydl, warnings_re):
|
||||
real_warning = ydl.report_warning
|
||||
|
||||
def _report_warning(w):
|
||||
def _report_warning(self, w, *args, **kwargs):
|
||||
if not any(re.search(w_re, w) for w_re in warnings_re):
|
||||
real_warning(w)
|
||||
|
||||
ydl.report_warning = _report_warning
|
||||
ydl.report_warning = types.MethodType(_report_warning, ydl)
|
||||
|
||||
|
||||
def http_server_port(httpd):
|
||||
|
|
|
|||
|
|
@ -9,21 +9,32 @@ import unittest
|
|||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
import itertools
|
||||
import re
|
||||
|
||||
from youtube_dl.traversal import (
|
||||
dict_get,
|
||||
get_first,
|
||||
require,
|
||||
subs_list_to_dict,
|
||||
T,
|
||||
traverse_obj,
|
||||
unpack,
|
||||
value,
|
||||
)
|
||||
from youtube_dl.compat import (
|
||||
compat_chr as chr,
|
||||
compat_etree_fromstring,
|
||||
compat_http_cookies,
|
||||
compat_map as map,
|
||||
compat_str,
|
||||
compat_zip as zip,
|
||||
)
|
||||
from youtube_dl.utils import (
|
||||
determine_ext,
|
||||
ExtractorError,
|
||||
int_or_none,
|
||||
join_nonempty,
|
||||
str_or_none,
|
||||
)
|
||||
|
||||
|
|
@ -446,42 +457,164 @@ class TestTraversal(_TestCase):
|
|||
msg='`any` should allow further branching')
|
||||
|
||||
def test_traversal_morsel(self):
|
||||
values = {
|
||||
'expires': 'a',
|
||||
'path': 'b',
|
||||
'comment': 'c',
|
||||
'domain': 'd',
|
||||
'max-age': 'e',
|
||||
'secure': 'f',
|
||||
'httponly': 'g',
|
||||
'version': 'h',
|
||||
'samesite': 'i',
|
||||
}
|
||||
# SameSite added in Py3.8, breaks .update for 3.5-3.7
|
||||
if sys.version_info < (3, 8):
|
||||
del values['samesite']
|
||||
morsel = compat_http_cookies.Morsel()
|
||||
# SameSite added in Py3.8, breaks .update for 3.5-3.7
|
||||
# Similarly Partitioned, Py3.14, thx Grub4k
|
||||
values = dict(zip(morsel, map(chr, itertools.count(ord('a')))))
|
||||
morsel.set(str('item_key'), 'item_value', 'coded_value')
|
||||
morsel.update(values)
|
||||
values['key'] = str('item_key')
|
||||
values['value'] = 'item_value'
|
||||
values.update({
|
||||
'key': str('item_key'),
|
||||
'value': 'item_value',
|
||||
}),
|
||||
values = dict((str(k), v) for k, v in values.items())
|
||||
# make test pass even without ordered dict
|
||||
value_set = set(values.values())
|
||||
|
||||
for key, value in values.items():
|
||||
self.assertEqual(traverse_obj(morsel, key), value,
|
||||
for key, val in values.items():
|
||||
self.assertEqual(traverse_obj(morsel, key), val,
|
||||
msg='Morsel should provide access to all values')
|
||||
self.assertEqual(set(traverse_obj(morsel, Ellipsis)), value_set,
|
||||
msg='`...` should yield all values')
|
||||
self.assertEqual(set(traverse_obj(morsel, lambda k, v: True)), value_set,
|
||||
msg='function key should yield all values')
|
||||
values = list(values.values())
|
||||
self.assertMaybeCountEqual(traverse_obj(morsel, Ellipsis), values,
|
||||
msg='`...` should yield all values')
|
||||
self.assertMaybeCountEqual(traverse_obj(morsel, lambda k, v: True), values,
|
||||
msg='function key should yield all values')
|
||||
self.assertIs(traverse_obj(morsel, [(None,), any]), morsel,
|
||||
msg='Morsel should not be implicitly changed to dict on usage')
|
||||
|
||||
def test_get_first(self):
|
||||
self.assertEqual(get_first([{'a': None}, {'a': 'spam'}], 'a'), 'spam')
|
||||
def test_traversal_filter(self):
|
||||
data = [None, False, True, 0, 1, 0.0, 1.1, '', 'str', {}, {0: 0}, [], [1]]
|
||||
|
||||
self.assertEqual(
|
||||
traverse_obj(data, (Ellipsis, filter)),
|
||||
[True, 1, 1.1, 'str', {0: 0}, [1]],
|
||||
'`filter` should filter falsy values')
|
||||
|
||||
|
||||
class TestTraversalHelpers(_TestCase):
|
||||
def test_traversal_require(self):
|
||||
with self.assertRaises(ExtractorError, msg='Missing `value` should raise'):
|
||||
traverse_obj(_TEST_DATA, ('None', T(require('value'))))
|
||||
self.assertEqual(
|
||||
traverse_obj(_TEST_DATA, ('str', T(require('value')))), 'str',
|
||||
'`require` should pass through non-`None` values')
|
||||
|
||||
def test_subs_list_to_dict(self):
|
||||
self.assertEqual(traverse_obj([
|
||||
{'name': 'de', 'url': 'https://example.com/subs/de.vtt'},
|
||||
{'name': 'en', 'url': 'https://example.com/subs/en1.ass'},
|
||||
{'name': 'en', 'url': 'https://example.com/subs/en2.ass'},
|
||||
], [Ellipsis, {
|
||||
'id': 'name',
|
||||
'url': 'url',
|
||||
}, all, T(subs_list_to_dict)]), {
|
||||
'de': [{'url': 'https://example.com/subs/de.vtt'}],
|
||||
'en': [
|
||||
{'url': 'https://example.com/subs/en1.ass'},
|
||||
{'url': 'https://example.com/subs/en2.ass'},
|
||||
],
|
||||
}, 'function should build subtitle dict from list of subtitles')
|
||||
self.assertEqual(traverse_obj([
|
||||
{'name': 'de', 'url': 'https://example.com/subs/de.ass'},
|
||||
{'name': 'de'},
|
||||
{'name': 'en', 'content': 'content'},
|
||||
{'url': 'https://example.com/subs/en'},
|
||||
], [Ellipsis, {
|
||||
'id': 'name',
|
||||
'data': 'content',
|
||||
'url': 'url',
|
||||
}, all, T(subs_list_to_dict(lang=None))]), {
|
||||
'de': [{'url': 'https://example.com/subs/de.ass'}],
|
||||
'en': [{'data': 'content'}],
|
||||
}, 'subs with mandatory items missing should be filtered')
|
||||
self.assertEqual(traverse_obj([
|
||||
{'url': 'https://example.com/subs/de.ass', 'name': 'de'},
|
||||
{'url': 'https://example.com/subs/en', 'name': 'en'},
|
||||
], [Ellipsis, {
|
||||
'id': 'name',
|
||||
'ext': ['url', T(determine_ext(default_ext=None))],
|
||||
'url': 'url',
|
||||
}, all, T(subs_list_to_dict(ext='ext'))]), {
|
||||
'de': [{'url': 'https://example.com/subs/de.ass', 'ext': 'ass'}],
|
||||
'en': [{'url': 'https://example.com/subs/en', 'ext': 'ext'}],
|
||||
}, '`ext` should set default ext but leave existing value untouched')
|
||||
self.assertEqual(traverse_obj([
|
||||
{'name': 'en', 'url': 'https://example.com/subs/en2', 'prio': True},
|
||||
{'name': 'en', 'url': 'https://example.com/subs/en1', 'prio': False},
|
||||
], [Ellipsis, {
|
||||
'id': 'name',
|
||||
'quality': ['prio', T(int)],
|
||||
'url': 'url',
|
||||
}, all, T(subs_list_to_dict(ext='ext'))]), {'en': [
|
||||
{'url': 'https://example.com/subs/en1', 'ext': 'ext'},
|
||||
{'url': 'https://example.com/subs/en2', 'ext': 'ext'},
|
||||
]}, '`quality` key should sort subtitle list accordingly')
|
||||
self.assertEqual(traverse_obj([
|
||||
{'name': 'de', 'url': 'https://example.com/subs/de.ass'},
|
||||
{'name': 'de'},
|
||||
{'name': 'en', 'content': 'content'},
|
||||
{'url': 'https://example.com/subs/en'},
|
||||
], [Ellipsis, {
|
||||
'id': 'name',
|
||||
'url': 'url',
|
||||
'data': 'content',
|
||||
}, all, T(subs_list_to_dict(lang='en'))]), {
|
||||
'de': [{'url': 'https://example.com/subs/de.ass'}],
|
||||
'en': [
|
||||
{'data': 'content'},
|
||||
{'url': 'https://example.com/subs/en'},
|
||||
],
|
||||
}, 'optionally provided lang should be used if no id available')
|
||||
self.assertEqual(traverse_obj([
|
||||
{'name': 1, 'url': 'https://example.com/subs/de1'},
|
||||
{'name': {}, 'url': 'https://example.com/subs/de2'},
|
||||
{'name': 'de', 'ext': 1, 'url': 'https://example.com/subs/de3'},
|
||||
{'name': 'de', 'ext': {}, 'url': 'https://example.com/subs/de4'},
|
||||
], [Ellipsis, {
|
||||
'id': 'name',
|
||||
'url': 'url',
|
||||
'ext': 'ext',
|
||||
}, all, T(subs_list_to_dict(lang=None))]), {
|
||||
'de': [
|
||||
{'url': 'https://example.com/subs/de3'},
|
||||
{'url': 'https://example.com/subs/de4'},
|
||||
],
|
||||
}, 'non str types should be ignored for id and ext')
|
||||
self.assertEqual(traverse_obj([
|
||||
{'name': 1, 'url': 'https://example.com/subs/de1'},
|
||||
{'name': {}, 'url': 'https://example.com/subs/de2'},
|
||||
{'name': 'de', 'ext': 1, 'url': 'https://example.com/subs/de3'},
|
||||
{'name': 'de', 'ext': {}, 'url': 'https://example.com/subs/de4'},
|
||||
], [Ellipsis, {
|
||||
'id': 'name',
|
||||
'url': 'url',
|
||||
'ext': 'ext',
|
||||
}, all, T(subs_list_to_dict(lang='de'))]), {
|
||||
'de': [
|
||||
{'url': 'https://example.com/subs/de1'},
|
||||
{'url': 'https://example.com/subs/de2'},
|
||||
{'url': 'https://example.com/subs/de3'},
|
||||
{'url': 'https://example.com/subs/de4'},
|
||||
],
|
||||
}, 'non str types should be replaced by default id')
|
||||
|
||||
def test_unpack(self):
|
||||
self.assertEqual(
|
||||
unpack(lambda *x: ''.join(map(compat_str, x)))([1, 2, 3]), '123')
|
||||
self.assertEqual(
|
||||
unpack(join_nonempty)([1, 2, 3]), '1-2-3')
|
||||
self.assertEqual(
|
||||
unpack(join_nonempty, delim=' ')([1, 2, 3]), '1 2 3')
|
||||
with self.assertRaises(TypeError):
|
||||
unpack(join_nonempty)()
|
||||
with self.assertRaises(TypeError):
|
||||
unpack()
|
||||
|
||||
def test_value(self):
|
||||
self.assertEqual(
|
||||
traverse_obj(_TEST_DATA, ('str', T(value('other')))), 'other',
|
||||
'`value` should substitute specified value')
|
||||
|
||||
|
||||
class TestDictGet(_TestCase):
|
||||
def test_dict_get(self):
|
||||
FALSE_VALUES = {
|
||||
'none': None,
|
||||
|
|
@ -504,6 +637,9 @@ class TestTraversal(_TestCase):
|
|||
self.assertEqual(dict_get(d, ('b', 'c', key, )), None)
|
||||
self.assertEqual(dict_get(d, ('b', 'c', key, ), skip_false_values=False), false_value)
|
||||
|
||||
def test_get_first(self):
|
||||
self.assertEqual(get_first([{'a': None}, {'a': 'spam'}], 'a'), 'spam')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ from youtube_dl.utils import (
|
|||
parse_iso8601,
|
||||
parse_resolution,
|
||||
parse_qs,
|
||||
partial_application,
|
||||
pkcs1pad,
|
||||
prepend_extension,
|
||||
read_batch_urls,
|
||||
|
|
@ -664,6 +665,8 @@ class TestUtil(unittest.TestCase):
|
|||
self.assertEqual(parse_duration('3h 11m 53s'), 11513)
|
||||
self.assertEqual(parse_duration('3 hours 11 minutes 53 seconds'), 11513)
|
||||
self.assertEqual(parse_duration('3 hours 11 mins 53 secs'), 11513)
|
||||
self.assertEqual(parse_duration('3 hours, 11 minutes, 53 seconds'), 11513)
|
||||
self.assertEqual(parse_duration('3 hours, 11 mins, 53 secs'), 11513)
|
||||
self.assertEqual(parse_duration('62m45s'), 3765)
|
||||
self.assertEqual(parse_duration('6m59s'), 419)
|
||||
self.assertEqual(parse_duration('49s'), 49)
|
||||
|
|
@ -682,6 +685,10 @@ class TestUtil(unittest.TestCase):
|
|||
self.assertEqual(parse_duration('PT1H0.040S'), 3600.04)
|
||||
self.assertEqual(parse_duration('PT00H03M30SZ'), 210)
|
||||
self.assertEqual(parse_duration('P0Y0M0DT0H4M20.880S'), 260.88)
|
||||
self.assertEqual(parse_duration('01:02:03:050'), 3723.05)
|
||||
self.assertEqual(parse_duration('103:050'), 103.05)
|
||||
self.assertEqual(parse_duration('1HR 3MIN'), 3780)
|
||||
self.assertEqual(parse_duration('2hrs 3mins'), 7380)
|
||||
|
||||
def test_fix_xml_ampersands(self):
|
||||
self.assertEqual(
|
||||
|
|
@ -895,6 +902,30 @@ class TestUtil(unittest.TestCase):
|
|||
'vcodec': 'av01.0.05M.08',
|
||||
'acodec': 'none',
|
||||
})
|
||||
self.assertEqual(parse_codecs('vp9.2'), {
|
||||
'vcodec': 'vp9.2',
|
||||
'acodec': 'none',
|
||||
'dynamic_range': 'HDR10',
|
||||
})
|
||||
self.assertEqual(parse_codecs('vp09.02.50.10.01.09.18.09.00'), {
|
||||
'vcodec': 'vp09.02.50.10.01.09.18.09.00',
|
||||
'acodec': 'none',
|
||||
'dynamic_range': 'HDR10',
|
||||
})
|
||||
self.assertEqual(parse_codecs('av01.0.12M.10.0.110.09.16.09.0'), {
|
||||
'vcodec': 'av01.0.12M.10.0.110.09.16.09.0',
|
||||
'acodec': 'none',
|
||||
'dynamic_range': 'HDR10',
|
||||
})
|
||||
self.assertEqual(parse_codecs('dvhe'), {
|
||||
'vcodec': 'dvhe',
|
||||
'acodec': 'none',
|
||||
'dynamic_range': 'DV',
|
||||
})
|
||||
self.assertEqual(parse_codecs('fLaC'), {
|
||||
'vcodec': 'none',
|
||||
'acodec': 'flac',
|
||||
})
|
||||
self.assertEqual(parse_codecs('theora, vorbis'), {
|
||||
'vcodec': 'theora',
|
||||
'acodec': 'vorbis',
|
||||
|
|
@ -1723,6 +1754,21 @@ Line 1
|
|||
'a', 'b', 'c', 'd',
|
||||
from_dict={'a': 'c', 'c': [], 'b': 'd', 'd': None}), 'c-d')
|
||||
|
||||
def test_partial_application(self):
|
||||
test_fn = partial_application(lambda x, kwarg=None: '{0}, kwarg={1!r}'.format(x, kwarg))
|
||||
self.assertTrue(
|
||||
callable(test_fn(kwarg=10)),
|
||||
'missing positional parameter should apply partially')
|
||||
self.assertEqual(
|
||||
test_fn(10, kwarg=42), '10, kwarg=42',
|
||||
'positionally passed argument should call function')
|
||||
self.assertEqual(
|
||||
test_fn(x=10), '10, kwarg=None',
|
||||
'keyword passed positional should call function')
|
||||
self.assertEqual(
|
||||
test_fn(kwarg=42)(10), '10, kwarg=42',
|
||||
'call after partial application should call the function')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ class YoutubeDL(object):
|
|||
|
||||
_NUMERIC_FIELDS = set((
|
||||
'width', 'height', 'tbr', 'abr', 'asr', 'vbr', 'fps', 'filesize', 'filesize_approx',
|
||||
'timestamp', 'upload_year', 'upload_month', 'upload_day',
|
||||
'timestamp', 'upload_year', 'upload_month', 'upload_day', 'available_at',
|
||||
'duration', 'view_count', 'like_count', 'dislike_count', 'repost_count',
|
||||
'average_rating', 'comment_count', 'age_limit',
|
||||
'start_time', 'end_time',
|
||||
|
|
@ -2404,60 +2404,52 @@ class YoutubeDL(object):
|
|||
return res
|
||||
|
||||
def _format_note(self, fdict):
|
||||
res = ''
|
||||
if fdict.get('ext') in ['f4f', 'f4m']:
|
||||
res += '(unsupported) '
|
||||
if fdict.get('language'):
|
||||
if res:
|
||||
res += ' '
|
||||
res += '[%s] ' % fdict['language']
|
||||
if fdict.get('format_note') is not None:
|
||||
res += fdict['format_note'] + ' '
|
||||
if fdict.get('tbr') is not None:
|
||||
res += '%4dk ' % fdict['tbr']
|
||||
|
||||
def simplified_codec(f, field):
|
||||
assert field in ('acodec', 'vcodec')
|
||||
codec = f.get(field)
|
||||
return (
|
||||
'unknown' if not codec
|
||||
else '.'.join(codec.split('.')[:4]) if codec != 'none'
|
||||
else 'images' if field == 'vcodec' and f.get('acodec') == 'none'
|
||||
else None if field == 'acodec' and f.get('vcodec') == 'none'
|
||||
else 'audio only' if field == 'vcodec'
|
||||
else 'video only')
|
||||
|
||||
res = join_nonempty(
|
||||
fdict.get('ext') in ('f4f', 'f4m') and '(unsupported)',
|
||||
fdict.get('language') and ('[%s]' % (fdict['language'],)),
|
||||
fdict.get('format_note') is not None and fdict['format_note'],
|
||||
fdict.get('tbr') is not None and ('%4dk' % fdict['tbr']),
|
||||
delim=' ')
|
||||
res = [res] if res else []
|
||||
if fdict.get('container') is not None:
|
||||
if res:
|
||||
res += ', '
|
||||
res += '%s container' % fdict['container']
|
||||
if (fdict.get('vcodec') is not None
|
||||
and fdict.get('vcodec') != 'none'):
|
||||
if res:
|
||||
res += ', '
|
||||
res += fdict['vcodec']
|
||||
if fdict.get('vbr') is not None:
|
||||
res += '@'
|
||||
res.append('%s container' % (fdict['container'],))
|
||||
if fdict.get('vcodec') not in (None, 'none'):
|
||||
codec = simplified_codec(fdict, 'vcodec')
|
||||
if codec and fdict.get('vbr') is not None:
|
||||
codec += '@'
|
||||
elif fdict.get('vbr') is not None and fdict.get('abr') is not None:
|
||||
res += 'video@'
|
||||
if fdict.get('vbr') is not None:
|
||||
res += '%4dk' % fdict['vbr']
|
||||
codec = 'video@'
|
||||
else:
|
||||
codec = None
|
||||
codec = join_nonempty(codec, fdict.get('vbr') is not None and ('%4dk' % fdict['vbr']))
|
||||
if codec:
|
||||
res.append(codec)
|
||||
if fdict.get('fps') is not None:
|
||||
if res:
|
||||
res += ', '
|
||||
res += '%sfps' % fdict['fps']
|
||||
if fdict.get('acodec') is not None:
|
||||
if res:
|
||||
res += ', '
|
||||
if fdict['acodec'] == 'none':
|
||||
res += 'video only'
|
||||
else:
|
||||
res += '%-5s' % fdict['acodec']
|
||||
elif fdict.get('abr') is not None:
|
||||
if res:
|
||||
res += ', '
|
||||
res += 'audio'
|
||||
if fdict.get('abr') is not None:
|
||||
res += '@%3dk' % fdict['abr']
|
||||
if fdict.get('asr') is not None:
|
||||
res += ' (%5dHz)' % fdict['asr']
|
||||
res.append('%sfps' % (fdict['fps'],))
|
||||
codec = (
|
||||
simplified_codec(fdict, 'acodec') if fdict.get('acodec') is not None
|
||||
else 'audio' if fdict.get('abr') is not None else None)
|
||||
if codec:
|
||||
res.append(join_nonempty(
|
||||
'%-4s' % (codec + (('@%3dk' % fdict['abr']) if fdict.get('abr') else ''),),
|
||||
fdict.get('asr') and '(%5dHz)' % fdict['asr'], delim=' '))
|
||||
if fdict.get('filesize') is not None:
|
||||
if res:
|
||||
res += ', '
|
||||
res += format_bytes(fdict['filesize'])
|
||||
res.append(format_bytes(fdict['filesize']))
|
||||
elif fdict.get('filesize_approx') is not None:
|
||||
if res:
|
||||
res += ', '
|
||||
res += '~' + format_bytes(fdict['filesize_approx'])
|
||||
return res
|
||||
res.append('~' + format_bytes(fdict['filesize_approx']))
|
||||
return ', '.join(res)
|
||||
|
||||
def list_formats(self, info_dict):
|
||||
formats = info_dict.get('formats', [info_dict])
|
||||
|
|
|
|||
|
|
@ -409,6 +409,8 @@ def _real_main(argv=None):
|
|||
'include_ads': opts.include_ads,
|
||||
'default_search': opts.default_search,
|
||||
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
|
||||
'youtube_player_js_version': opts.youtube_player_js_version,
|
||||
'youtube_player_js_variant': opts.youtube_player_js_variant,
|
||||
'encoding': opts.encoding,
|
||||
'extract_flat': opts.extract_flat,
|
||||
'mark_watched': opts.mark_watched,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ except AttributeError:
|
|||
try:
|
||||
import collections.abc as compat_collections_abc
|
||||
except ImportError:
|
||||
import collections as compat_collections_abc
|
||||
compat_collections_abc = collections
|
||||
|
||||
|
||||
# compat_urllib_request
|
||||
|
|
@ -3452,6 +3452,8 @@ except ImportError:
|
|||
except ImportError:
|
||||
compat_map = map
|
||||
|
||||
|
||||
# compat_filter, compat_filter_fns
|
||||
try:
|
||||
from future_builtins import filter as compat_filter
|
||||
except ImportError:
|
||||
|
|
@ -3459,6 +3461,9 @@ except ImportError:
|
|||
from itertools import ifilter as compat_filter
|
||||
except ImportError:
|
||||
compat_filter = filter
|
||||
# "Is this function one or maybe the other filter()?"
|
||||
compat_filter_fns = tuple(set((filter, compat_filter)))
|
||||
|
||||
|
||||
# compat_zip
|
||||
try:
|
||||
|
|
@ -3478,6 +3483,40 @@ except ImportError:
|
|||
from itertools import izip_longest as compat_itertools_zip_longest
|
||||
|
||||
|
||||
# compat_abc_ABC
|
||||
try:
|
||||
from abc import ABC as compat_abc_ABC
|
||||
except ImportError:
|
||||
# Py < 3.4
|
||||
from abc import ABCMeta as _ABCMeta
|
||||
compat_abc_ABC = _ABCMeta(str('ABC'), (object,), {})
|
||||
|
||||
|
||||
# dict mixin used here
|
||||
# like UserDict.DictMixin, without methods created by MutableMapping
|
||||
class _DictMixin(compat_abc_ABC):
|
||||
def has_key(self, key):
|
||||
return key in self
|
||||
|
||||
# get(), clear(), setdefault() in MM
|
||||
|
||||
def iterkeys(self):
|
||||
return (k for k in self)
|
||||
|
||||
def itervalues(self):
|
||||
return (self[k] for k in self)
|
||||
|
||||
def iteritems(self):
|
||||
return ((k, self[k]) for k in self)
|
||||
|
||||
# pop(), popitem() in MM
|
||||
|
||||
def copy(self):
|
||||
return type(self)(self)
|
||||
|
||||
# update() in MM
|
||||
|
||||
|
||||
# compat_collections_chain_map
|
||||
# collections.ChainMap: new class
|
||||
try:
|
||||
|
|
@ -3632,6 +3671,129 @@ except ImportError:
|
|||
compat_zstandard = None
|
||||
|
||||
|
||||
# compat_thread
|
||||
try:
|
||||
import _thread as compat_thread
|
||||
except ImportError:
|
||||
try:
|
||||
import thread as compat_thread
|
||||
except ImportError:
|
||||
import dummy_thread as compat_thread
|
||||
|
||||
|
||||
# compat_dict
|
||||
# compat_builtins_dict
|
||||
# compat_dict_items
|
||||
if sys.version_info >= (3, 6):
|
||||
compat_dict = compat_builtins_dict = dict
|
||||
compat_dict_items = dict.items
|
||||
else:
|
||||
_get_ident = compat_thread.get_ident
|
||||
|
||||
class compat_dict(compat_collections_abc.MutableMapping, _DictMixin, dict):
|
||||
"""`dict` that preserves insertion order with interface like Py3.7+"""
|
||||
|
||||
_order = [] # default that should never be used
|
||||
|
||||
def __init__(self, *mappings_or_iterables, **kwargs):
|
||||
# order an unordered dict using a list of keys: actual Py 2.7+
|
||||
# OrderedDict uses a doubly linked list for better performance
|
||||
self._order = []
|
||||
for arg in mappings_or_iterables:
|
||||
self.__update(arg)
|
||||
if kwargs:
|
||||
self.__update(kwargs)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
try:
|
||||
if key not in self._order:
|
||||
self._order.append(key)
|
||||
dict.__setitem__(self, key, value)
|
||||
except Exception:
|
||||
if key in self._order[-1:] and key not in self:
|
||||
del self._order[-1]
|
||||
raise
|
||||
|
||||
def __len__(self):
|
||||
return dict.__len__(self)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
try:
|
||||
# expected case, O(len(self)), but who dels anyway?
|
||||
self._order.remove(key)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def __iter__(self):
|
||||
for from_ in self._order:
|
||||
if from_ in self:
|
||||
yield from_
|
||||
|
||||
def __del__(self):
|
||||
for attr in ('_order',):
|
||||
try:
|
||||
delattr(self, attr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __repr__(self, _repr_running={}):
|
||||
# skip recursive items ...
|
||||
call_key = id(self), _get_ident()
|
||||
if _repr_running.get(call_key):
|
||||
return '...'
|
||||
_repr_running[call_key] = True
|
||||
try:
|
||||
return '%s({%s})' % (
|
||||
type(self).__name__,
|
||||
','.join('%r: %r' % k_v for k_v in self.items()))
|
||||
finally:
|
||||
del _repr_running[call_key]
|
||||
|
||||
# merge/update (PEP 584)
|
||||
|
||||
def __or__(self, other):
|
||||
if not isinstance(other, compat_collections_abc.Mapping):
|
||||
return NotImplemented
|
||||
new = type(self)(self)
|
||||
new.update(other)
|
||||
return new
|
||||
|
||||
def __ror__(self, other):
|
||||
if not isinstance(other, compat_collections_abc.Mapping):
|
||||
return NotImplemented
|
||||
new = type(other)(other)
|
||||
new.update(self)
|
||||
return new
|
||||
|
||||
def __ior__(self, other):
|
||||
self.update(other)
|
||||
return self
|
||||
|
||||
# optimisations
|
||||
|
||||
def __reversed__(self):
|
||||
for from_ in reversed(self._order):
|
||||
if from_ in self:
|
||||
yield from_
|
||||
|
||||
def __contains__(self, item):
|
||||
return dict.__contains__(self, item)
|
||||
|
||||
# allow overriding update without breaking __init__
|
||||
def __update(self, *args, **kwargs):
|
||||
super(compat_dict, self).update(*args, **kwargs)
|
||||
|
||||
compat_builtins_dict = dict
|
||||
# Using the object's method, not dict's:
|
||||
# an ordered dict's items can be returned unstably by unordered
|
||||
# dict.items as if the method was not ((k, self[k]) for k in self)
|
||||
compat_dict_items = lambda d: d.items()
|
||||
|
||||
|
||||
legacy = [
|
||||
'compat_HTMLParseError',
|
||||
'compat_HTMLParser',
|
||||
|
|
@ -3662,9 +3824,11 @@ legacy = [
|
|||
|
||||
__all__ = [
|
||||
'compat_Struct',
|
||||
'compat_abc_ABC',
|
||||
'compat_base64_b64decode',
|
||||
'compat_basestring',
|
||||
'compat_brotli',
|
||||
'compat_builtins_dict',
|
||||
'compat_casefold',
|
||||
'compat_chr',
|
||||
'compat_collections_abc',
|
||||
|
|
@ -3672,9 +3836,12 @@ __all__ = [
|
|||
'compat_contextlib_suppress',
|
||||
'compat_ctypes_WINFUNCTYPE',
|
||||
'compat_datetime_timedelta_total_seconds',
|
||||
'compat_dict',
|
||||
'compat_dict_items',
|
||||
'compat_etree_fromstring',
|
||||
'compat_etree_iterfind',
|
||||
'compat_filter',
|
||||
'compat_filter_fns',
|
||||
'compat_get_terminal_size',
|
||||
'compat_getenv',
|
||||
'compat_getpass_getpass',
|
||||
|
|
@ -3716,6 +3883,7 @@ __all__ = [
|
|||
'compat_struct_unpack',
|
||||
'compat_subprocess_get_DEVNULL',
|
||||
'compat_subprocess_Popen',
|
||||
'compat_thread',
|
||||
'compat_tokenize_tokenize',
|
||||
'compat_urllib_error',
|
||||
'compat_urllib_parse',
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from ..utils import (
|
|||
decodeArgument,
|
||||
encodeFilename,
|
||||
error_to_compat_str,
|
||||
float_or_none,
|
||||
format_bytes,
|
||||
shell_quote,
|
||||
timeconvert,
|
||||
|
|
@ -367,14 +368,27 @@ class FileDownloader(object):
|
|||
})
|
||||
return True
|
||||
|
||||
min_sleep_interval = self.params.get('sleep_interval')
|
||||
if min_sleep_interval:
|
||||
max_sleep_interval = self.params.get('max_sleep_interval', min_sleep_interval)
|
||||
sleep_interval = random.uniform(min_sleep_interval, max_sleep_interval)
|
||||
min_sleep_interval, max_sleep_interval = (
|
||||
float_or_none(self.params.get(interval), default=0)
|
||||
for interval in ('sleep_interval', 'max_sleep_interval'))
|
||||
|
||||
sleep_note = ''
|
||||
available_at = info_dict.get('available_at')
|
||||
if available_at:
|
||||
forced_sleep_interval = available_at - int(time.time())
|
||||
if forced_sleep_interval > min_sleep_interval:
|
||||
sleep_note = 'as required by the site'
|
||||
min_sleep_interval = forced_sleep_interval
|
||||
if forced_sleep_interval > max_sleep_interval:
|
||||
max_sleep_interval = forced_sleep_interval
|
||||
|
||||
sleep_interval = random.uniform(
|
||||
min_sleep_interval, max_sleep_interval or min_sleep_interval)
|
||||
|
||||
if sleep_interval > 0:
|
||||
self.to_screen(
|
||||
'[download] Sleeping %s seconds...' % (
|
||||
int(sleep_interval) if sleep_interval.is_integer()
|
||||
else '%.2f' % sleep_interval))
|
||||
'[download] Sleeping %.2f seconds %s...' % (
|
||||
sleep_interval, sleep_note))
|
||||
time.sleep(sleep_interval)
|
||||
|
||||
return self.real_download(filename, info_dict)
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ class InfoExtractor(object):
|
|||
width : height ratio as float.
|
||||
* no_resume The server does not support resuming the
|
||||
(HTTP or RTMP) download. Boolean.
|
||||
* available_at Unix timestamp of when a format will be available to download
|
||||
* downloader_options A dictionary of downloader options as
|
||||
described in FileDownloader
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -404,6 +404,10 @@ def parseOpts(overrideArguments=None):
|
|||
'-F', '--list-formats',
|
||||
action='store_true', dest='listformats',
|
||||
help='List all available formats of requested videos')
|
||||
video_format.add_option(
|
||||
'--no-list-formats',
|
||||
action='store_false', dest='listformats',
|
||||
help='Do not list available formats of requested videos (default)')
|
||||
video_format.add_option(
|
||||
'--youtube-include-dash-manifest',
|
||||
action='store_true', dest='youtube_include_dash_manifest', default=True,
|
||||
|
|
@ -412,6 +416,17 @@ def parseOpts(overrideArguments=None):
|
|||
'--youtube-skip-dash-manifest',
|
||||
action='store_false', dest='youtube_include_dash_manifest',
|
||||
help='Do not download the DASH manifests and related data on YouTube videos')
|
||||
video_format.add_option(
|
||||
'--youtube-player-js-variant',
|
||||
action='store', dest='youtube_player_js_variant',
|
||||
help='For YouTube, the player javascript variant to use for n/sig deciphering; `actual` to follow the site; default `%default`.',
|
||||
choices=('actual', 'main', 'tcc', 'tce', 'es5', 'es6', 'tv', 'tv_es6', 'phone', 'tablet'),
|
||||
default='actual', metavar='VARIANT')
|
||||
video_format.add_option(
|
||||
'--youtube-player-js-version',
|
||||
action='store', dest='youtube_player_js_version',
|
||||
help='For YouTube, the player javascript version to use for n/sig deciphering, specified as `signature_timestamp@hash`, or `actual` to follow the site; default `%default`',
|
||||
default='actual', metavar='STS@HASH')
|
||||
video_format.add_option(
|
||||
'--merge-output-format',
|
||||
action='store', dest='merge_output_format', metavar='FORMAT', default=None,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
from .utils import (
|
||||
dict_get,
|
||||
get_first,
|
||||
require,
|
||||
subs_list_to_dict,
|
||||
T,
|
||||
traverse_obj,
|
||||
unpack,
|
||||
value,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ from .compat import (
|
|||
compat_etree_fromstring,
|
||||
compat_etree_iterfind,
|
||||
compat_expanduser,
|
||||
compat_filter as filter,
|
||||
compat_filter_fns,
|
||||
compat_html_entities,
|
||||
compat_html_entities_html5,
|
||||
compat_http_client,
|
||||
|
|
@ -1859,6 +1861,39 @@ def write_json_file(obj, fn):
|
|||
raise
|
||||
|
||||
|
||||
class partial_application(object):
|
||||
"""Allow a function to use pre-set argument values"""
|
||||
|
||||
# see _try_bind_args()
|
||||
try:
|
||||
inspect.signature
|
||||
|
||||
@staticmethod
|
||||
def required_args(fn):
|
||||
return [
|
||||
param.name for param in inspect.signature(fn).parameters.values()
|
||||
if (param.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
||||
and param.default is inspect.Parameter.empty)]
|
||||
|
||||
except AttributeError:
|
||||
|
||||
# Py < 3.3
|
||||
@staticmethod
|
||||
def required_args(fn):
|
||||
fn_args = inspect.getargspec(fn)
|
||||
n_defaults = len(fn_args.defaults or [])
|
||||
return (fn_args.args or [])[:-n_defaults if n_defaults > 0 else None]
|
||||
|
||||
def __new__(cls, func):
|
||||
@functools.wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
if set(cls.required_args(func)[len(args):]).difference(kwargs):
|
||||
return functools.partial(func, *args, **kwargs)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
if sys.version_info >= (2, 7):
|
||||
def find_xpath_attr(node, xpath, key, val=None):
|
||||
""" Find the xpath xpath[@key=val] """
|
||||
|
|
@ -3152,6 +3187,7 @@ def extract_timezone(date_str):
|
|||
return timezone, date_str
|
||||
|
||||
|
||||
@partial_application
|
||||
def parse_iso8601(date_str, delimiter='T', timezone=None):
|
||||
""" Return a UNIX timestamp from the given date """
|
||||
|
||||
|
|
@ -3229,6 +3265,7 @@ def unified_timestamp(date_str, day_first=True):
|
|||
return calendar.timegm(timetuple) + pm_delta * 3600 - compat_datetime_timedelta_total_seconds(timezone)
|
||||
|
||||
|
||||
@partial_application
|
||||
def determine_ext(url, default_ext='unknown_video'):
|
||||
if url is None or '.' not in url:
|
||||
return default_ext
|
||||
|
|
@ -3807,6 +3844,7 @@ def base_url(url):
|
|||
return re.match(r'https?://[^?#&]+/', url).group()
|
||||
|
||||
|
||||
@partial_application
|
||||
def urljoin(base, path):
|
||||
path = _decode_compat_str(path, encoding='utf-8', or_none=True)
|
||||
if not path:
|
||||
|
|
@ -3831,6 +3869,7 @@ class PUTRequest(compat_urllib_request.Request):
|
|||
return 'PUT'
|
||||
|
||||
|
||||
@partial_application
|
||||
def int_or_none(v, scale=1, default=None, get_attr=None, invscale=1, base=None):
|
||||
if get_attr:
|
||||
if v is not None:
|
||||
|
|
@ -3857,6 +3896,7 @@ def str_to_int(int_str):
|
|||
return int_or_none(int_str)
|
||||
|
||||
|
||||
@partial_application
|
||||
def float_or_none(v, scale=1, invscale=1, default=None):
|
||||
if v is None:
|
||||
return default
|
||||
|
|
@ -3891,38 +3931,46 @@ def parse_duration(s):
|
|||
return None
|
||||
|
||||
s = s.strip()
|
||||
if not s:
|
||||
return None
|
||||
|
||||
days, hours, mins, secs, ms = [None] * 5
|
||||
m = re.match(r'(?:(?:(?:(?P<days>[0-9]+):)?(?P<hours>[0-9]+):)?(?P<mins>[0-9]+):)?(?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?Z?$', s)
|
||||
m = re.match(r'''(?x)
|
||||
(?P<before_secs>
|
||||
(?:(?:(?P<days>[0-9]+):)?(?P<hours>[0-9]+):)?
|
||||
(?P<mins>[0-9]+):)?
|
||||
(?P<secs>(?(before_secs)[0-9]{1,2}|[0-9]+))
|
||||
(?:[.:](?P<ms>[0-9]+))?Z?$
|
||||
''', s)
|
||||
if m:
|
||||
days, hours, mins, secs, ms = m.groups()
|
||||
days, hours, mins, secs, ms = m.group('days', 'hours', 'mins', 'secs', 'ms')
|
||||
else:
|
||||
m = re.match(
|
||||
r'''(?ix)(?:P?
|
||||
(?:
|
||||
[0-9]+\s*y(?:ears?)?\s*
|
||||
[0-9]+\s*y(?:ears?)?,?\s*
|
||||
)?
|
||||
(?:
|
||||
[0-9]+\s*m(?:onths?)?\s*
|
||||
[0-9]+\s*m(?:onths?)?,?\s*
|
||||
)?
|
||||
(?:
|
||||
[0-9]+\s*w(?:eeks?)?\s*
|
||||
[0-9]+\s*w(?:eeks?)?,?\s*
|
||||
)?
|
||||
(?:
|
||||
(?P<days>[0-9]+)\s*d(?:ays?)?\s*
|
||||
(?P<days>[0-9]+)\s*d(?:ays?)?,?\s*
|
||||
)?
|
||||
T)?
|
||||
(?:
|
||||
(?P<hours>[0-9]+)\s*h(?:ours?)?\s*
|
||||
(?P<hours>[0-9]+)\s*h(?:(?:ou)?rs?)?,?\s*
|
||||
)?
|
||||
(?:
|
||||
(?P<mins>[0-9]+)\s*m(?:in(?:ute)?s?)?\s*
|
||||
(?P<mins>[0-9]+)\s*m(?:in(?:ute)?s?)?,?\s*
|
||||
)?
|
||||
(?:
|
||||
(?P<secs>[0-9]+)(?P<ms>\.[0-9]+)?\s*s(?:ec(?:ond)?s?)?\s*
|
||||
(?P<secs>[0-9]+)(?:\.(?P<ms>[0-9]+))?\s*s(?:ec(?:ond)?s?)?\s*
|
||||
)?Z?$''', s)
|
||||
if m:
|
||||
days, hours, mins, secs, ms = m.groups()
|
||||
days, hours, mins, secs, ms = m.group('days', 'hours', 'mins', 'secs', 'ms')
|
||||
else:
|
||||
m = re.match(r'(?i)(?:(?P<hours>[0-9.]+)\s*(?:hours?)|(?P<mins>[0-9.]+)\s*(?:mins?\.?|minutes?)\s*)Z?$', s)
|
||||
if m:
|
||||
|
|
@ -3930,17 +3978,13 @@ def parse_duration(s):
|
|||
else:
|
||||
return None
|
||||
|
||||
duration = 0
|
||||
if secs:
|
||||
duration += float(secs)
|
||||
if mins:
|
||||
duration += float(mins) * 60
|
||||
if hours:
|
||||
duration += float(hours) * 60 * 60
|
||||
if days:
|
||||
duration += float(days) * 24 * 60 * 60
|
||||
if ms:
|
||||
duration += float(ms)
|
||||
duration = (
|
||||
((((float(days) * 24) if days else 0)
|
||||
+ (float(hours) if hours else 0)) * 60
|
||||
+ (float(mins) if mins else 0)) * 60
|
||||
+ (float(secs) if secs else 0)
|
||||
+ (float(ms) / 10 ** len(ms) if ms else 0))
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
|
|
@ -4251,6 +4295,7 @@ def urlencode_postdata(*args, **kargs):
|
|||
return compat_urllib_parse_urlencode(*args, **kargs).encode('ascii')
|
||||
|
||||
|
||||
@partial_application
|
||||
def update_url(url, **kwargs):
|
||||
"""Replace URL components specified by kwargs
|
||||
url: compat_str or parsed URL tuple
|
||||
|
|
@ -4272,6 +4317,7 @@ def update_url(url, **kwargs):
|
|||
return compat_urllib_parse.urlunparse(url._replace(**kwargs))
|
||||
|
||||
|
||||
@partial_application
|
||||
def update_url_query(url, query):
|
||||
return update_url(url, query_update=query)
|
||||
|
||||
|
|
@ -4698,30 +4744,45 @@ def parse_codecs(codecs_str):
|
|||
if not codecs_str:
|
||||
return {}
|
||||
split_codecs = list(filter(None, map(
|
||||
lambda str: str.strip(), codecs_str.strip().strip(',').split(','))))
|
||||
vcodec, acodec = None, None
|
||||
lambda s: s.strip(), codecs_str.strip().split(','))))
|
||||
vcodec, acodec, hdr = None, None, None
|
||||
for full_codec in split_codecs:
|
||||
codec = full_codec.split('.')[0]
|
||||
if codec in ('avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2', 'h263', 'h264', 'mp4v', 'hvc1', 'av01', 'theora'):
|
||||
if not vcodec:
|
||||
vcodec = full_codec
|
||||
elif codec in ('mp4a', 'opus', 'vorbis', 'mp3', 'aac', 'ac-3', 'ec-3', 'eac3', 'dtsc', 'dtse', 'dtsh', 'dtsl'):
|
||||
codec, rest = full_codec.partition('.')[::2]
|
||||
codec = codec.lower()
|
||||
full_codec = '.'.join((codec, rest)) if rest else codec
|
||||
codec = re.sub(r'0+(?=\d)', '', codec)
|
||||
if codec in ('avc1', 'avc2', 'avc3', 'avc4', 'vp9', 'vp8', 'hev1', 'hev2',
|
||||
'h263', 'h264', 'mp4v', 'hvc1', 'av1', 'theora', 'dvh1', 'dvhe'):
|
||||
if vcodec:
|
||||
continue
|
||||
vcodec = full_codec
|
||||
if codec in ('dvh1', 'dvhe'):
|
||||
hdr = 'DV'
|
||||
elif codec in ('av1', 'vp9'):
|
||||
n, m = {
|
||||
'av1': (2, '10'),
|
||||
'vp9': (0, '2'),
|
||||
}[codec]
|
||||
if (rest.split('.', n + 1)[n:] or [''])[0].lstrip('0') == m:
|
||||
hdr = 'HDR10'
|
||||
elif codec in ('flac', 'mp4a', 'opus', 'vorbis', 'mp3', 'aac', 'ac-4',
|
||||
'ac-3', 'ec-3', 'eac3', 'dtsc', 'dtse', 'dtsh', 'dtsl'):
|
||||
if not acodec:
|
||||
acodec = full_codec
|
||||
else:
|
||||
write_string('WARNING: Unknown codec %s\n' % full_codec, sys.stderr)
|
||||
if not vcodec and not acodec:
|
||||
if len(split_codecs) == 2:
|
||||
return {
|
||||
'vcodec': split_codecs[0],
|
||||
'acodec': split_codecs[1],
|
||||
}
|
||||
else:
|
||||
return {
|
||||
write_string('WARNING: Unknown codec %s\n' % (full_codec,), sys.stderr)
|
||||
|
||||
return (
|
||||
filter_dict({
|
||||
'vcodec': vcodec or 'none',
|
||||
'acodec': acodec or 'none',
|
||||
}
|
||||
return {}
|
||||
'dynamic_range': hdr,
|
||||
}) if vcodec or acodec
|
||||
else {
|
||||
'vcodec': split_codecs[0],
|
||||
'acodec': split_codecs[1],
|
||||
} if len(split_codecs) == 2
|
||||
else {})
|
||||
|
||||
|
||||
def urlhandle_detect_ext(url_handle):
|
||||
|
|
@ -6283,6 +6344,7 @@ def traverse_obj(obj, *paths, **kwargs):
|
|||
Read as: `{key: traverse_obj(obj, path) for key, path in dct.items()}`.
|
||||
- `any`-builtin: Take the first matching object and return it, resetting branching.
|
||||
- `all`-builtin: Take all matching objects and return them as a list, resetting branching.
|
||||
- `filter`-builtin: Return the value if it is truthy, `None` otherwise.
|
||||
|
||||
`tuple`, `list`, and `dict` all support nested paths and branches.
|
||||
|
||||
|
|
@ -6324,6 +6386,11 @@ def traverse_obj(obj, *paths, **kwargs):
|
|||
# instant compat
|
||||
str = compat_str
|
||||
|
||||
from .compat import (
|
||||
compat_builtins_dict as dict_, # the basic dict type
|
||||
compat_dict as dict, # dict preserving imsertion order
|
||||
)
|
||||
|
||||
casefold = lambda k: compat_casefold(k) if isinstance(k, str) else k
|
||||
|
||||
if isinstance(expected_type, type):
|
||||
|
|
@ -6406,7 +6473,7 @@ def traverse_obj(obj, *paths, **kwargs):
|
|||
if not branching: # string traversal
|
||||
result = ''.join(result)
|
||||
|
||||
elif isinstance(key, dict):
|
||||
elif isinstance(key, dict_):
|
||||
iter_obj = ((k, _traverse_obj(obj, v, False, is_last)) for k, v in key.items())
|
||||
result = dict((k, v if v is not None else default) for k, v in iter_obj
|
||||
if v is not None or default is not NO_DEFAULT) or None
|
||||
|
|
@ -6484,7 +6551,7 @@ def traverse_obj(obj, *paths, **kwargs):
|
|||
has_branched = False
|
||||
|
||||
key = None
|
||||
for last, key in lazy_last(variadic(path, (str, bytes, dict, set))):
|
||||
for last, key in lazy_last(variadic(path, (str, bytes, dict_, set))):
|
||||
if not casesense and isinstance(key, str):
|
||||
key = compat_casefold(key)
|
||||
|
||||
|
|
@ -6497,6 +6564,11 @@ def traverse_obj(obj, *paths, **kwargs):
|
|||
objs = (list(filtered_objs),)
|
||||
continue
|
||||
|
||||
# filter might be from __builtin__, future_builtins, or itertools.ifilter
|
||||
if key in compat_filter_fns:
|
||||
objs = filter(None, objs)
|
||||
continue
|
||||
|
||||
if __debug__ and callable(key):
|
||||
# Verify function signature
|
||||
_try_bind_args(key, None, None)
|
||||
|
|
@ -6509,10 +6581,10 @@ def traverse_obj(obj, *paths, **kwargs):
|
|||
|
||||
objs = from_iterable(new_objs)
|
||||
|
||||
if test_type and not isinstance(key, (dict, list, tuple)):
|
||||
if test_type and not isinstance(key, (dict_, list, tuple)):
|
||||
objs = map(type_test, objs)
|
||||
|
||||
return objs, has_branched, isinstance(key, dict)
|
||||
return objs, has_branched, isinstance(key, dict_)
|
||||
|
||||
def _traverse_obj(obj, path, allow_empty, test_type):
|
||||
results, has_branched, is_dict = apply_path(obj, path, test_type)
|
||||
|
|
@ -6535,6 +6607,76 @@ def traverse_obj(obj, *paths, **kwargs):
|
|||
return None if default is NO_DEFAULT else default
|
||||
|
||||
|
||||
def value(value):
|
||||
return lambda _: value
|
||||
|
||||
|
||||
class require(ExtractorError):
|
||||
def __init__(self, name, expected=False):
|
||||
super(require, self).__init__(
|
||||
'Unable to extract {0}'.format(name), expected=expected)
|
||||
|
||||
def __call__(self, value):
|
||||
if value is None:
|
||||
raise self
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@partial_application
|
||||
# typing: (subs: list[dict], /, *, lang='und', ext=None) -> dict[str, list[dict]
|
||||
def subs_list_to_dict(subs, lang='und', ext=None):
|
||||
"""
|
||||
Convert subtitles from a traversal into a subtitle dict.
|
||||
The path should have an `all` immediately before this function.
|
||||
|
||||
Arguments:
|
||||
`lang` The default language tag for subtitle dicts with no
|
||||
`lang` (`und`: undefined)
|
||||
`ext` The default value for `ext` in the subtitle dicts
|
||||
|
||||
In the dict you can set the following additional items:
|
||||
`id` The language tag to which the subtitle dict should be added
|
||||
`quality` The sort order for each subtitle dict
|
||||
"""
|
||||
|
||||
result = collections.defaultdict(list)
|
||||
|
||||
for sub in subs:
|
||||
tn_url = url_or_none(sub.pop('url', None))
|
||||
if tn_url:
|
||||
sub['url'] = tn_url
|
||||
elif not sub.get('data'):
|
||||
continue
|
||||
sub_lang = sub.pop('id', None)
|
||||
if not isinstance(sub_lang, compat_str):
|
||||
if not lang:
|
||||
continue
|
||||
sub_lang = lang
|
||||
sub_ext = sub.get('ext')
|
||||
if not isinstance(sub_ext, compat_str):
|
||||
if not ext:
|
||||
sub.pop('ext', None)
|
||||
else:
|
||||
sub['ext'] = ext
|
||||
result[sub_lang].append(sub)
|
||||
result = dict(result)
|
||||
|
||||
for subs in result.values():
|
||||
subs.sort(key=lambda x: x.pop('quality', 0) or 0)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def unpack(func, **kwargs):
|
||||
"""Make a function that applies `partial(func, **kwargs)` to its argument as *args"""
|
||||
@functools.wraps(func)
|
||||
def inner(items):
|
||||
return func(*items, **kwargs)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def T(*x):
|
||||
""" For use in yt-dl instead of {type, ...} or set((type, ...)) """
|
||||
return set(x)
|
||||
|
|
|
|||
Loading…
Reference in a new issue