mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-26 00:00:34 +02:00
Compare commits
6
Commits
2026.08.19
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66f49765d5 | ||
|
|
9fb5969797 | ||
|
|
88a9516584 | ||
|
|
81ecd58b13 | ||
|
|
5022b8c119 | ||
|
|
91f784d6fd |
@@ -66,8 +66,7 @@ def convert_code_blocks(readme):
|
||||
|
||||
def move_sections(readme):
|
||||
MOVE_TAG_TEMPLATE = '<!-- MANPAGE: MOVE "%s" SECTION HERE -->'
|
||||
sections = re.findall(r'(?m)^%s$' % (
|
||||
re.escape(MOVE_TAG_TEMPLATE).replace(r'\%', '%') % '(.+)'), readme)
|
||||
sections = re.findall(r'(?m)^%s$' % (re.escape(MOVE_TAG_TEMPLATE) % '(.+)'), readme)
|
||||
|
||||
for section_name in sections:
|
||||
move_tag = MOVE_TAG_TEMPLATE % section_name
|
||||
|
||||
+1
-37
@@ -3,7 +3,6 @@ import hashlib
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
import ssl
|
||||
import sys
|
||||
import types
|
||||
|
||||
@@ -318,36 +317,6 @@ def expect_info_dict(self, got_dict, expected_dict):
|
||||
'Missing keys in test definition: {}'.format(', '.join(sorted(missing_keys))))
|
||||
|
||||
|
||||
def assertRegexpMatches(self, text, regexp, msg=None):
|
||||
if hasattr(self, 'assertRegexp'):
|
||||
return self.assertRegexp(text, regexp, msg)
|
||||
else:
|
||||
m = re.match(regexp, text)
|
||||
if not m:
|
||||
note = f'Regexp didn\'t match: {regexp!r} not found'
|
||||
if len(text) < 1000:
|
||||
note += f' in {text!r}'
|
||||
if msg is None:
|
||||
msg = note
|
||||
else:
|
||||
msg = note + ', ' + msg
|
||||
self.assertTrue(m, msg)
|
||||
|
||||
|
||||
def assertGreaterEqual(self, got, expected, msg=None):
|
||||
if not (got >= expected):
|
||||
if msg is None:
|
||||
msg = f'{got!r} not greater than or equal to {expected!r}'
|
||||
self.assertTrue(got >= expected, msg)
|
||||
|
||||
|
||||
def assertLessEqual(self, got, expected, msg=None):
|
||||
if not (got <= expected):
|
||||
if msg is None:
|
||||
msg = f'{got!r} not less than or equal to {expected!r}'
|
||||
self.assertTrue(got <= expected, msg)
|
||||
|
||||
|
||||
def assertEqual(self, got, expected, msg=None):
|
||||
if got != expected:
|
||||
if msg is None:
|
||||
@@ -366,12 +335,7 @@ def expect_warnings(ydl, warnings_re):
|
||||
|
||||
|
||||
def http_server_port(httpd):
|
||||
if os.name == 'java' and isinstance(httpd.socket, ssl.SSLSocket):
|
||||
# In Jython SSLSocket is not a subclass of socket.socket
|
||||
sock = httpd.socket.sock
|
||||
else:
|
||||
sock = httpd.socket
|
||||
return sock.getsockname()[1]
|
||||
return httpd.server_address[1]
|
||||
|
||||
|
||||
def verify_address_availability(address):
|
||||
|
||||
@@ -15,7 +15,7 @@ import contextlib
|
||||
import copy
|
||||
import json
|
||||
|
||||
from test.helper import FakeYDL, assertRegexpMatches, try_rm
|
||||
from test.helper import FakeYDL, try_rm
|
||||
from yt_dlp import YoutubeDL
|
||||
from yt_dlp.extractor.common import InfoExtractor
|
||||
from yt_dlp.postprocessor.common import PostProcessor
|
||||
@@ -860,10 +860,10 @@ class TestYoutubeDL(unittest.TestCase):
|
||||
def test_format_note(self):
|
||||
ydl = YoutubeDL()
|
||||
self.assertEqual(ydl._format_note({}), '')
|
||||
assertRegexpMatches(self, ydl._format_note({
|
||||
self.assertRegex(ydl._format_note({
|
||||
'vbr': 10,
|
||||
}), r'^\s*10k$')
|
||||
assertRegexpMatches(self, ydl._format_note({
|
||||
self.assertRegex(ydl._format_note({
|
||||
'fps': 30,
|
||||
}), r'^30fps$')
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ import hashlib
|
||||
import json
|
||||
|
||||
from test.helper import (
|
||||
assertGreaterEqual,
|
||||
assertLessEqual,
|
||||
expect_info_dict,
|
||||
expect_warnings,
|
||||
get_params,
|
||||
@@ -201,8 +199,8 @@ def generator(test_case, tname):
|
||||
num_entries = len(res_dict.get('entries', []))
|
||||
if 'playlist_mincount' in test_case:
|
||||
mincount = test_case['playlist_mincount']
|
||||
assertGreaterEqual(
|
||||
self, num_entries, mincount,
|
||||
self.assertGreaterEqual(
|
||||
num_entries, mincount,
|
||||
f'Expected at least {mincount} entries in playlist {test_url}, but got only {num_entries}')
|
||||
if 'playlist_count' in test_case:
|
||||
count = test_case['playlist_count']
|
||||
@@ -212,8 +210,8 @@ def generator(test_case, tname):
|
||||
f'Expected exactly {count} entries in playlist {test_url}, but got {got}')
|
||||
if 'playlist_maxcount' in test_case:
|
||||
maxcount = test_case['playlist_maxcount']
|
||||
assertLessEqual(
|
||||
self, num_entries, maxcount,
|
||||
self.assertLessEqual(
|
||||
num_entries, maxcount,
|
||||
f'Expected at most {maxcount} entries in playlist {test_url}, but got more')
|
||||
if 'playlist_duration_sum' in test_case:
|
||||
got_duration = sum(e['duration'] for e in res_dict['entries'])
|
||||
@@ -241,8 +239,8 @@ def generator(test_case, tname):
|
||||
if params.get('test'):
|
||||
expected_minsize = max(expected_minsize, 10000)
|
||||
got_fsize = os.path.getsize(tc_filename)
|
||||
assertGreaterEqual(
|
||||
self, got_fsize, expected_minsize,
|
||||
self.assertGreaterEqual(
|
||||
got_fsize, expected_minsize,
|
||||
f'Expected {tc_filename} to be at least {format_bytes(expected_minsize)}, '
|
||||
f'but it\'s only {format_bytes(got_fsize)} ')
|
||||
if 'md5' in tc:
|
||||
|
||||
+6
-19
@@ -338,7 +338,7 @@ class TestHTTPRequestHandler(TestRequestHandlerBase):
|
||||
https_server_thread.start()
|
||||
|
||||
with handler(verify=False) as rh:
|
||||
with pytest.raises(SSLError, match=r'(?i)ssl(?:v3|/tls).alert.handshake.failure') as exc_info:
|
||||
with pytest.raises(SSLError, match=r'(?i)(?:sslv3|tls).alert.handshake.failure') as exc_info:
|
||||
validate_and_send(rh, Request(f'https://127.0.0.1:{https_port}/headers'))
|
||||
assert not issubclass(exc_info.type, CertificateVerifyError)
|
||||
|
||||
@@ -984,28 +984,15 @@ class TestUrllibRequestHandler(TestRequestHandlerBase):
|
||||
):
|
||||
validate_and_send(rh, Request(f'https://127.0.0.1:{self.https_port}/headers'))
|
||||
|
||||
@pytest.mark.parametrize('req,match,version_check', [
|
||||
@pytest.mark.parametrize('req,match', [
|
||||
# https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1256
|
||||
# bpo-39603: Check implemented in 3.7.9+, 3.8.5+
|
||||
(
|
||||
Request('http://127.0.0.1', method='GET\n'),
|
||||
'method can\'t contain control characters',
|
||||
lambda v: v < (3, 7, 9) or (3, 8, 0) <= v < (3, 8, 5),
|
||||
),
|
||||
(Request('http://127.0.0.1', method='GET\n'), 'method can\'t contain control characters'),
|
||||
# https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1265
|
||||
# bpo-38576: Check implemented in 3.7.8+, 3.8.3+
|
||||
(
|
||||
Request('http://127.0.0. 1', method='GET'),
|
||||
'URL can\'t contain control characters',
|
||||
lambda v: v < (3, 7, 8) or (3, 8, 0) <= v < (3, 8, 3),
|
||||
),
|
||||
(Request('http://127.0.0. 1', method='GET'), 'URL can\'t contain control characters'),
|
||||
# https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1288C31-L1288C50
|
||||
(Request('http://127.0.0.1', headers={'foo\n': 'bar'}), 'Invalid header name', None),
|
||||
(Request('http://127.0.0.1', headers={'foo\n': 'bar'}), 'Invalid header name'),
|
||||
])
|
||||
def test_httplib_validation_errors(self, handler, req, match, version_check):
|
||||
if version_check and version_check(sys.version_info):
|
||||
pytest.skip(f'Python {sys.version} version does not have the required validation for this test.')
|
||||
|
||||
def test_httplib_validation_errors(self, handler, req, match):
|
||||
with handler() as rh:
|
||||
with pytest.raises(RequestError, match=match) as exc_info:
|
||||
validate_and_send(rh, req)
|
||||
|
||||
+2
-9
@@ -1347,15 +1347,8 @@ class TestUtil(unittest.TestCase):
|
||||
self.assertEqual(extract_attributes('<e _:funny-name1=1>'), {'_:funny-name1': '1'})
|
||||
self.assertEqual(extract_attributes('<e x="Fáilte 世界 \U0001f600">'), {'x': 'Fáilte 世界 \U0001f600'})
|
||||
self.assertEqual(extract_attributes('<e x="décomposé">'), {'x': 'décompose\u0301'})
|
||||
# "Narrow" Python builds don't support unicode code points outside BMP.
|
||||
try:
|
||||
chr(0x10000)
|
||||
supports_outside_bmp = True
|
||||
except ValueError:
|
||||
supports_outside_bmp = False
|
||||
if supports_outside_bmp:
|
||||
self.assertEqual(extract_attributes('<e x="Smile 😀!">'), {'x': 'Smile \U0001f600!'})
|
||||
# Malformed HTML should not break attributes extraction on older Python
|
||||
self.assertEqual(extract_attributes('<e x="Smile 😀!">'), {'x': 'Smile \U0001f600!'})
|
||||
# Malformed HTML should not break attribute extraction
|
||||
self.assertEqual(extract_attributes('<mal"formed/>'), {})
|
||||
|
||||
def test_clean_html(self):
|
||||
|
||||
@@ -186,7 +186,7 @@ class TestWebsSocketRequestHandlerConformance:
|
||||
|
||||
def test_ssl_error(self, handler):
|
||||
with handler(verify=False) as rh:
|
||||
with pytest.raises(SSLError, match=r'ssl(?:v3|/tls) alert handshake failure') as exc_info:
|
||||
with pytest.raises(SSLError, match=r'(?:sslv3|tls) alert handshake failure') as exc_info:
|
||||
ws_validate_and_send(rh, Request(self.bad_wss_host))
|
||||
assert not issubclass(exc_info.type, CertificateVerifyError)
|
||||
|
||||
|
||||
+1
-2
@@ -2398,8 +2398,7 @@ class YoutubeDL:
|
||||
selectors = []
|
||||
current_selector = None
|
||||
for type_, string_, start, _, _ in tokens:
|
||||
# ENCODING is only defined in Python 3.x
|
||||
if type_ == getattr(tokenize, 'ENCODING', None):
|
||||
if type_ == tokenize.ENCODING:
|
||||
continue
|
||||
elif type_ in [tokenize.NAME, tokenize.NUMBER]:
|
||||
current_selector = FormatSelector(SINGLE, string_, [])
|
||||
|
||||
+1
-1
@@ -293,7 +293,7 @@ def aes_decrypt_text(data, password, key_size_bytes):
|
||||
- Mode of operation is 'counter'
|
||||
|
||||
@param {str} data Base64 encoded string
|
||||
@param {str,unicode} password Password (will be encoded with utf-8)
|
||||
@param {str} password Password (will be encoded with UTF-8)
|
||||
@param {int} key_size_bytes Possible values: 16 for 128-Bit, 24 for 192-Bit or 32 for 256-Bit
|
||||
@returns {str} Decrypted data
|
||||
"""
|
||||
|
||||
@@ -8,9 +8,8 @@ passthrough_module(__name__, '._deprecated')
|
||||
del passthrough_module
|
||||
|
||||
|
||||
# HTMLParseError has been deprecated in Python 3.3 and removed in
|
||||
# Python 3.5. Introducing dummy exception for Python >3.5 for compatible
|
||||
# and uniform cross-version exception handling
|
||||
# HTMLParseError was deprecated in Python 3.3 and removed in Python 3.5.
|
||||
# Keep a replacement for API compatibility and uniform exception handling.
|
||||
class compat_HTMLParseError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ def write_piff_header(stream, params):
|
||||
sample_entry_payload += u16.pack(0x18) # depth
|
||||
sample_entry_payload += s16.pack(-1) # pre defined
|
||||
|
||||
codec_private_data = binascii.unhexlify(params['codec_private_data'].encode())
|
||||
codec_private_data = binascii.unhexlify(params['codec_private_data'])
|
||||
if fourcc in ('H264', 'AVC1'):
|
||||
sps, pps = codec_private_data.split(u32.pack(1))[1:]
|
||||
avcc_payload = u8.pack(1) # configuration version
|
||||
|
||||
@@ -1221,6 +1221,7 @@ from .nhk import (
|
||||
from .nhl import NHLIE
|
||||
from .nick import NickIE
|
||||
from .niconico import (
|
||||
NiconicoChannelIE,
|
||||
NiconicoHistoryIE,
|
||||
NiconicoIE,
|
||||
NiconicoLiveIE,
|
||||
|
||||
@@ -432,29 +432,28 @@ class InfoExtractor:
|
||||
|
||||
chapter: Name or title of the chapter the video belongs to.
|
||||
chapter_number: Number of the chapter the video belongs to, as an integer.
|
||||
chapter_id: Id of the chapter the video belongs to, as a unicode string.
|
||||
chapter_id: Id of the chapter the video belongs to.
|
||||
|
||||
The following fields should only be used when the video is an episode of some
|
||||
series, programme or podcast:
|
||||
|
||||
series: Title of the series or programme the video episode belongs to.
|
||||
series_id: Id of the series or programme the video episode belongs to, as a unicode string.
|
||||
series_id: Id of the series or programme the video episode belongs to.
|
||||
season: Title of the season the video episode belongs to.
|
||||
season_number: Number of the season the video episode belongs to, as an integer.
|
||||
season_id: Id of the season the video episode belongs to, as a unicode string.
|
||||
season_id: Id of the season the video episode belongs to.
|
||||
episode: Title of the video episode. Unlike mandatory video title field,
|
||||
this field should denote the exact title of the video episode
|
||||
without any kind of decoration.
|
||||
episode_number: Number of the video episode within a season, as an integer.
|
||||
episode_id: Id of the video episode, as a unicode string.
|
||||
episode_id: Id of the video episode.
|
||||
|
||||
The following fields should only be used when the media is a track or a part of
|
||||
a music album:
|
||||
|
||||
track: Title of the track.
|
||||
track_number: Number of the track within an album or a disc, as an integer.
|
||||
track_id: Id of the track (useful in case of custom indexing, e.g. 6.iii),
|
||||
as a unicode string.
|
||||
track_id: Id of the track (useful for custom indexing, e.g. 6.iii).
|
||||
artists: List of artists of the track.
|
||||
composers: List of composers of the piece.
|
||||
genres: List of genres of the track.
|
||||
@@ -487,7 +486,7 @@ class InfoExtractor:
|
||||
creator: Use "creators" instead.
|
||||
The creator of the video.
|
||||
|
||||
Unless mentioned otherwise, the fields should be Unicode strings.
|
||||
Unless mentioned otherwise, the fields should be strings.
|
||||
|
||||
Unless mentioned otherwise, None is equivalent to absence of information.
|
||||
|
||||
|
||||
+26
-23
@@ -116,41 +116,41 @@ class GoIE(AdobePassIE):
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
'skip': 'This video requires AdobePass MSO credentials',
|
||||
}, {
|
||||
'url': 'https://www.freeform.com/episode/bda0eaf7-761a-4838-aa44-96f794000844/playlist/PL553044961',
|
||||
'url': 'https://www.freeform.com/episode/235128d8-2609-4df4-9874-0b0b687fe9f9/playlist/PL5539647334',
|
||||
'info_dict': {
|
||||
'id': 'VDKA39007340',
|
||||
'id': 'VDKA39623200',
|
||||
'ext': 'mp4',
|
||||
'title': 'Angel\'s Landing',
|
||||
'description': 'md5:91bf084e785c968fab16734df7313446',
|
||||
'title': 'New House / New Rules',
|
||||
'description': 'md5:6f38b4b1649dc9f3a9e7acf911a70056',
|
||||
'age_limit': 14,
|
||||
'duration': 2523,
|
||||
'duration': 2733,
|
||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||
'series': 'How I Escaped My Cult',
|
||||
'season': 'Season 1',
|
||||
'season_number': 1,
|
||||
'episode': 'Episode 2',
|
||||
'episode_number': 2,
|
||||
'timestamp': 1740038400.0,
|
||||
'upload_date': '20250220',
|
||||
'series': 'Project Runway',
|
||||
'season': 'Season 21',
|
||||
'season_number': 21,
|
||||
'episode': 'Episode 1',
|
||||
'episode_number': 1,
|
||||
'timestamp': 1754020800,
|
||||
'upload_date': '20250801',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}, {
|
||||
'url': 'https://www.nationalgeographic.com/tv/episode/ca694661-1186-41ae-8089-82f64d69b16d/playlist/PL554408064',
|
||||
'url': 'https://www.nationalgeographic.com/tv/episode/df0e5bd8-f9bb-4c92-9f94-74dec4dad8a7/playlist/PL553044961',
|
||||
'info_dict': {
|
||||
'id': 'VDKA39492078',
|
||||
'id': 'VDKA35475602',
|
||||
'ext': 'mp4',
|
||||
'title': 'Heart of the Emperors',
|
||||
'description': 'md5:4fc50a2878f030bb3a7eac9124dca677',
|
||||
'title': 'The Pol Shebang',
|
||||
'description': 'md5:ccea1210c5ebcdc5c1a435f01bdb8b82',
|
||||
'age_limit': 0,
|
||||
'duration': 2775,
|
||||
'duration': 1323,
|
||||
'thumbnail': r're:https?://.+/.+\.jpg',
|
||||
'series': 'Secrets of the Penguins',
|
||||
'series': 'The Incredible Pol Farm',
|
||||
'season': 'Season 1',
|
||||
'season_number': 1,
|
||||
'episode': 'Episode 1',
|
||||
'episode_number': 1,
|
||||
'timestamp': 1745204400.0,
|
||||
'upload_date': '20250421',
|
||||
'episode': 'Episode 14',
|
||||
'episode_number': 14,
|
||||
'timestamp': 1704614400,
|
||||
'upload_date': '20240107',
|
||||
},
|
||||
'params': {'skip_download': 'm3u8'},
|
||||
}, {
|
||||
@@ -190,7 +190,10 @@ class GoIE(AdobePassIE):
|
||||
|
||||
site_info = self._SITE_INFO[site]
|
||||
brand = site_info['brand']
|
||||
video_data = self._extract_videos(brand, video_id)[0]
|
||||
videos = self._extract_videos(brand, video_id)
|
||||
if not videos:
|
||||
self.report_drm(video_id)
|
||||
video_data = videos[0]
|
||||
video_id = video_data['id']
|
||||
title = video_data['title']
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ class ITVIE(InfoExtractor):
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
||||
platform_tag_subs, featureset_subs = next(
|
||||
((platform_tag, featureset)
|
||||
for platform_tag, featuresets in reversed(list(variants.items())) for featureset in featuresets
|
||||
for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets
|
||||
if try_get(featureset, lambda x: x[2]) == 'outband-webvtt'),
|
||||
(None, None))
|
||||
|
||||
@@ -143,7 +143,7 @@ class ITVIE(InfoExtractor):
|
||||
# See: https://github.com/yt-dlp/yt-dlp/issues/986
|
||||
platform_tag_video, featureset_video = next(
|
||||
((platform_tag, featureset)
|
||||
for platform_tag, featuresets in reversed(list(variants.items())) for featureset in featuresets
|
||||
for platform_tag, featuresets in reversed(variants.items()) for featureset in featuresets
|
||||
if set(try_get(featureset, lambda x: x[:2]) or []) == {'aes', 'hls'}),
|
||||
(None, None))
|
||||
if not platform_tag_video or not featureset_video:
|
||||
|
||||
@@ -3,6 +3,7 @@ import functools
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from .common import InfoExtractor, SearchInfoExtractor
|
||||
from ..networking.exceptions import HTTPError
|
||||
@@ -14,6 +15,7 @@ from ..utils import (
|
||||
extract_attributes,
|
||||
float_or_none,
|
||||
int_or_none,
|
||||
join_nonempty,
|
||||
parse_bitrate,
|
||||
parse_iso8601,
|
||||
parse_qs,
|
||||
@@ -31,8 +33,10 @@ from ..utils import (
|
||||
)
|
||||
from ..utils.traversal import (
|
||||
find_element,
|
||||
find_elements,
|
||||
require,
|
||||
traverse_obj,
|
||||
trim_str,
|
||||
)
|
||||
|
||||
|
||||
@@ -1080,3 +1084,98 @@ class NiconicoLiveIE(NiconicoBaseIE):
|
||||
'thumbnails': thumbnails,
|
||||
'formats': formats,
|
||||
}
|
||||
|
||||
|
||||
class NiconicoChannelIE(NiconicoBaseIE):
|
||||
IE_NAME = 'niconico:channel'
|
||||
|
||||
_PAGE_SIZE = 20
|
||||
_SEARCH_PAGE_SIZE = 32
|
||||
_VALID_URL = [
|
||||
r'https?://ch\.nicovideo\.jp/(?P<id>[\w-]+)/(?P<type>video)/?(?P<slug>continuation|member|pay|so\d{8})?(?:[/?#]|$)',
|
||||
r'https?://ch\.nicovideo\.jp/(?P<type>search)/(?P<id>[^/?#]+)',
|
||||
]
|
||||
_TESTS = [{
|
||||
'url': 'https://ch.nicovideo.jp/higurashianime/video',
|
||||
'info_dict': {
|
||||
'id': 'higurashianime',
|
||||
'title': '「ひぐらしのなく頃に」オフィシャルチャンネル',
|
||||
},
|
||||
'playlist_mincount': 54,
|
||||
}, {
|
||||
'url': 'https://ch.nicovideo.jp/amiami-ssr/video?page=2',
|
||||
'info_dict': {
|
||||
'id': 'amiami-ssr',
|
||||
'title': 'あみあみSSRチャンネル',
|
||||
},
|
||||
'playlist_count': 20,
|
||||
}, {
|
||||
'url': 'https://ch.nicovideo.jp/yukarisama/video/pay',
|
||||
'info_dict': {
|
||||
'id': 'yukarisama',
|
||||
'title': '縁結びのゆかり様',
|
||||
},
|
||||
'playlist_mincount': 16,
|
||||
}, {
|
||||
'url': 'https://ch.nicovideo.jp/mokou1/video/member',
|
||||
'info_dict': {
|
||||
'id': 'mokou1',
|
||||
'title': 'もこう。',
|
||||
},
|
||||
'playlist_mincount': 49,
|
||||
}, {
|
||||
'url': 'https://ch.nicovideo.jp/amiami-ch/video/continuation',
|
||||
'info_dict': {
|
||||
'id': 'amiami-ch',
|
||||
'title': 'あみあみチャンネル',
|
||||
},
|
||||
'playlist_mincount': 1,
|
||||
}, {
|
||||
'url': 'https://ch.nicovideo.jp/search/%E3%81%AF%E3%81%AA%E3%81%BE%E3%81%8D%E3%81%93%E3%82%82%E3%81%A1%E3%81%83?channel_id=ch2585696&type=video',
|
||||
'info_dict': {
|
||||
'id': 'secondshot',
|
||||
'title': 'セカンドショットちゃんねる - はなまきこもちぃ',
|
||||
},
|
||||
'playlist_mincount': 115,
|
||||
}, {
|
||||
'url': 'https://ch.nicovideo.jp/amiami-ch/video/so44060088',
|
||||
'only_matching': True,
|
||||
}]
|
||||
|
||||
def _fetch_page(self, url, playlist_id, page):
|
||||
page += 1
|
||||
webpage = self._download_webpage(
|
||||
url, playlist_id, f'Downloading page {page}', query={'page': page})
|
||||
for url in traverse_obj(webpage, (
|
||||
{find_elements(cls='watchLink', html=True)},
|
||||
..., {extract_attributes}, 'href', {url_or_none},
|
||||
)):
|
||||
yield self.url_result(url, NiconicoIE)
|
||||
|
||||
def _real_extract(self, url):
|
||||
mobj = self._match_valid_url(url)
|
||||
display_id = urllib.parse.unquote(mobj.group('id'))
|
||||
playlist_type = mobj.group('type')
|
||||
|
||||
if playlist_type == 'search':
|
||||
keyword = display_id
|
||||
page_size = self._SEARCH_PAGE_SIZE
|
||||
else:
|
||||
if (slug := mobj.group('slug')) and slug.startswith('so'):
|
||||
return self.url_result(
|
||||
f'{self._BASE_URL}/watch/{slug}', NiconicoIE)
|
||||
keyword = None
|
||||
page_size = self._PAGE_SIZE
|
||||
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
channel_name = traverse_obj(webpage, (
|
||||
{find_element(cls='channel_name')}, {find_element(tag='a', html=True)},
|
||||
{extract_attributes}, 'href', {str}, {trim_str(start='/')}, filter))
|
||||
site_name = self._og_search_property('site_name', webpage, default=None)
|
||||
|
||||
fetch_page = functools.partial(self._fetch_page, url, display_id)
|
||||
page = traverse_obj(parse_qs(url), ('page', -1, {int_or_none}))
|
||||
entries = fetch_page(page - 1) if page else OnDemandPagedList(fetch_page, page_size)
|
||||
|
||||
return self.playlist_result(
|
||||
entries, channel_name, join_nonempty(site_name, keyword, delim=' - '))
|
||||
|
||||
@@ -46,7 +46,7 @@ class TedTalkIE(TedBaseIE):
|
||||
webpage = self._download_webpage(url, display_id)
|
||||
talk_info = self._search_nextjs_data(webpage, display_id)['props']['pageProps']['videoData']
|
||||
video_id = talk_info['id']
|
||||
player_data = self._parse_json(talk_info.get('playerData'), video_id)
|
||||
player_data = talk_info.get('videoPlayerData') or {}
|
||||
|
||||
http_url = None
|
||||
formats, subtitles = [], {}
|
||||
@@ -193,8 +193,8 @@ class TedPlaylistIE(TedBaseIE):
|
||||
'url': 'https://www.ted.com/playlists/171/the_most_popular_talks_of_all',
|
||||
'info_dict': {
|
||||
'id': '171',
|
||||
'title': 'The most popular talks of all time',
|
||||
'description': 'md5:d2f22831dc86c7040e733a3cb3993d78',
|
||||
'title': 'The most popular TED Talks of all time',
|
||||
'description': 'md5:5346ef094754d2edd7e1a4cd3a166168',
|
||||
},
|
||||
'playlist_mincount': 25,
|
||||
}]
|
||||
|
||||
@@ -76,7 +76,7 @@ class YoutubeIEContentProviderLogger(IEContentProviderLogger):
|
||||
if self.log_level <= self.LogLevel.ERROR:
|
||||
self.__ie._downloader.report_error(
|
||||
self._format_msg(message), is_error=False,
|
||||
tb=''.join(traceback.format_exception(None, cause, cause.__traceback__)) if cause else None)
|
||||
tb=''.join(traceback.format_exception(cause)) if cause else None)
|
||||
|
||||
|
||||
class PoTokenCache:
|
||||
|
||||
@@ -108,9 +108,7 @@ def make_ssl_context(
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.check_hostname = verify
|
||||
context.verify_mode = ssl.CERT_REQUIRED if verify else ssl.CERT_NONE
|
||||
# OpenSSL 1.1.1+ Python 3.8+ keylog file
|
||||
if hasattr(context, 'keylog_filename'):
|
||||
context.keylog_filename = os.environ.get('SSLKEYLOGFILE') or None
|
||||
context.keylog_filename = os.environ.get('SSLKEYLOGFILE') or None
|
||||
|
||||
# Some servers may reject requests if ALPN extension is not sent. See:
|
||||
# https://github.com/python/cpython/issues/85140
|
||||
|
||||
@@ -123,7 +123,7 @@ class RequestsResponseAdapter(Response):
|
||||
# Work around issue with `.read(amt)` then `.read()`
|
||||
# See: https://github.com/urllib3/urllib3/issues/3636
|
||||
if amt is None:
|
||||
# compat: py3.9: Python 3.9 preallocates the whole read buffer, read in chunks
|
||||
# Read in chunks to avoid preallocating a large buffer
|
||||
read_chunk = functools.partial(self.fp.read, 1 << 20, decode_content=True)
|
||||
return b''.join(iter(read_chunk, b''))
|
||||
# Interact with urllib3 response directly.
|
||||
|
||||
@@ -296,13 +296,9 @@ class UrllibResponseAdapter(Response):
|
||||
"""
|
||||
|
||||
def __init__(self, res: http.client.HTTPResponse | urllib.response.addinfourl):
|
||||
# addinfourl: In Python 3.9+, .status was introduced and .getcode() was deprecated [1]
|
||||
# HTTPResponse: .getcode() was deprecated, .status always existed [2]
|
||||
# 1. https://docs.python.org/3/library/urllib.request.html#urllib.response.addinfourl.getcode
|
||||
# 2. https://docs.python.org/3.10/library/http.client.html#http.client.HTTPResponse.status
|
||||
super().__init__(
|
||||
fp=res, headers=res.headers, url=res.url,
|
||||
status=getattr(res, 'status', None) or res.getcode(), reason=getattr(res, 'reason', None))
|
||||
status=res.status, reason=getattr(res, 'reason', None))
|
||||
|
||||
def read(self, amt=None):
|
||||
if self.closed:
|
||||
|
||||
+1
-1
@@ -609,7 +609,7 @@ class Updater:
|
||||
self.ydl._download_retcode = 100
|
||||
|
||||
def _report_permission_error(self, file):
|
||||
self._report_error(f'Unable to write to {file}; try running as administrator', True)
|
||||
self._report_error(f'Insufficient permissions to write to {file}', True)
|
||||
|
||||
def _report_network_error(self, action, delim=';', tag=None):
|
||||
if not tag:
|
||||
|
||||
+8
-15
@@ -238,11 +238,9 @@ def find_xpath_attr(node, xpath, key, val=None):
|
||||
expr = xpath + (f'[@{key}]' if val is None else f"[@{key}='{val}']")
|
||||
return node.find(expr)
|
||||
|
||||
# On python2.6 the xml.etree.ElementTree.Element methods don't support
|
||||
# the namespace parameter
|
||||
|
||||
|
||||
def xpath_with_ns(path, ns_map):
|
||||
"""Expand namespace-prefixed names to Clark notation."""
|
||||
components = [c.split(':') for c in path.split('/')]
|
||||
replaced = []
|
||||
for c in components:
|
||||
@@ -876,7 +874,7 @@ class Popen(subprocess.Popen):
|
||||
|
||||
self.__text_mode = kwargs.get('encoding') or kwargs.get('errors') or text or kwargs.get('universal_newlines')
|
||||
if text is True:
|
||||
kwargs['universal_newlines'] = True # For 3.6 compatibility
|
||||
kwargs['text'] = True
|
||||
kwargs.setdefault('encoding', 'utf-8')
|
||||
kwargs.setdefault('errors', 'replace')
|
||||
|
||||
@@ -1012,7 +1010,7 @@ class ExtractorError(YoutubeDLError):
|
||||
def format_traceback(self):
|
||||
return join_nonempty(
|
||||
self.traceback and ''.join(traceback.format_tb(self.traceback)),
|
||||
self.cause and ''.join(traceback.format_exception(None, self.cause, self.cause.__traceback__)[1:]),
|
||||
self.cause and ''.join(traceback.format_exception(self.cause)[1:]),
|
||||
delim='\n') or None
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
@@ -1960,11 +1958,7 @@ def setproctitle(title):
|
||||
libc = ctypes.cdll.LoadLibrary('libc.so.6')
|
||||
except OSError:
|
||||
return
|
||||
except TypeError:
|
||||
# LoadLibrary in Windows Python 2.7.13 only expects
|
||||
# a bytestring, but since unicode_literals turns
|
||||
# every string into a unicode string, it fails.
|
||||
return
|
||||
|
||||
title_bytes = title.encode()
|
||||
buf = ctypes.create_string_buffer(len(title_bytes))
|
||||
buf.value = title_bytes
|
||||
@@ -2655,11 +2649,10 @@ def multipart_encode(data, boundary=None):
|
||||
Encode a dict to RFC 7578-compliant form-data
|
||||
|
||||
data:
|
||||
A dict where keys and values can be either Unicode or bytes-like
|
||||
objects.
|
||||
A dict where keys and values can be either str or bytes-like objects.
|
||||
boundary:
|
||||
If specified a Unicode object, it's used as the boundary. Otherwise
|
||||
a random boundary is generated.
|
||||
An ASCII string to use as the boundary. If omitted, a random boundary
|
||||
is generated.
|
||||
|
||||
Reference: https://tools.ietf.org/html/rfc7578
|
||||
"""
|
||||
@@ -3422,7 +3415,7 @@ def ass_subtitles_timecode(seconds):
|
||||
def dfxp2srt(dfxp_data):
|
||||
"""
|
||||
@param dfxp_data A bytes-like object containing DFXP data
|
||||
@returns A unicode object containing converted SRT data
|
||||
@returns A string containing the converted SRT data
|
||||
"""
|
||||
LEGACY_NAMESPACES = (
|
||||
(b'http://www.w3.org/ns/ttml', [
|
||||
|
||||
Reference in New Issue
Block a user