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.
@@ -99,7 +99,7 @@ INNERTUBE_CLIENTS = {
99
99
  'INNERTUBE_CONTEXT': {
100
100
  'client': {
101
101
  'clientName': 'WEB',
102
- 'clientVersion': '2.20250925.01.00',
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.20250925.01.00',
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.20250923.21.00',
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.20250922.03.00',
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.20250922.03.00',
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': '20.10.38',
198
+ 'clientVersion': '21.02.35',
199
199
  'androidSdkVersion': 30,
200
- 'userAgent': 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip',
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': '20.10.38',
232
- 'userAgent': 'com.google.android.youtube/20.10.38 (Linux; U; Android 11) gzip',
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.65.10',
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.65.10 (Linux; U; Android 12L; eureka-user Build/SQ3A.220605.009.A1) gzip',
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': '20.10.4',
263
+ 'clientVersion': '21.02.3',
264
264
  'deviceMake': 'Apple',
265
265
  'deviceModel': 'iPhone16,2',
266
- 'userAgent': 'com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)',
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.20250925.01.00',
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.20250923.13.00',
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.20251105',
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 = ('tv', 'android_sdkless', 'web')
149
- _DEFAULT_JSLESS_CLIENTS = ('android_sdkless', 'web_safari', 'web')
150
- _DEFAULT_AUTHED_CLIENTS = ('tv_downgraded', 'web_safari', 'web')
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 _sig_spec_cache_id(self, player_url, spec_id):
2197
- return join_nonempty(self._player_js_cache_key(player_url), str(spec_id))
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
- def _load_sig_spec_from_cache(self, spec_cache_id):
2200
- # This is almost identical to _load_player_data_from_cache
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 _cached(self, func, *cache_id):
2227
- def inner(*args, **kwargs):
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
- if CACHE_ENABLED and (sts := self._load_player_data_from_cache('sts', player_url)):
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 CACHE_ENABLED and sts:
2278
- self._store_player_data_to_cache('sts', player_url, sts)
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 to video id.', only_once=True)
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 = url_or_none(try_get(sc, lambda x: x['url'][0]))
3426
- encrypted_sig = try_get(sc, lambda x: x['s'][0])
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
- msg = f'Some {client_name} client https formats have been skipped as they are missing a URL. '
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
- msg += 'YouTube is forcing SABR streaming for this client. '
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
- spec_cache_id = self._sig_spec_cache_id(player_url, len(encrypted_sig))
3452
- spec = self._load_sig_spec_from_cache(spec_cache_id)
3453
- if spec:
3454
- self.write_debug(f'Using cached signature function {spec_cache_id}', only_once=True)
3455
- fmt_url += '&{}={}'.format(traverse_obj(sc, ('sp', -1)) or 'signature',
3456
- solve_sig(encrypted_sig, spec))
3457
- else:
3458
- fmt['_jsc_s_challenge'] = encrypted_sig
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
- if n_challenge in self._player_cache:
3468
- fmt_url = update_url_query(fmt_url, {'n': self._player_cache[n_challenge]})
3469
- else:
3470
- fmt['_jsc_n_challenge'] = n_challenge
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
- hls_manifest_url = hls_manifest_url.rstrip('/') + f'/pot/{po_token}'
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
- else:
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
- dash_manifest_url = dash_manifest_url.rstrip('/') + f'/pot/{po_token}'
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
- else:
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?
@@ -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, 14):
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, 0.10.x, 0.11.x, 0.12.x, 0.13.x are supported')
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
@@ -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.16.233125'
3
+ __version__ = '2026.01.19.000359'
4
4
 
5
- RELEASE_GIT_HEAD = 'ede54330fb38866936c63ebb96c490a2d4b1b58c'
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.16.233125dev'
15
+ _pkg_version = '2026.01.19.000359dev'