yt-dlp 2026.1.16.233125.dev0__py3-none-any.whl → 2026.1.19.359.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yt_dlp/extractor/_extractors.py +23 -28
- yt_dlp/extractor/extractors.py +13 -7
- yt_dlp/extractor/lazy_extractors.py +222 -222
- yt_dlp/extractor/youtube/_base.py +16 -16
- yt_dlp/extractor/youtube/_video.py +173 -153
- yt_dlp/networking/_curlcffi.py +2 -2
- yt_dlp/utils/_jsruntime.py +8 -0
- yt_dlp/version.py +3 -3
- {yt_dlp-2026.1.16.233125.dev0.data → yt_dlp-2026.1.19.359.dev0.data}/data/share/doc/yt_dlp/README.txt +12 -12
- {yt_dlp-2026.1.16.233125.dev0.data → yt_dlp-2026.1.19.359.dev0.data}/data/share/man/man1/yt-dlp.1 +4 -4
- {yt_dlp-2026.1.16.233125.dev0.dist-info → yt_dlp-2026.1.19.359.dev0.dist-info}/METADATA +8 -3
- {yt_dlp-2026.1.16.233125.dev0.dist-info → yt_dlp-2026.1.19.359.dev0.dist-info}/RECORD +18 -18
- {yt_dlp-2026.1.16.233125.dev0.data → yt_dlp-2026.1.19.359.dev0.data}/data/share/bash-completion/completions/yt-dlp +0 -0
- {yt_dlp-2026.1.16.233125.dev0.data → yt_dlp-2026.1.19.359.dev0.data}/data/share/fish/vendor_completions.d/yt-dlp.fish +0 -0
- {yt_dlp-2026.1.16.233125.dev0.data → yt_dlp-2026.1.19.359.dev0.data}/data/share/zsh/site-functions/_yt-dlp +0 -0
- {yt_dlp-2026.1.16.233125.dev0.dist-info → yt_dlp-2026.1.19.359.dev0.dist-info}/WHEEL +0 -0
- {yt_dlp-2026.1.16.233125.dev0.dist-info → yt_dlp-2026.1.19.359.dev0.dist-info}/entry_points.txt +0 -0
- {yt_dlp-2026.1.16.233125.dev0.dist-info → yt_dlp-2026.1.19.359.dev0.dist-info}/licenses/LICENSE +0 -0
|
@@ -99,7 +99,7 @@ INNERTUBE_CLIENTS = {
|
|
|
99
99
|
'INNERTUBE_CONTEXT': {
|
|
100
100
|
'client': {
|
|
101
101
|
'clientName': 'WEB',
|
|
102
|
-
'clientVersion': '2.
|
|
102
|
+
'clientVersion': '2.20260114.08.00',
|
|
103
103
|
},
|
|
104
104
|
},
|
|
105
105
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 1,
|
|
@@ -112,7 +112,7 @@ INNERTUBE_CLIENTS = {
|
|
|
112
112
|
'INNERTUBE_CONTEXT': {
|
|
113
113
|
'client': {
|
|
114
114
|
'clientName': 'WEB',
|
|
115
|
-
'clientVersion': '2.
|
|
115
|
+
'clientVersion': '2.20260114.08.00',
|
|
116
116
|
'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15,gzip(gfe)',
|
|
117
117
|
},
|
|
118
118
|
},
|
|
@@ -125,7 +125,7 @@ INNERTUBE_CLIENTS = {
|
|
|
125
125
|
'INNERTUBE_CONTEXT': {
|
|
126
126
|
'client': {
|
|
127
127
|
'clientName': 'WEB_EMBEDDED_PLAYER',
|
|
128
|
-
'clientVersion': '1.
|
|
128
|
+
'clientVersion': '1.20260115.01.00',
|
|
129
129
|
},
|
|
130
130
|
},
|
|
131
131
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 56,
|
|
@@ -136,7 +136,7 @@ INNERTUBE_CLIENTS = {
|
|
|
136
136
|
'INNERTUBE_CONTEXT': {
|
|
137
137
|
'client': {
|
|
138
138
|
'clientName': 'WEB_REMIX',
|
|
139
|
-
'clientVersion': '1.
|
|
139
|
+
'clientVersion': '1.20260114.03.00',
|
|
140
140
|
},
|
|
141
141
|
},
|
|
142
142
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
|
|
@@ -166,7 +166,7 @@ INNERTUBE_CLIENTS = {
|
|
|
166
166
|
'INNERTUBE_CONTEXT': {
|
|
167
167
|
'client': {
|
|
168
168
|
'clientName': 'WEB_CREATOR',
|
|
169
|
-
'clientVersion': '1.
|
|
169
|
+
'clientVersion': '1.20260114.05.00',
|
|
170
170
|
},
|
|
171
171
|
},
|
|
172
172
|
'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
|
|
@@ -195,9 +195,9 @@ INNERTUBE_CLIENTS = {
|
|
|
195
195
|
'INNERTUBE_CONTEXT': {
|
|
196
196
|
'client': {
|
|
197
197
|
'clientName': 'ANDROID',
|
|
198
|
-
'clientVersion': '
|
|
198
|
+
'clientVersion': '21.02.35',
|
|
199
199
|
'androidSdkVersion': 30,
|
|
200
|
-
'userAgent': 'com.google.android.youtube/
|
|
200
|
+
'userAgent': 'com.google.android.youtube/21.02.35 (Linux; U; Android 11) gzip',
|
|
201
201
|
'osName': 'Android',
|
|
202
202
|
'osVersion': '11',
|
|
203
203
|
},
|
|
@@ -228,8 +228,8 @@ INNERTUBE_CLIENTS = {
|
|
|
228
228
|
'INNERTUBE_CONTEXT': {
|
|
229
229
|
'client': {
|
|
230
230
|
'clientName': 'ANDROID',
|
|
231
|
-
'clientVersion': '
|
|
232
|
-
'userAgent': 'com.google.android.youtube/
|
|
231
|
+
'clientVersion': '21.02.35',
|
|
232
|
+
'userAgent': 'com.google.android.youtube/21.02.35 (Linux; U; Android 11) gzip',
|
|
233
233
|
'osName': 'Android',
|
|
234
234
|
'osVersion': '11',
|
|
235
235
|
},
|
|
@@ -242,11 +242,11 @@ INNERTUBE_CLIENTS = {
|
|
|
242
242
|
'INNERTUBE_CONTEXT': {
|
|
243
243
|
'client': {
|
|
244
244
|
'clientName': 'ANDROID_VR',
|
|
245
|
-
'clientVersion': '1.
|
|
245
|
+
'clientVersion': '1.71.26',
|
|
246
246
|
'deviceMake': 'Oculus',
|
|
247
247
|
'deviceModel': 'Quest 3',
|
|
248
248
|
'androidSdkVersion': 32,
|
|
249
|
-
'userAgent': 'com.google.android.apps.youtube.vr.oculus/1.
|
|
249
|
+
'userAgent': 'com.google.android.apps.youtube.vr.oculus/1.71.26 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip',
|
|
250
250
|
'osName': 'Android',
|
|
251
251
|
'osVersion': '12L',
|
|
252
252
|
},
|
|
@@ -260,10 +260,10 @@ INNERTUBE_CLIENTS = {
|
|
|
260
260
|
'INNERTUBE_CONTEXT': {
|
|
261
261
|
'client': {
|
|
262
262
|
'clientName': 'IOS',
|
|
263
|
-
'clientVersion': '
|
|
263
|
+
'clientVersion': '21.02.3',
|
|
264
264
|
'deviceMake': 'Apple',
|
|
265
265
|
'deviceModel': 'iPhone16,2',
|
|
266
|
-
'userAgent': 'com.google.ios.youtube/
|
|
266
|
+
'userAgent': 'com.google.ios.youtube/21.02.3 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)',
|
|
267
267
|
'osName': 'iPhone',
|
|
268
268
|
'osVersion': '18.3.2.22D82',
|
|
269
269
|
},
|
|
@@ -291,7 +291,7 @@ INNERTUBE_CLIENTS = {
|
|
|
291
291
|
'INNERTUBE_CONTEXT': {
|
|
292
292
|
'client': {
|
|
293
293
|
'clientName': 'MWEB',
|
|
294
|
-
'clientVersion': '2.
|
|
294
|
+
'clientVersion': '2.20260115.01.00',
|
|
295
295
|
# mweb previously did not require PO Token with this UA
|
|
296
296
|
'userAgent': 'Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)',
|
|
297
297
|
},
|
|
@@ -322,7 +322,7 @@ INNERTUBE_CLIENTS = {
|
|
|
322
322
|
'INNERTUBE_CONTEXT': {
|
|
323
323
|
'client': {
|
|
324
324
|
'clientName': 'TVHTML5',
|
|
325
|
-
'clientVersion': '7.
|
|
325
|
+
'clientVersion': '7.20260114.12.00',
|
|
326
326
|
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
|
327
327
|
},
|
|
328
328
|
},
|
|
@@ -335,7 +335,7 @@ INNERTUBE_CLIENTS = {
|
|
|
335
335
|
'INNERTUBE_CONTEXT': {
|
|
336
336
|
'client': {
|
|
337
337
|
'clientName': 'TVHTML5',
|
|
338
|
-
'clientVersion': '5.
|
|
338
|
+
'clientVersion': '5.20260114',
|
|
339
339
|
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
|
|
340
340
|
},
|
|
341
341
|
},
|
|
@@ -10,7 +10,6 @@ import re
|
|
|
10
10
|
import sys
|
|
11
11
|
import threading
|
|
12
12
|
import time
|
|
13
|
-
import traceback
|
|
14
13
|
import urllib.parse
|
|
15
14
|
|
|
16
15
|
from ._base import (
|
|
@@ -63,6 +62,7 @@ from ...utils import (
|
|
|
63
62
|
unescapeHTML,
|
|
64
63
|
unified_strdate,
|
|
65
64
|
unsmuggle_url,
|
|
65
|
+
update_url,
|
|
66
66
|
update_url_query,
|
|
67
67
|
url_or_none,
|
|
68
68
|
urljoin,
|
|
@@ -145,9 +145,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
145
145
|
r'\b(?P<id>vfl[a-zA-Z0-9_-]+)\b.*?\.js$',
|
|
146
146
|
)
|
|
147
147
|
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
|
|
148
|
-
_DEFAULT_CLIENTS = ('
|
|
149
|
-
_DEFAULT_JSLESS_CLIENTS = ('android_sdkless',
|
|
150
|
-
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', '
|
|
148
|
+
_DEFAULT_CLIENTS = ('android_sdkless', 'web', 'web_safari')
|
|
149
|
+
_DEFAULT_JSLESS_CLIENTS = ('android_sdkless',)
|
|
150
|
+
_DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web', 'web_safari')
|
|
151
151
|
# Premium does not require POT (except for subtitles)
|
|
152
152
|
_DEFAULT_PREMIUM_CLIENTS = ('tv_downgraded', 'web_creator', 'web')
|
|
153
153
|
|
|
@@ -2193,64 +2193,32 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
2193
2193
|
self._code_cache[player_js_key] = code
|
|
2194
2194
|
return self._code_cache.get(player_js_key)
|
|
2195
2195
|
|
|
2196
|
-
def
|
|
2197
|
-
|
|
2196
|
+
def _load_player_data_from_cache(self, name, player_url, *cache_keys, use_disk_cache=False):
|
|
2197
|
+
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url), *map(str_or_none, cache_keys))
|
|
2198
|
+
if cache_id in self._player_cache:
|
|
2199
|
+
return self._player_cache[cache_id]
|
|
2198
2200
|
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
# I hate it
|
|
2202
|
-
if spec_cache_id in self._player_cache:
|
|
2203
|
-
return self._player_cache[spec_cache_id]
|
|
2204
|
-
spec = self.cache.load('youtube-sigfuncs', spec_cache_id, min_ver='2025.07.21')
|
|
2205
|
-
if spec:
|
|
2206
|
-
self._player_cache[spec_cache_id] = spec
|
|
2207
|
-
return spec
|
|
2208
|
-
|
|
2209
|
-
def _store_sig_spec_to_cache(self, spec_cache_id, spec):
|
|
2210
|
-
if spec_cache_id not in self._player_cache:
|
|
2211
|
-
self._player_cache[spec_cache_id] = spec
|
|
2212
|
-
self.cache.store('youtube-sigfuncs', spec_cache_id, spec)
|
|
2213
|
-
|
|
2214
|
-
def _load_player_data_from_cache(self, name, player_url):
|
|
2215
|
-
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url))
|
|
2216
|
-
|
|
2217
|
-
if data := self._player_cache.get(cache_id):
|
|
2218
|
-
return data
|
|
2201
|
+
if not use_disk_cache:
|
|
2202
|
+
return None
|
|
2219
2203
|
|
|
2220
|
-
data = self.cache.load(*cache_id, min_ver='2025.07.21')
|
|
2204
|
+
data = self.cache.load(cache_id[0], join_nonempty(*cache_id[1:]), min_ver='2025.07.21')
|
|
2221
2205
|
if data:
|
|
2222
2206
|
self._player_cache[cache_id] = data
|
|
2223
2207
|
|
|
2224
2208
|
return data
|
|
2225
2209
|
|
|
2226
|
-
def
|
|
2227
|
-
|
|
2228
|
-
if cache_id not in self._player_cache:
|
|
2229
|
-
try:
|
|
2230
|
-
self._player_cache[cache_id] = func(*args, **kwargs)
|
|
2231
|
-
except ExtractorError as e:
|
|
2232
|
-
self._player_cache[cache_id] = e
|
|
2233
|
-
except Exception as e:
|
|
2234
|
-
self._player_cache[cache_id] = ExtractorError(traceback.format_exc(), cause=e)
|
|
2235
|
-
|
|
2236
|
-
ret = self._player_cache[cache_id]
|
|
2237
|
-
if isinstance(ret, Exception):
|
|
2238
|
-
raise ret
|
|
2239
|
-
return ret
|
|
2240
|
-
return inner
|
|
2241
|
-
|
|
2242
|
-
def _store_player_data_to_cache(self, name, player_url, data):
|
|
2243
|
-
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url))
|
|
2210
|
+
def _store_player_data_to_cache(self, data, name, player_url, *cache_keys, use_disk_cache=False):
|
|
2211
|
+
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url), *map(str_or_none, cache_keys))
|
|
2244
2212
|
if cache_id not in self._player_cache:
|
|
2245
|
-
self.cache.store(*cache_id, data)
|
|
2246
2213
|
self._player_cache[cache_id] = data
|
|
2214
|
+
if use_disk_cache:
|
|
2215
|
+
self.cache.store(cache_id[0], join_nonempty(*cache_id[1:]), data)
|
|
2247
2216
|
|
|
2248
2217
|
def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False):
|
|
2249
2218
|
"""
|
|
2250
2219
|
Extract signatureTimestamp (sts)
|
|
2251
2220
|
Required to tell API what sig/player version is in use.
|
|
2252
2221
|
"""
|
|
2253
|
-
CACHE_ENABLED = False # TODO: enable when preprocessed player JS cache is solved/enabled
|
|
2254
2222
|
|
|
2255
2223
|
player_sts_override = self._get_player_js_version()[0]
|
|
2256
2224
|
if player_sts_override:
|
|
@@ -2267,15 +2235,17 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
2267
2235
|
self.report_warning(error_msg)
|
|
2268
2236
|
return None
|
|
2269
2237
|
|
|
2270
|
-
|
|
2238
|
+
# TODO: Pass `use_disk_cache=True` when preprocessed player JS cache is solved
|
|
2239
|
+
if sts := self._load_player_data_from_cache('sts', player_url):
|
|
2271
2240
|
return sts
|
|
2272
2241
|
|
|
2273
2242
|
if code := self._load_player(video_id, player_url, fatal=fatal):
|
|
2274
2243
|
sts = int_or_none(self._search_regex(
|
|
2275
2244
|
r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
|
|
2276
2245
|
'JS player signature timestamp', group='sts', fatal=fatal))
|
|
2277
|
-
if
|
|
2278
|
-
|
|
2246
|
+
if sts:
|
|
2247
|
+
# TODO: Pass `use_disk_cache=True` when preprocessed player JS cache is solved
|
|
2248
|
+
self._store_player_data_to_cache(sts, 'sts', player_url)
|
|
2279
2249
|
|
|
2280
2250
|
return sts
|
|
2281
2251
|
|
|
@@ -2793,7 +2763,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
2793
2763
|
'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'serializedExperimentFlags', {urllib.parse.parse_qs}))
|
|
2794
2764
|
if 'true' in traverse_obj(experiments, (..., 'html5_generate_content_po_token', -1)):
|
|
2795
2765
|
self.write_debug(
|
|
2796
|
-
f'{video_id}: Detected experiment to bind GVS PO Token
|
|
2766
|
+
f'{video_id}: Detected experiment to bind GVS PO Token '
|
|
2767
|
+
f'to video ID for {client} client', only_once=True)
|
|
2797
2768
|
gvs_bind_to_video_id = True
|
|
2798
2769
|
|
|
2799
2770
|
# GVS WebPO Token is bound to visitor_data / Visitor ID when logged out.
|
|
@@ -3233,6 +3204,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3233
3204
|
'audio_quality_ultralow', 'audio_quality_low', 'audio_quality_medium', 'audio_quality_high', # Audio only formats
|
|
3234
3205
|
'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres',
|
|
3235
3206
|
])
|
|
3207
|
+
skip_player_js = 'js' in self._configuration_arg('player_skip')
|
|
3236
3208
|
format_types = self._configuration_arg('formats')
|
|
3237
3209
|
all_formats = 'duplicate' in format_types
|
|
3238
3210
|
if self._configuration_arg('include_duplicate_formats'):
|
|
@@ -3278,6 +3250,98 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3278
3250
|
return language_code, DEFAULT_LANG_VALUE
|
|
3279
3251
|
return language_code, -1
|
|
3280
3252
|
|
|
3253
|
+
def get_manifest_n_challenge(manifest_url):
|
|
3254
|
+
if not url_or_none(manifest_url):
|
|
3255
|
+
return None
|
|
3256
|
+
# Same pattern that the player JS uses to read/replace the n challenge value
|
|
3257
|
+
return self._search_regex(
|
|
3258
|
+
r'/n/([^/]+)/', urllib.parse.urlparse(manifest_url).path,
|
|
3259
|
+
'n challenge', default=None)
|
|
3260
|
+
|
|
3261
|
+
n_challenges = set()
|
|
3262
|
+
s_challenges = set()
|
|
3263
|
+
|
|
3264
|
+
def solve_js_challenges():
|
|
3265
|
+
# Solve all n/sig challenges in bulk and store the results in self._player_cache
|
|
3266
|
+
challenge_requests = []
|
|
3267
|
+
if n_challenges:
|
|
3268
|
+
challenge_requests.append(JsChallengeRequest(
|
|
3269
|
+
type=JsChallengeType.N,
|
|
3270
|
+
video_id=video_id,
|
|
3271
|
+
input=NChallengeInput(challenges=list(n_challenges), player_url=player_url)))
|
|
3272
|
+
if s_challenges:
|
|
3273
|
+
cached_sigfuncs = set()
|
|
3274
|
+
for spec_id in s_challenges:
|
|
3275
|
+
if self._load_player_data_from_cache('sigfuncs', player_url, spec_id, use_disk_cache=True):
|
|
3276
|
+
cached_sigfuncs.add(spec_id)
|
|
3277
|
+
s_challenges.difference_update(cached_sigfuncs)
|
|
3278
|
+
|
|
3279
|
+
challenge_requests.append(JsChallengeRequest(
|
|
3280
|
+
type=JsChallengeType.SIG,
|
|
3281
|
+
video_id=video_id,
|
|
3282
|
+
input=SigChallengeInput(
|
|
3283
|
+
challenges=[''.join(map(chr, range(spec_id))) for spec_id in s_challenges],
|
|
3284
|
+
player_url=player_url)))
|
|
3285
|
+
|
|
3286
|
+
if challenge_requests:
|
|
3287
|
+
for _challenge_request, challenge_response in self._jsc_director.bulk_solve(challenge_requests):
|
|
3288
|
+
if challenge_response.type == JsChallengeType.SIG:
|
|
3289
|
+
for challenge, result in challenge_response.output.results.items():
|
|
3290
|
+
spec_id = len(challenge)
|
|
3291
|
+
self._store_player_data_to_cache(
|
|
3292
|
+
[ord(c) for c in result], 'sigfuncs',
|
|
3293
|
+
player_url, spec_id, use_disk_cache=True)
|
|
3294
|
+
if spec_id in s_challenges:
|
|
3295
|
+
s_challenges.remove(spec_id)
|
|
3296
|
+
|
|
3297
|
+
elif challenge_response.type == JsChallengeType.N:
|
|
3298
|
+
for challenge, result in challenge_response.output.results.items():
|
|
3299
|
+
self._store_player_data_to_cache(result, 'n', player_url, challenge)
|
|
3300
|
+
if challenge in n_challenges:
|
|
3301
|
+
n_challenges.remove(challenge)
|
|
3302
|
+
|
|
3303
|
+
# Raise warning if any challenge requests remain
|
|
3304
|
+
# Depending on type of challenge request
|
|
3305
|
+
help_message = (
|
|
3306
|
+
'Ensure you have a supported JavaScript runtime and '
|
|
3307
|
+
'challenge solver script distribution installed. '
|
|
3308
|
+
'Review any warnings presented before this message. '
|
|
3309
|
+
f'For more details, refer to {_EJS_WIKI_URL}')
|
|
3310
|
+
if s_challenges:
|
|
3311
|
+
self.report_warning(
|
|
3312
|
+
f'Signature solving failed: Some formats may be missing. {help_message}',
|
|
3313
|
+
video_id=video_id, only_once=True)
|
|
3314
|
+
if n_challenges:
|
|
3315
|
+
self.report_warning(
|
|
3316
|
+
f'n challenge solving failed: Some formats may be missing. {help_message}',
|
|
3317
|
+
video_id=video_id, only_once=True)
|
|
3318
|
+
|
|
3319
|
+
# Clear challenge sets so that any subsequent call of this function is a no-op
|
|
3320
|
+
s_challenges.clear()
|
|
3321
|
+
n_challenges.clear()
|
|
3322
|
+
|
|
3323
|
+
# 1st pass to collect all n/sig challenges so they can later be solved at once in bulk
|
|
3324
|
+
for streaming_data in traverse_obj(player_responses, (..., 'streamingData', {dict})):
|
|
3325
|
+
# HTTPS formats
|
|
3326
|
+
for fmt_stream in traverse_obj(streaming_data, (('formats', 'adaptiveFormats'), ..., {dict})):
|
|
3327
|
+
fmt_url = fmt_stream.get('url')
|
|
3328
|
+
s_challenge = None
|
|
3329
|
+
if not fmt_url:
|
|
3330
|
+
sc = urllib.parse.parse_qs(fmt_stream.get('signatureCipher'))
|
|
3331
|
+
fmt_url = traverse_obj(sc, ('url', 0, {url_or_none}))
|
|
3332
|
+
s_challenge = traverse_obj(sc, ('s', 0))
|
|
3333
|
+
|
|
3334
|
+
if s_challenge:
|
|
3335
|
+
s_challenges.add(len(s_challenge))
|
|
3336
|
+
|
|
3337
|
+
if n_challenge := traverse_obj(fmt_url, ({parse_qs}, 'n', 0)):
|
|
3338
|
+
n_challenges.add(n_challenge)
|
|
3339
|
+
|
|
3340
|
+
# Manifest formats
|
|
3341
|
+
n_challenges.update(traverse_obj(
|
|
3342
|
+
streaming_data, (('hlsManifestUrl', 'dashManifestUrl'), {get_manifest_n_challenge})))
|
|
3343
|
+
|
|
3344
|
+
# Final pass to extract formats and solve n/sig challenges as needed
|
|
3281
3345
|
for pr in player_responses:
|
|
3282
3346
|
streaming_data = traverse_obj(pr, 'streamingData')
|
|
3283
3347
|
if not streaming_data:
|
|
@@ -3385,7 +3449,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3385
3449
|
def process_https_formats():
|
|
3386
3450
|
proto = 'https'
|
|
3387
3451
|
https_fmts = []
|
|
3388
|
-
skip_player_js = 'js' in self._configuration_arg('player_skip')
|
|
3389
3452
|
|
|
3390
3453
|
for fmt_stream in streaming_formats:
|
|
3391
3454
|
if fmt_stream.get('targetDurationSec'):
|
|
@@ -3422,19 +3485,21 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3422
3485
|
# See: https://github.com/yt-dlp/yt-dlp/issues/14883
|
|
3423
3486
|
get_language_code_and_preference(fmt_stream)
|
|
3424
3487
|
sc = urllib.parse.parse_qs(fmt_stream.get('signatureCipher'))
|
|
3425
|
-
fmt_url =
|
|
3426
|
-
encrypted_sig =
|
|
3488
|
+
fmt_url = traverse_obj(sc, ('url', 0, {url_or_none}))
|
|
3489
|
+
encrypted_sig = traverse_obj(sc, ('s', 0))
|
|
3427
3490
|
if not all((sc, fmt_url, skip_player_js or player_url, encrypted_sig)):
|
|
3428
|
-
|
|
3491
|
+
msg_tmpl = (
|
|
3492
|
+
'{}Some {} client https formats have been skipped as they are missing a URL. '
|
|
3493
|
+
'{}. See https://github.com/yt-dlp/yt-dlp/issues/12482 for more details')
|
|
3429
3494
|
if client_name in ('web', 'web_safari'):
|
|
3430
|
-
|
|
3495
|
+
self.write_debug(msg_tmpl.format(
|
|
3496
|
+
f'{video_id}: ', client_name,
|
|
3497
|
+
'YouTube is forcing SABR streaming for this client'), only_once=True)
|
|
3431
3498
|
else:
|
|
3432
|
-
msg
|
|
3499
|
+
msg = (
|
|
3433
3500
|
f'YouTube may have enabled the SABR-only streaming experiment for '
|
|
3434
|
-
f'{"your account" if self.is_authenticated else "the current session"}
|
|
3435
|
-
)
|
|
3436
|
-
msg += 'See https://github.com/yt-dlp/yt-dlp/issues/12482 for more details'
|
|
3437
|
-
self.report_warning(msg, video_id, only_once=True)
|
|
3501
|
+
f'{"your account" if self.is_authenticated else "the current session"}')
|
|
3502
|
+
self.report_warning(msg_tmpl.format('', client_name, msg), video_id, only_once=True)
|
|
3438
3503
|
continue
|
|
3439
3504
|
|
|
3440
3505
|
fmt = process_format_stream(
|
|
@@ -3444,19 +3509,17 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3444
3509
|
continue
|
|
3445
3510
|
|
|
3446
3511
|
# signature
|
|
3447
|
-
# Attempt to load sig spec from cache
|
|
3448
3512
|
if encrypted_sig:
|
|
3449
3513
|
if skip_player_js:
|
|
3450
3514
|
continue
|
|
3451
|
-
|
|
3452
|
-
spec = self.
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
fmt['_jsc_s_sc'] = sc
|
|
3515
|
+
solve_js_challenges()
|
|
3516
|
+
spec = self._load_player_data_from_cache(
|
|
3517
|
+
'sigfuncs', player_url, len(encrypted_sig), use_disk_cache=True)
|
|
3518
|
+
if not spec:
|
|
3519
|
+
continue
|
|
3520
|
+
fmt_url += '&{}={}'.format(
|
|
3521
|
+
traverse_obj(sc, ('sp', -1)) or 'signature',
|
|
3522
|
+
solve_sig(encrypted_sig, spec))
|
|
3460
3523
|
|
|
3461
3524
|
# n challenge
|
|
3462
3525
|
query = parse_qs(fmt_url)
|
|
@@ -3464,10 +3527,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3464
3527
|
if skip_player_js:
|
|
3465
3528
|
continue
|
|
3466
3529
|
n_challenge = query['n'][0]
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3530
|
+
solve_js_challenges()
|
|
3531
|
+
n_result = self._load_player_data_from_cache('n', player_url, n_challenge)
|
|
3532
|
+
if not n_result:
|
|
3533
|
+
continue
|
|
3534
|
+
fmt_url = update_url_query(fmt_url, {'n': n_result})
|
|
3471
3535
|
|
|
3472
3536
|
if po_token:
|
|
3473
3537
|
fmt_url = update_url_query(fmt_url, {'pot': po_token})
|
|
@@ -3484,80 +3548,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3484
3548
|
|
|
3485
3549
|
https_fmts.append(fmt)
|
|
3486
3550
|
|
|
3487
|
-
# Bulk process sig/n handling
|
|
3488
|
-
# Retrieve all JSC Sig and n requests for this player response in one go
|
|
3489
|
-
n_challenges = {}
|
|
3490
|
-
s_challenges = {}
|
|
3491
|
-
for fmt in https_fmts:
|
|
3492
|
-
# This will de-duplicate requests
|
|
3493
|
-
n_challenge = fmt.pop('_jsc_n_challenge', None)
|
|
3494
|
-
if n_challenge is not None:
|
|
3495
|
-
n_challenges.setdefault(n_challenge, []).append(fmt)
|
|
3496
|
-
|
|
3497
|
-
s_challenge = fmt.pop('_jsc_s_challenge', None)
|
|
3498
|
-
if s_challenge is not None:
|
|
3499
|
-
s_challenges.setdefault(len(s_challenge), {}).setdefault(s_challenge, []).append(fmt)
|
|
3500
|
-
|
|
3501
|
-
challenge_requests = []
|
|
3502
|
-
if n_challenges:
|
|
3503
|
-
challenge_requests.append(JsChallengeRequest(
|
|
3504
|
-
type=JsChallengeType.N,
|
|
3505
|
-
video_id=video_id,
|
|
3506
|
-
input=NChallengeInput(challenges=list(n_challenges.keys()), player_url=player_url)))
|
|
3507
|
-
if s_challenges:
|
|
3508
|
-
challenge_requests.append(JsChallengeRequest(
|
|
3509
|
-
type=JsChallengeType.SIG,
|
|
3510
|
-
video_id=video_id,
|
|
3511
|
-
input=SigChallengeInput(challenges=[''.join(map(chr, range(spec_id))) for spec_id in s_challenges], player_url=player_url)))
|
|
3512
|
-
|
|
3513
|
-
if challenge_requests:
|
|
3514
|
-
for _challenge_request, challenge_response in self._jsc_director.bulk_solve(challenge_requests):
|
|
3515
|
-
if challenge_response.type == JsChallengeType.SIG:
|
|
3516
|
-
for challenge, result in challenge_response.output.results.items():
|
|
3517
|
-
spec_id = len(challenge)
|
|
3518
|
-
spec = [ord(c) for c in result]
|
|
3519
|
-
self._store_sig_spec_to_cache(self._sig_spec_cache_id(player_url, spec_id), spec)
|
|
3520
|
-
s_challenge_data = s_challenges.pop(spec_id, {})
|
|
3521
|
-
if not s_challenge_data:
|
|
3522
|
-
continue
|
|
3523
|
-
for s_challenge, fmts in s_challenge_data.items():
|
|
3524
|
-
solved_challenge = solve_sig(s_challenge, spec)
|
|
3525
|
-
for fmt in fmts:
|
|
3526
|
-
sc = fmt.pop('_jsc_s_sc')
|
|
3527
|
-
fmt['url'] += '&{}={}'.format(
|
|
3528
|
-
traverse_obj(sc, ('sp', -1)) or 'signature',
|
|
3529
|
-
solved_challenge)
|
|
3530
|
-
|
|
3531
|
-
elif challenge_response.type == JsChallengeType.N:
|
|
3532
|
-
for challenge, result in challenge_response.output.results.items():
|
|
3533
|
-
fmts = n_challenges.pop(challenge, [])
|
|
3534
|
-
for fmt in fmts:
|
|
3535
|
-
self._player_cache[challenge] = result
|
|
3536
|
-
fmt['url'] = update_url_query(fmt['url'], {'n': result})
|
|
3537
|
-
|
|
3538
|
-
# Raise warning if any challenge requests remain
|
|
3539
|
-
# Depending on type of challenge request
|
|
3540
|
-
|
|
3541
|
-
help_message = (
|
|
3542
|
-
'Ensure you have a supported JavaScript runtime and '
|
|
3543
|
-
'challenge solver script distribution installed. '
|
|
3544
|
-
'Review any warnings presented before this message. '
|
|
3545
|
-
f'For more details, refer to {_EJS_WIKI_URL}')
|
|
3546
|
-
|
|
3547
|
-
if s_challenges:
|
|
3548
|
-
self.report_warning(
|
|
3549
|
-
f'Signature solving failed: Some formats may be missing. {help_message}',
|
|
3550
|
-
video_id=video_id, only_once=True)
|
|
3551
|
-
if n_challenges:
|
|
3552
|
-
self.report_warning(
|
|
3553
|
-
f'n challenge solving failed: Some formats may be missing. {help_message}',
|
|
3554
|
-
video_id=video_id, only_once=True)
|
|
3555
|
-
|
|
3556
|
-
for cfmts in list(s_challenges.values()) + list(n_challenges.values()):
|
|
3557
|
-
for fmt in cfmts:
|
|
3558
|
-
if fmt in https_fmts:
|
|
3559
|
-
https_fmts.remove(fmt)
|
|
3560
|
-
|
|
3561
3551
|
for fmt in https_fmts:
|
|
3562
3552
|
if (all_formats or 'dashy' in format_types) and fmt['filesize']:
|
|
3563
3553
|
yield {
|
|
@@ -3640,17 +3630,34 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3640
3630
|
|
|
3641
3631
|
hls_manifest_url = 'hls' not in skip_manifests and streaming_data.get('hlsManifestUrl')
|
|
3642
3632
|
if hls_manifest_url:
|
|
3633
|
+
manifest_path = urllib.parse.urlparse(hls_manifest_url).path
|
|
3634
|
+
if m := re.fullmatch(r'(?P<path>.+)(?P<suffix>/(?:file|playlist)/index\.m3u8)', manifest_path):
|
|
3635
|
+
manifest_path, manifest_suffix = m.group('path', 'suffix')
|
|
3636
|
+
else:
|
|
3637
|
+
manifest_suffix = ''
|
|
3638
|
+
|
|
3639
|
+
solved_n = False
|
|
3640
|
+
n_challenge = get_manifest_n_challenge(hls_manifest_url)
|
|
3641
|
+
if n_challenge and not skip_player_js:
|
|
3642
|
+
solve_js_challenges()
|
|
3643
|
+
n_result = self._load_player_data_from_cache('n', player_url, n_challenge)
|
|
3644
|
+
if n_result:
|
|
3645
|
+
manifest_path = manifest_path.replace(f'/n/{n_challenge}', f'/n/{n_result}')
|
|
3646
|
+
solved_n = n_result in manifest_path
|
|
3647
|
+
|
|
3643
3648
|
pot_policy: GvsPoTokenPolicy = self._get_default_ytcfg(
|
|
3644
3649
|
client_name)['GVS_PO_TOKEN_POLICY'][StreamingProtocol.HLS]
|
|
3645
3650
|
require_po_token = gvs_pot_required(pot_policy, is_premium_subscriber, player_token_provided)
|
|
3646
3651
|
po_token = gvs_pots.get(client_name, fetch_po_token_func(required=require_po_token or pot_policy.recommended))
|
|
3647
3652
|
if po_token:
|
|
3648
|
-
|
|
3653
|
+
manifest_path = manifest_path.rstrip('/') + f'/pot/{po_token}'
|
|
3649
3654
|
if client_name not in gvs_pots:
|
|
3650
3655
|
gvs_pots[client_name] = po_token
|
|
3656
|
+
|
|
3651
3657
|
if require_po_token and not po_token and 'missing_pot' not in self._configuration_arg('formats'):
|
|
3652
3658
|
self._report_pot_format_skipped(video_id, client_name, 'hls')
|
|
3653
|
-
|
|
3659
|
+
elif solved_n or not n_challenge:
|
|
3660
|
+
hls_manifest_url = update_url(hls_manifest_url, path=f'{manifest_path}{manifest_suffix}')
|
|
3654
3661
|
fmts, subs = self._extract_m3u8_formats_and_subtitles(
|
|
3655
3662
|
hls_manifest_url, video_id, 'mp4', fatal=False, live=live_status == 'is_live')
|
|
3656
3663
|
for sub in traverse_obj(subs, (..., ..., {dict})):
|
|
@@ -3665,17 +3672,30 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
|
|
3665
3672
|
|
|
3666
3673
|
dash_manifest_url = 'dash' not in skip_manifests and streaming_data.get('dashManifestUrl')
|
|
3667
3674
|
if dash_manifest_url:
|
|
3675
|
+
manifest_path = urllib.parse.urlparse(dash_manifest_url).path
|
|
3676
|
+
|
|
3677
|
+
solved_n = False
|
|
3678
|
+
n_challenge = get_manifest_n_challenge(dash_manifest_url)
|
|
3679
|
+
if n_challenge and not skip_player_js:
|
|
3680
|
+
solve_js_challenges()
|
|
3681
|
+
n_result = self._load_player_data_from_cache('n', player_url, n_challenge)
|
|
3682
|
+
if n_result:
|
|
3683
|
+
manifest_path = manifest_path.replace(f'/n/{n_challenge}', f'/n/{n_result}')
|
|
3684
|
+
solved_n = n_result in manifest_path
|
|
3685
|
+
|
|
3668
3686
|
pot_policy: GvsPoTokenPolicy = self._get_default_ytcfg(
|
|
3669
3687
|
client_name)['GVS_PO_TOKEN_POLICY'][StreamingProtocol.DASH]
|
|
3670
3688
|
require_po_token = gvs_pot_required(pot_policy, is_premium_subscriber, player_token_provided)
|
|
3671
3689
|
po_token = gvs_pots.get(client_name, fetch_po_token_func(required=require_po_token or pot_policy.recommended))
|
|
3672
3690
|
if po_token:
|
|
3673
|
-
|
|
3691
|
+
manifest_path = manifest_path.rstrip('/') + f'/pot/{po_token}'
|
|
3674
3692
|
if client_name not in gvs_pots:
|
|
3675
3693
|
gvs_pots[client_name] = po_token
|
|
3694
|
+
|
|
3676
3695
|
if require_po_token and not po_token and 'missing_pot' not in self._configuration_arg('formats'):
|
|
3677
3696
|
self._report_pot_format_skipped(video_id, client_name, 'dash')
|
|
3678
|
-
|
|
3697
|
+
elif solved_n or not n_challenge:
|
|
3698
|
+
dash_manifest_url = update_url(dash_manifest_url, path=manifest_path)
|
|
3679
3699
|
formats, subs = self._extract_mpd_formats_and_subtitles(dash_manifest_url, video_id, fatal=False)
|
|
3680
3700
|
for sub in traverse_obj(subs, (..., ..., {dict})):
|
|
3681
3701
|
# TODO: If DASH video requires a PO Token, do the subs also require pot?
|
yt_dlp/networking/_curlcffi.py
CHANGED
|
@@ -33,9 +33,9 @@ if curl_cffi is None:
|
|
|
33
33
|
|
|
34
34
|
curl_cffi_version = tuple(map(int, re.split(r'[^\d]+', curl_cffi.__version__)[:3]))
|
|
35
35
|
|
|
36
|
-
if curl_cffi_version != (0, 5, 10) and not (0, 10) <= curl_cffi_version < (0,
|
|
36
|
+
if curl_cffi_version != (0, 5, 10) and not (0, 10) <= curl_cffi_version < (0, 15):
|
|
37
37
|
curl_cffi._yt_dlp__version = f'{curl_cffi.__version__} (unsupported)'
|
|
38
|
-
raise ImportError('Only curl_cffi versions 0.5.10
|
|
38
|
+
raise ImportError('Only curl_cffi versions 0.5.10 and 0.10.x through 0.14.x are supported')
|
|
39
39
|
|
|
40
40
|
import curl_cffi.requests
|
|
41
41
|
from curl_cffi.const import CurlECode, CurlOpt
|
yt_dlp/utils/_jsruntime.py
CHANGED
|
@@ -5,6 +5,7 @@ import dataclasses
|
|
|
5
5
|
import functools
|
|
6
6
|
import os.path
|
|
7
7
|
import sys
|
|
8
|
+
import sysconfig
|
|
8
9
|
|
|
9
10
|
from ._utils import _get_exe_version_output, detect_exe_version, version_tuple
|
|
10
11
|
|
|
@@ -13,6 +14,13 @@ _FALLBACK_PATHEXT = ('.COM', '.EXE', '.BAT', '.CMD')
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def _find_exe(basename: str) -> str:
|
|
17
|
+
# Check in Python "scripts" path, e.g. for pipx-installed binaries
|
|
18
|
+
binary = os.path.join(
|
|
19
|
+
sysconfig.get_path('scripts'),
|
|
20
|
+
basename + sysconfig.get_config_var('EXE'))
|
|
21
|
+
if os.access(binary, os.F_OK | os.X_OK) and not os.path.isdir(binary):
|
|
22
|
+
return binary
|
|
23
|
+
|
|
16
24
|
if os.name != 'nt':
|
|
17
25
|
return basename
|
|
18
26
|
|
yt_dlp/version.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Autogenerated by devscripts/update-version.py
|
|
2
2
|
|
|
3
|
-
__version__ = '2026.01.
|
|
3
|
+
__version__ = '2026.01.19.000359'
|
|
4
4
|
|
|
5
|
-
RELEASE_GIT_HEAD = '
|
|
5
|
+
RELEASE_GIT_HEAD = '9ab4777b97b5280ae1f53d1fe1b8ac542727238b'
|
|
6
6
|
|
|
7
7
|
VARIANT = 'pip'
|
|
8
8
|
|
|
@@ -12,4 +12,4 @@ CHANNEL = 'nightly'
|
|
|
12
12
|
|
|
13
13
|
ORIGIN = 'yt-dlp/yt-dlp-nightly-builds'
|
|
14
14
|
|
|
15
|
-
_pkg_version = '2026.01.
|
|
15
|
+
_pkg_version = '2026.01.19.000359dev'
|