yt-dlp 2025.11.23.5251.dev0__py3-none-any.whl → 2025.11.24.232953.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/nhk.py CHANGED
@@ -23,96 +23,38 @@ from ..utils import (
23
23
 
24
24
 
25
25
  class NhkBaseIE(InfoExtractor):
26
- _API_URL_TEMPLATE = 'https://nwapi.nhk.jp/nhkworld/%sod%slist/v7b/%s/%s/%s/all%s.json'
26
+ _API_URL_TEMPLATE = 'https://api.nhkworld.jp/showsapi/v1/{lang}/{content_format}_{page_type}/{m_id}{extra_page}'
27
27
  _BASE_URL_REGEX = r'https?://www3\.nhk\.or\.jp/nhkworld/(?P<lang>[a-z]{2})/'
28
28
 
29
29
  def _call_api(self, m_id, lang, is_video, is_episode, is_clip):
30
+ content_format = 'video' if is_video else 'audio'
31
+ content_type = 'clips' if is_clip else 'episodes'
32
+ if not is_episode:
33
+ extra_page = f'/{content_format}_{content_type}'
34
+ page_type = 'programs'
35
+ else:
36
+ extra_page = ''
37
+ page_type = content_type
38
+
30
39
  return self._download_json(
31
- self._API_URL_TEMPLATE % (
32
- 'v' if is_video else 'r',
33
- 'clip' if is_clip else 'esd',
34
- 'episode' if is_episode else 'program',
35
- m_id, lang, '/all' if is_video else ''),
36
- m_id, query={'apikey': 'EJfK8jdS57GqlupFgAfAAwr573q01y6k'})['data']['episodes'] or []
37
-
38
- def _get_api_info(self, refresh=True):
39
- if not refresh:
40
- return self.cache.load('nhk', 'api_info')
41
-
42
- self.cache.store('nhk', 'api_info', {})
43
- movie_player_js = self._download_webpage(
44
- 'https://movie-a.nhk.or.jp/world/player/js/movie-player.js', None,
45
- note='Downloading stream API information')
46
- api_info = {
47
- 'url': self._search_regex(
48
- r'prod:[^;]+\bapiUrl:\s*[\'"]([^\'"]+)[\'"]', movie_player_js, None, 'stream API url'),
49
- 'token': self._search_regex(
50
- r'prod:[^;]+\btoken:\s*[\'"]([^\'"]+)[\'"]', movie_player_js, None, 'stream API token'),
51
- }
52
- self.cache.store('nhk', 'api_info', api_info)
53
- return api_info
54
-
55
- def _extract_stream_info(self, vod_id):
56
- for refresh in (False, True):
57
- api_info = self._get_api_info(refresh)
58
- if not api_info:
59
- continue
60
-
61
- api_url = api_info.pop('url')
62
- meta = traverse_obj(
63
- self._download_json(
64
- api_url, vod_id, 'Downloading stream url info', fatal=False, query={
65
- **api_info,
66
- 'type': 'json',
67
- 'optional_id': vod_id,
68
- 'active_flg': 1,
69
- }), ('meta', 0))
70
- stream_url = traverse_obj(
71
- meta, ('movie_url', ('mb_auto', 'auto_sp', 'auto_pc'), {url_or_none}), get_all=False)
72
-
73
- if stream_url:
74
- formats, subtitles = self._extract_m3u8_formats_and_subtitles(stream_url, vod_id)
75
- return {
76
- **traverse_obj(meta, {
77
- 'duration': ('duration', {int_or_none}),
78
- 'timestamp': ('publication_date', {unified_timestamp}),
79
- 'release_timestamp': ('insert_date', {unified_timestamp}),
80
- 'modified_timestamp': ('update_date', {unified_timestamp}),
81
- }),
82
- 'formats': formats,
83
- 'subtitles': subtitles,
84
- }
85
- raise ExtractorError('Unable to extract stream url')
40
+ self._API_URL_TEMPLATE.format(
41
+ lang=lang, content_format=content_format, page_type=page_type,
42
+ m_id=m_id, extra_page=extra_page),
43
+ join_nonempty(m_id, lang))
86
44
 
87
45
  def _extract_episode_info(self, url, episode=None):
88
46
  fetch_episode = episode is None
89
47
  lang, m_type, episode_id = NhkVodIE._match_valid_url(url).group('lang', 'type', 'id')
90
48
  is_video = m_type != 'audio'
91
49
 
92
- if is_video:
93
- episode_id = episode_id[:4] + '-' + episode_id[4:]
94
-
95
50
  if fetch_episode:
96
51
  episode = self._call_api(
97
- episode_id, lang, is_video, True, episode_id[:4] == '9999')[0]
52
+ episode_id, lang, is_video, is_episode=True, is_clip=episode_id[:4] == '9999')
98
53
 
99
- def get_clean_field(key):
100
- return clean_html(episode.get(key + '_clean') or episode.get(key))
54
+ video_id = join_nonempty('id', 'lang', from_dict=episode)
101
55
 
102
- title = get_clean_field('sub_title')
103
- series = get_clean_field('title')
104
-
105
- thumbnails = []
106
- for s, w, h in [('', 640, 360), ('_l', 1280, 720)]:
107
- img_path = episode.get('image' + s)
108
- if not img_path:
109
- continue
110
- thumbnails.append({
111
- 'id': f'{h}p',
112
- 'height': h,
113
- 'width': w,
114
- 'url': 'https://www3.nhk.or.jp' + img_path,
115
- })
56
+ title = episode.get('title')
57
+ series = traverse_obj(episode, (('video_program', 'audio_program'), any, 'title'))
116
58
 
117
59
  episode_name = title
118
60
  if series and title:
@@ -125,37 +67,52 @@ class NhkBaseIE(InfoExtractor):
125
67
  episode_name = None
126
68
 
127
69
  info = {
128
- 'id': episode_id + '-' + lang,
70
+ 'id': video_id,
129
71
  'title': title,
130
- 'description': get_clean_field('description'),
131
- 'thumbnails': thumbnails,
132
72
  'series': series,
133
73
  'episode': episode_name,
74
+ **traverse_obj(episode, {
75
+ 'description': ('description', {str}),
76
+ 'release_timestamp': ('first_broadcasted_at', {unified_timestamp}),
77
+ 'categories': ('categories', ..., 'name', {str}),
78
+ 'tags': ('tags', ..., 'name', {str}),
79
+ 'thumbnails': ('images', lambda _, v: v['url'], {
80
+ 'url': ('url', {urljoin(url)}),
81
+ 'width': ('width', {int_or_none}),
82
+ 'height': ('height', {int_or_none}),
83
+ }),
84
+ 'webpage_url': ('url', {urljoin(url)}),
85
+ }),
86
+ 'extractor_key': NhkVodIE.ie_key(),
87
+ 'extractor': NhkVodIE.IE_NAME,
134
88
  }
135
89
 
136
- if is_video:
137
- vod_id = episode['vod_id']
138
- info.update({
139
- **self._extract_stream_info(vod_id),
140
- 'id': vod_id,
141
- })
142
-
90
+ # XXX: We are assuming that 'video' and 'audio' are mutually exclusive
91
+ stream_info = traverse_obj(episode, (('video', 'audio'), {dict}, any)) or {}
92
+ if not stream_info.get('url'):
93
+ self.raise_no_formats('Stream not found; it has most likely expired', expected=True)
143
94
  else:
144
- if fetch_episode:
95
+ stream_url = stream_info['url']
96
+ if is_video:
97
+ formats, subtitles = self._extract_m3u8_formats_and_subtitles(stream_url, video_id)
98
+ info.update({
99
+ 'formats': formats,
100
+ 'subtitles': subtitles,
101
+ **traverse_obj(stream_info, ({
102
+ 'duration': ('duration', {int_or_none}),
103
+ 'timestamp': ('published_at', {unified_timestamp}),
104
+ })),
105
+ })
106
+ else:
145
107
  # From https://www3.nhk.or.jp/nhkworld/common/player/radio/inline/rod.html
146
- audio_path = remove_end(episode['audio']['audio'], '.m4a')
108
+ audio_path = remove_end(stream_url, '.m4a')
147
109
  info['formats'] = self._extract_m3u8_formats(
148
110
  f'{urljoin("https://vod-stream.nhk.jp", audio_path)}/index.m3u8',
149
111
  episode_id, 'm4a', entry_protocol='m3u8_native',
150
112
  m3u8_id='hls', fatal=False)
151
113
  for f in info['formats']:
152
114
  f['language'] = lang
153
- else:
154
- info.update({
155
- '_type': 'url_transparent',
156
- 'ie_key': NhkVodIE.ie_key(),
157
- 'url': url,
158
- })
115
+
159
116
  return info
160
117
 
161
118
 
@@ -168,29 +125,29 @@ class NhkVodIE(NhkBaseIE):
168
125
  # Content available only for a limited period of time. Visit
169
126
  # https://www3.nhk.or.jp/nhkworld/en/ondemand/ for working samples.
170
127
  _TESTS = [{
171
- 'url': 'https://www3.nhk.or.jp/nhkworld/en/ondemand/video/2049126/',
128
+ 'url': 'https://www3.nhk.or.jp/nhkworld/en/shows/2049165/',
172
129
  'info_dict': {
173
- 'id': 'nw_vod_v_en_2049_126_20230413233000_01_1681398302',
130
+ 'id': '2049165-en',
174
131
  'ext': 'mp4',
175
- 'title': 'Japan Railway Journal - The Tohoku Shinkansen: Full Speed Ahead',
176
- 'description': 'md5:49f7c5b206e03868a2fdf0d0814b92f6',
132
+ 'title': 'Japan Railway Journal - Choshi Electric Railway: Fighting to Get Back on Track',
133
+ 'description': 'md5:ab57df2fca7f04245148c2e787bb203d',
177
134
  'thumbnail': r're:https://.+/.+\.jpg',
178
- 'episode': 'The Tohoku Shinkansen: Full Speed Ahead',
135
+ 'episode': 'Choshi Electric Railway: Fighting to Get Back on Track',
179
136
  'series': 'Japan Railway Journal',
180
- 'modified_timestamp': 1707217907,
181
- 'timestamp': 1681428600,
182
- 'release_timestamp': 1693883728,
183
- 'duration': 1679,
184
- 'upload_date': '20230413',
185
- 'modified_date': '20240206',
186
- 'release_date': '20230905',
137
+ 'duration': 1680,
138
+ 'categories': ['Biz & Tech'],
139
+ 'tags': ['Akita', 'Chiba', 'Trains', 'Transcript', 'All (Japan Navigator)'],
140
+ 'timestamp': 1759055880,
141
+ 'upload_date': '20250928',
142
+ 'release_timestamp': 1758810600,
143
+ 'release_date': '20250925',
187
144
  },
188
145
  }, {
189
146
  # video clip
190
147
  'url': 'https://www3.nhk.or.jp/nhkworld/en/ondemand/video/9999011/',
191
148
  'md5': '153c3016dfd252ba09726588149cf0e7',
192
149
  'info_dict': {
193
- 'id': 'lpZXIwaDE6_Z-976CPsFdxyICyWUzlT5',
150
+ 'id': '9999011-en',
194
151
  'ext': 'mp4',
195
152
  'title': 'Dining with the Chef - Chef Saito\'s Family recipe: MENCHI-KATSU',
196
153
  'description': 'md5:5aee4a9f9d81c26281862382103b0ea5',
@@ -198,24 +155,23 @@ class NhkVodIE(NhkBaseIE):
198
155
  'series': 'Dining with the Chef',
199
156
  'episode': 'Chef Saito\'s Family recipe: MENCHI-KATSU',
200
157
  'duration': 148,
201
- 'upload_date': '20190816',
202
- 'release_date': '20230902',
203
- 'release_timestamp': 1693619292,
204
- 'modified_timestamp': 1707217907,
205
- 'modified_date': '20240206',
206
- 'timestamp': 1565997540,
158
+ 'categories': ['Food'],
159
+ 'tags': ['Washoku'],
160
+ 'timestamp': 1548212400,
161
+ 'upload_date': '20190123',
207
162
  },
208
163
  }, {
209
164
  # radio
210
- 'url': 'https://www3.nhk.or.jp/nhkworld/en/ondemand/audio/livinginjapan-20231001-1/',
165
+ 'url': 'https://www3.nhk.or.jp/nhkworld/en/shows/audio/livinginjapan-20240901-1/',
211
166
  'info_dict': {
212
- 'id': 'livinginjapan-20231001-1-en',
167
+ 'id': 'livinginjapan-20240901-1-en',
213
168
  'ext': 'm4a',
214
- 'title': 'Living in Japan - Tips for Travelers to Japan / Ramen Vending Machines',
169
+ 'title': 'Living in Japan - Weekend Hiking / Self-protection from crime',
215
170
  'series': 'Living in Japan',
216
- 'description': 'md5:0a0e2077d8f07a03071e990a6f51bfab',
171
+ 'description': 'md5:4d0e14ab73bdbfedb60a53b093954ed6',
217
172
  'thumbnail': r're:https://.+/.+\.jpg',
218
- 'episode': 'Tips for Travelers to Japan / Ramen Vending Machines',
173
+ 'episode': 'Weekend Hiking / Self-protection from crime',
174
+ 'categories': ['Interactive'],
219
175
  },
220
176
  }, {
221
177
  'url': 'https://www3.nhk.or.jp/nhkworld/en/ondemand/video/2015173/',
@@ -256,96 +212,51 @@ class NhkVodIE(NhkBaseIE):
256
212
  },
257
213
  'skip': 'expires 2023-10-15',
258
214
  }, {
259
- # a one-off (single-episode series). title from the api is just '<p></p>'
260
- 'url': 'https://www3.nhk.or.jp/nhkworld/en/ondemand/video/3004952/',
215
+ # a one-off (single-episode series). title from the api is just null
216
+ 'url': 'https://www3.nhk.or.jp/nhkworld/en/shows/3026036/',
261
217
  'info_dict': {
262
- 'id': 'nw_vod_v_en_3004_952_20230723091000_01_1690074552',
218
+ 'id': '3026036-en',
263
219
  'ext': 'mp4',
264
- 'title': 'Barakan Discovers - AMAMI OSHIMA: Isson\'s Treasure Isla',
265
- 'description': 'md5:5db620c46a0698451cc59add8816b797',
266
- 'thumbnail': r're:https://.+/.+\.jpg',
267
- 'release_date': '20230905',
268
- 'timestamp': 1690103400,
269
- 'duration': 2939,
270
- 'release_timestamp': 1693898699,
271
- 'upload_date': '20230723',
272
- 'modified_timestamp': 1707217907,
273
- 'modified_date': '20240206',
274
- 'episode': 'AMAMI OSHIMA: Isson\'s Treasure Isla',
275
- 'series': 'Barakan Discovers',
220
+ 'title': 'STATELESS: The Japanese Left Behind in the Philippines',
221
+ 'description': 'md5:9a2fd51cdfa9f52baae28569e0053786',
222
+ 'duration': 2955,
223
+ 'thumbnail': 'https://www3.nhk.or.jp/nhkworld/en/shows/3026036/images/wide_l_QPtWpt4lzVhm3NzPAMIIF35MCg4CdNwcikPaTS5Q.jpg',
224
+ 'categories': ['Documentary', 'Culture & Lifestyle'],
225
+ 'tags': ['Transcript', 'Documentary 360', 'The Pursuit of PEACE'],
226
+ 'timestamp': 1758931800,
227
+ 'upload_date': '20250927',
228
+ 'release_timestamp': 1758931800,
229
+ 'release_date': '20250927',
276
230
  },
277
231
  }, {
278
232
  # /ondemand/video/ url with alphabetical character in 5th position of id
279
233
  'url': 'https://www3.nhk.or.jp/nhkworld/en/ondemand/video/9999a07/',
280
234
  'info_dict': {
281
- 'id': 'nw_c_en_9999-a07',
235
+ 'id': '9999a07-en',
282
236
  'ext': 'mp4',
283
237
  'episode': 'Mini-Dramas on SDGs: Ep 1 Close the Gender Gap [Director\'s Cut]',
284
238
  'series': 'Mini-Dramas on SDGs',
285
- 'modified_date': '20240206',
286
239
  'title': 'Mini-Dramas on SDGs - Mini-Dramas on SDGs: Ep 1 Close the Gender Gap [Director\'s Cut]',
287
240
  'description': 'md5:3f9dcb4db22fceb675d90448a040d3f6',
288
- 'timestamp': 1621962360,
289
- 'duration': 189,
290
- 'release_date': '20230903',
291
- 'modified_timestamp': 1707217907,
241
+ 'timestamp': 1621911600,
242
+ 'duration': 190,
292
243
  'upload_date': '20210525',
293
244
  'thumbnail': r're:https://.+/.+\.jpg',
294
- 'release_timestamp': 1693713487,
245
+ 'categories': ['Current Affairs', 'Entertainment'],
295
246
  },
296
247
  }, {
297
248
  'url': 'https://www3.nhk.or.jp/nhkworld/en/ondemand/video/9999d17/',
298
249
  'info_dict': {
299
- 'id': 'nw_c_en_9999-d17',
250
+ 'id': '9999d17-en',
300
251
  'ext': 'mp4',
301
252
  'title': 'Flowers of snow blossom - The 72 Pentads of Yamato',
302
253
  'description': 'Today’s focus: Snow',
303
- 'release_timestamp': 1693792402,
304
- 'release_date': '20230904',
305
- 'upload_date': '20220128',
306
- 'timestamp': 1643370960,
307
254
  'thumbnail': r're:https://.+/.+\.jpg',
308
255
  'duration': 136,
309
- 'series': '',
310
- 'modified_date': '20240206',
311
- 'modified_timestamp': 1707217907,
312
- },
313
- }, {
314
- # new /shows/ url format
315
- 'url': 'https://www3.nhk.or.jp/nhkworld/en/shows/2032307/',
316
- 'info_dict': {
317
- 'id': 'nw_vod_v_en_2032_307_20240321113000_01_1710990282',
318
- 'ext': 'mp4',
319
- 'title': 'Japanology Plus - 20th Anniversary Special Part 1',
320
- 'description': 'md5:817d41fc8e54339ad2a916161ea24faf',
321
- 'episode': '20th Anniversary Special Part 1',
322
- 'series': 'Japanology Plus',
323
- 'thumbnail': r're:https://.+/.+\.jpg',
324
- 'duration': 1680,
325
- 'timestamp': 1711020600,
326
- 'upload_date': '20240321',
327
- 'release_timestamp': 1711022683,
328
- 'release_date': '20240321',
329
- 'modified_timestamp': 1711031012,
330
- 'modified_date': '20240321',
331
- },
332
- }, {
333
- 'url': 'https://www3.nhk.or.jp/nhkworld/en/shows/3020025/',
334
- 'info_dict': {
335
- 'id': 'nw_vod_v_en_3020_025_20230325144000_01_1679723944',
336
- 'ext': 'mp4',
337
- 'title': '100 Ideas to Save the World - Working Styles Evolve',
338
- 'description': 'md5:9e6c7778eaaf4f7b4af83569649f84d9',
339
- 'episode': 'Working Styles Evolve',
340
- 'series': '100 Ideas to Save the World',
341
- 'thumbnail': r're:https://.+/.+\.jpg',
342
- 'duration': 899,
343
- 'upload_date': '20230325',
344
- 'timestamp': 1679755200,
345
- 'release_date': '20230905',
346
- 'release_timestamp': 1693880540,
347
- 'modified_date': '20240206',
348
- 'modified_timestamp': 1707217907,
256
+ 'categories': ['Culture & Lifestyle', 'Science & Nature'],
257
+ 'tags': ['Nara', 'Temples & Shrines', 'Winter', 'Snow'],
258
+ 'timestamp': 1643339040,
259
+ 'upload_date': '20220128',
349
260
  },
350
261
  }, {
351
262
  # new /shows/audio/ url format
@@ -373,6 +284,7 @@ class NhkVodProgramIE(NhkBaseIE):
373
284
  'id': 'sumo',
374
285
  'title': 'GRAND SUMO Highlights',
375
286
  'description': 'md5:fc20d02dc6ce85e4b72e0273aa52fdbf',
287
+ 'series': 'GRAND SUMO Highlights',
376
288
  },
377
289
  'playlist_mincount': 1,
378
290
  }, {
@@ -381,6 +293,7 @@ class NhkVodProgramIE(NhkBaseIE):
381
293
  'id': 'japanrailway',
382
294
  'title': 'Japan Railway Journal',
383
295
  'description': 'md5:ea39d93af7d05835baadf10d1aae0e3f',
296
+ 'series': 'Japan Railway Journal',
384
297
  },
385
298
  'playlist_mincount': 12,
386
299
  }, {
@@ -390,6 +303,7 @@ class NhkVodProgramIE(NhkBaseIE):
390
303
  'id': 'japanrailway',
391
304
  'title': 'Japan Railway Journal',
392
305
  'description': 'md5:ea39d93af7d05835baadf10d1aae0e3f',
306
+ 'series': 'Japan Railway Journal',
393
307
  },
394
308
  'playlist_mincount': 12,
395
309
  }, {
@@ -399,17 +313,9 @@ class NhkVodProgramIE(NhkBaseIE):
399
313
  'id': 'livinginjapan',
400
314
  'title': 'Living in Japan',
401
315
  'description': 'md5:665bb36ec2a12c5a7f598ee713fc2b54',
316
+ 'series': 'Living in Japan',
402
317
  },
403
- 'playlist_mincount': 12,
404
- }, {
405
- # /tv/ program url
406
- 'url': 'https://www3.nhk.or.jp/nhkworld/en/tv/designtalksplus/',
407
- 'info_dict': {
408
- 'id': 'designtalksplus',
409
- 'title': 'DESIGN TALKS plus',
410
- 'description': 'md5:47b3b3a9f10d4ac7b33b53b70a7d2837',
411
- },
412
- 'playlist_mincount': 20,
318
+ 'playlist_mincount': 11,
413
319
  }, {
414
320
  'url': 'https://www3.nhk.or.jp/nhkworld/en/shows/10yearshayaomiyazaki/',
415
321
  'only_matching': True,
@@ -430,9 +336,8 @@ class NhkVodProgramIE(NhkBaseIE):
430
336
  program_id, lang, m_type != 'audio', False, episode_type == 'clip')
431
337
 
432
338
  def entries():
433
- for episode in episodes:
434
- if episode_path := episode.get('url'):
435
- yield self._extract_episode_info(urljoin(url, episode_path), episode)
339
+ for episode in traverse_obj(episodes, ('items', lambda _, v: v['url'])):
340
+ yield self._extract_episode_info(urljoin(url, episode['url']), episode)
436
341
 
437
342
  html = self._download_webpage(url, program_id)
438
343
  program_title = self._extract_meta_from_class_elements([
@@ -446,7 +351,7 @@ class NhkVodProgramIE(NhkBaseIE):
446
351
  'tAudioProgramMain__info', # /shows/audio/programs/
447
352
  'p-program-description'], html) # /tv/
448
353
 
449
- return self.playlist_result(entries(), program_id, program_title, program_description)
354
+ return self.playlist_result(entries(), program_id, program_title, program_description, series=program_title)
450
355
 
451
356
 
452
357
  class NhkForSchoolBangumiIE(InfoExtractor):
@@ -1,21 +1,61 @@
1
1
  from __future__ import annotations
2
+
2
3
  import abc
3
4
  import dataclasses
4
5
  import functools
5
6
  import os.path
7
+ import sys
6
8
 
7
9
  from ._utils import _get_exe_version_output, detect_exe_version, int_or_none
8
10
 
9
11
 
10
- # NOT public API
11
- def runtime_version_tuple(v):
12
+ def _runtime_version_tuple(v):
12
13
  # NB: will return (0,) if `v` is an invalid version string
13
14
  return tuple(int_or_none(x, default=0) for x in v.split('.'))
14
15
 
15
16
 
17
+ _FALLBACK_PATHEXT = ('.COM', '.EXE', '.BAT', '.CMD')
18
+
19
+
20
+ def _find_exe(basename: str) -> str:
21
+ if os.name != 'nt':
22
+ return basename
23
+
24
+ paths: list[str] = []
25
+
26
+ # binary dir
27
+ if getattr(sys, 'frozen', False):
28
+ paths.append(os.path.dirname(sys.executable))
29
+ # cwd
30
+ paths.append(os.getcwd())
31
+ # PATH items
32
+ if path := os.environ.get('PATH'):
33
+ paths.extend(filter(None, path.split(os.path.pathsep)))
34
+
35
+ pathext = os.environ.get('PATHEXT')
36
+ if pathext is None:
37
+ exts = _FALLBACK_PATHEXT
38
+ else:
39
+ exts = tuple(ext for ext in pathext.split(os.pathsep) if ext)
40
+
41
+ visited = []
42
+ for path in map(os.path.realpath, paths):
43
+ normed = os.path.normcase(path)
44
+ if normed in visited:
45
+ continue
46
+ visited.append(normed)
47
+
48
+ for ext in exts:
49
+ binary = os.path.join(path, f'{basename}{ext}')
50
+ if os.access(binary, os.F_OK | os.X_OK) and not os.path.isdir(binary):
51
+ return binary
52
+
53
+ return basename
54
+
55
+
16
56
  def _determine_runtime_path(path, basename):
17
57
  if not path:
18
- return basename
58
+ return _find_exe(basename)
19
59
  if os.path.isdir(path):
20
60
  return os.path.join(path, basename)
21
61
  return path
@@ -52,7 +92,7 @@ class DenoJsRuntime(JsRuntime):
52
92
  if not out:
53
93
  return None
54
94
  version = detect_exe_version(out, r'^deno (\S+)', 'unknown')
55
- vt = runtime_version_tuple(version)
95
+ vt = _runtime_version_tuple(version)
56
96
  return JsRuntimeInfo(
57
97
  name='deno', path=path, version=version, version_tuple=vt,
58
98
  supported=vt >= self.MIN_SUPPORTED_VERSION)
@@ -67,7 +107,7 @@ class BunJsRuntime(JsRuntime):
67
107
  if not out:
68
108
  return None
69
109
  version = detect_exe_version(out, r'^(\S+)', 'unknown')
70
- vt = runtime_version_tuple(version)
110
+ vt = _runtime_version_tuple(version)
71
111
  return JsRuntimeInfo(
72
112
  name='bun', path=path, version=version, version_tuple=vt,
73
113
  supported=vt >= self.MIN_SUPPORTED_VERSION)
@@ -82,7 +122,7 @@ class NodeJsRuntime(JsRuntime):
82
122
  if not out:
83
123
  return None
84
124
  version = detect_exe_version(out, r'^v(\S+)', 'unknown')
85
- vt = runtime_version_tuple(version)
125
+ vt = _runtime_version_tuple(version)
86
126
  return JsRuntimeInfo(
87
127
  name='node', path=path, version=version, version_tuple=vt,
88
128
  supported=vt >= self.MIN_SUPPORTED_VERSION)
@@ -100,7 +140,7 @@ class QuickJsRuntime(JsRuntime):
100
140
  is_ng = 'QuickJS-ng' in out
101
141
 
102
142
  version = detect_exe_version(out, r'^QuickJS(?:-ng)?\s+version\s+(\S+)', 'unknown')
103
- vt = runtime_version_tuple(version.replace('-', '.'))
143
+ vt = _runtime_version_tuple(version.replace('-', '.'))
104
144
  if is_ng:
105
145
  return JsRuntimeInfo(
106
146
  name='quickjs-ng', path=path, version=version, version_tuple=vt,
yt_dlp/utils/_utils.py CHANGED
@@ -876,13 +876,19 @@ class Popen(subprocess.Popen):
876
876
  kwargs.setdefault('encoding', 'utf-8')
877
877
  kwargs.setdefault('errors', 'replace')
878
878
 
879
- if shell and os.name == 'nt' and kwargs.get('executable') is None:
880
- if not isinstance(args, str):
881
- args = shell_quote(args, shell=True)
882
- shell = False
883
- # Set variable for `cmd.exe` newline escaping (see `utils.shell_quote`)
884
- env['='] = '"^\n\n"'
885
- args = f'{self.__comspec()} /Q /S /D /V:OFF /E:ON /C "{args}"'
879
+ if os.name == 'nt' and kwargs.get('executable') is None:
880
+ # Must apply shell escaping if we are trying to run a batch file
881
+ # These conditions should be very specific to limit impact
882
+ if not shell and isinstance(args, list) and args and args[0].lower().endswith(('.bat', '.cmd')):
883
+ shell = True
884
+
885
+ if shell:
886
+ if not isinstance(args, str):
887
+ args = shell_quote(args, shell=True)
888
+ shell = False
889
+ # Set variable for `cmd.exe` newline escaping (see `utils.shell_quote`)
890
+ env['='] = '"^\n\n"'
891
+ args = f'{self.__comspec()} /Q /S /D /V:OFF /E:ON /C "{args}"'
886
892
 
887
893
  super().__init__(args, *remaining, env=env, shell=shell, **kwargs, startupinfo=self._startupinfo)
888
894
 
yt_dlp/version.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # Autogenerated by devscripts/update-version.py
2
2
 
3
- __version__ = '2025.11.23.005251'
3
+ __version__ = '2025.11.24.232953'
4
4
 
5
- RELEASE_GIT_HEAD = '715af0c636b2b33fb3df1eb2ee37eac8262d43ac'
5
+ RELEASE_GIT_HEAD = '12d411722a3d7a0382d1d230a904ecd4e20298b6'
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 = '2025.11.23.005251dev'
15
+ _pkg_version = '2025.11.24.232953dev'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yt-dlp
3
- Version: 2025.11.23.5251.dev0
3
+ Version: 2025.11.24.232953.dev0
4
4
  Summary: A feature-rich command-line audio/video downloader
5
5
  Project-URL: Documentation, https://github.com/yt-dlp/yt-dlp#readme
6
6
  Project-URL: Repository, https://github.com/yt-dlp/yt-dlp
@@ -11,7 +11,7 @@ yt_dlp/options.py,sha256=Icc0JRiKOzITWoMujE_ihEmkCS-uCcie42XhIh8LvS4,100388
11
11
  yt_dlp/plugins.py,sha256=EGmR0ydaahNspGrgszTNX4-YjHe93WOOhcw1gf6PZSs,8215
12
12
  yt_dlp/socks.py,sha256=oAuAfWM6jxI8A5hHDLEKq2U2-k9NyMB_z6nrKzNE9fg,8936
13
13
  yt_dlp/update.py,sha256=sY7gNFBQorzs7sEjRrqL5QOsTBNmGGa_FnpTtbxY1vA,25280
14
- yt_dlp/version.py,sha256=wZtfH01wffjia452sDKTTNzqXADrpdph1Hn2zmdLu6g,360
14
+ yt_dlp/version.py,sha256=WoYAXI3H2HZLNIfzwyZWW-8F3bTEki6fr5do-_qE0YE,360
15
15
  yt_dlp/webvtt.py,sha256=ONkXaaNCZcX8pQhJn3iwIKyaQ34BtVDrMEdG6wRNZwM,11451
16
16
  yt_dlp/__pyinstaller/__init__.py,sha256=-c4Zo8nQGKAm8wc_LDscxMtK7zr_YhZwRnC9CMruUBE,72
17
17
  yt_dlp/__pyinstaller/hook-yt_dlp.py,sha256=5Rd0zV2pDskjY1KtT0wsjxv4hStx67sLCjUexsFvFus,1339
@@ -583,7 +583,7 @@ yt_dlp/extractor/nexx.py,sha256=lIUUkPpjca_jZEQ_3QmDrc8SGJfONn3CHCN92ZjmzD4,2090
583
583
  yt_dlp/extractor/nfb.py,sha256=hojdCNttsF5TSPCOW-Cj8WPdR6Jp-Ak5zj-fwh1uH3E,12645
584
584
  yt_dlp/extractor/nfhsnetwork.py,sha256=4vijxhIcCQxh3Z09-zCkRaB2TNtYJX90ulhUQ2XZxdU,5868
585
585
  yt_dlp/extractor/nfl.py,sha256=p-TvFIjxxJcIo0XsrTuvQsGXP219aytg0dyjAM_rl24,16180
586
- yt_dlp/extractor/nhk.py,sha256=erUh2TVl1FlsfXQUFToBHu7mTm5sCgOJmd264hln0J8,43398
586
+ yt_dlp/extractor/nhk.py,sha256=gMfHgvE9kVdmX7k_QbnFS5mDUtvco3zlic7W_Srbjio,40002
587
587
  yt_dlp/extractor/nhl.py,sha256=4DmHhgYERxwCx0E1xtxC_hQPfVpsqUQ_ZPSU3-48BJM,4905
588
588
  yt_dlp/extractor/nick.py,sha256=J8DjWFvs0BSA-mTCg2pXpNLgZNV2sl2FLVWVY--PwiE,1953
589
589
  yt_dlp/extractor/niconico.py,sha256=NGXnKbiCeY8-0OyBtHcUC_kJXmRkzdlxqB07Z3YswQI,43065
@@ -1114,21 +1114,21 @@ yt_dlp/postprocessor/sponsorblock.py,sha256=McDfxOTwAmtUnD9TxmRqm9Gyf6DRW4W6bBo5
1114
1114
  yt_dlp/postprocessor/xattrpp.py,sha256=PVKZpRXvWj3lHWU8GnyzZKnELjtO7vNYqZkbpJcqZBA,3305
1115
1115
  yt_dlp/utils/__init__.py,sha256=fktzbumix8bd9Xi288JebTYkxCuNhG21qkcSno-3g_s,283
1116
1116
  yt_dlp/utils/_deprecated.py,sha256=5KjqmcPW8uIc77xkhvz1gwxBb-jBF7cwG5nI6xxHebU,1300
1117
- yt_dlp/utils/_jsruntime.py,sha256=tMBSJzoTHLWfY-adS0gDPkCH7B74zGoc-GIL9OHnoS4,3443
1117
+ yt_dlp/utils/_jsruntime.py,sha256=z4_Zx9UIvJMqUDOKPtJLwWKWUJ0JnrkwF8e77k7LI-Y,4477
1118
1118
  yt_dlp/utils/_legacy.py,sha256=hmczdkw5SELzsFcB2AUblAY9bw8gIBDuPFTBlYvXe_4,7858
1119
- yt_dlp/utils/_utils.py,sha256=KrfsoyoF_A9Oj1LQJuJLj3Q7qGUZpGAg9qW-o8tD0QE,190462
1119
+ yt_dlp/utils/_utils.py,sha256=i2Y4abakGQ1_0tl67yTcKvJ41S9OnDbTijMefNOPJAU,190785
1120
1120
  yt_dlp/utils/networking.py,sha256=2GeL1sPpEvQaj_E8J_3Xl-TkalawkmoPCZTwo5akU08,8651
1121
1121
  yt_dlp/utils/progress.py,sha256=t9kVvJ0oWuEqRzo9fdFbIhHUBtO_8mg348QwZ1faqLo,3261
1122
1122
  yt_dlp/utils/traversal.py,sha256=64E3RcZ56iSX50RI_HbKdDNftkETMLBaEPX791_b7yQ,18265
1123
1123
  yt_dlp/utils/jslib/__init__.py,sha256=CbdJiRA7Eh5PnjF2V4lDTcg0J0XjBMaaq0H4pCfq9Tk,87
1124
1124
  yt_dlp/utils/jslib/devalue.py,sha256=7DCGK_zUN0ZeV5hwPT06zaRMUxX_hyUyFWqs79rxw24,5621
1125
- yt_dlp-2025.11.23.5251.dev0.data/data/share/bash-completion/completions/yt-dlp,sha256=b0pb9GLseKD27CjnLE6LlhVxhfmQjmyqV6r_CRbd6ko,5989
1126
- yt_dlp-2025.11.23.5251.dev0.data/data/share/doc/yt_dlp/README.txt,sha256=aO1yTJrp0tKhrdUa5fWmMnVxkrpUi0u3FoYo2uE2rVo,164489
1127
- yt_dlp-2025.11.23.5251.dev0.data/data/share/fish/vendor_completions.d/yt-dlp.fish,sha256=hLa6lZnm7keENpNCjml9A88hbvefdsdoOpAiaNCVyHo,51488
1128
- yt_dlp-2025.11.23.5251.dev0.data/data/share/man/man1/yt-dlp.1,sha256=YyGTf0wqbKUSsZigQ_6pOsAvxMyXNv_pd-6KmDBOrIY,158932
1129
- yt_dlp-2025.11.23.5251.dev0.data/data/share/zsh/site-functions/_yt-dlp,sha256=pNhu8tT4ZKrksLRI2mXLqarzGGhnOlm_hkCBVhSxLzg,5985
1130
- yt_dlp-2025.11.23.5251.dev0.dist-info/METADATA,sha256=ommtPdgOK2FzFmCXYaR-IEWMGk0nDdWeXcSAho6Qkq8,179885
1131
- yt_dlp-2025.11.23.5251.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1132
- yt_dlp-2025.11.23.5251.dev0.dist-info/entry_points.txt,sha256=vWfetvzYgZIwDfMW6BjCe0Cy4pmTZEXRNzxAkfYlRJA,103
1133
- yt_dlp-2025.11.23.5251.dev0.dist-info/licenses/LICENSE,sha256=fhLl30uuEsshWBuhV87SDhmGoFCN0Q0Oikq5pM-U6Fw,1211
1134
- yt_dlp-2025.11.23.5251.dev0.dist-info/RECORD,,
1125
+ yt_dlp-2025.11.24.232953.dev0.data/data/share/bash-completion/completions/yt-dlp,sha256=b0pb9GLseKD27CjnLE6LlhVxhfmQjmyqV6r_CRbd6ko,5989
1126
+ yt_dlp-2025.11.24.232953.dev0.data/data/share/doc/yt_dlp/README.txt,sha256=aO1yTJrp0tKhrdUa5fWmMnVxkrpUi0u3FoYo2uE2rVo,164489
1127
+ yt_dlp-2025.11.24.232953.dev0.data/data/share/fish/vendor_completions.d/yt-dlp.fish,sha256=hLa6lZnm7keENpNCjml9A88hbvefdsdoOpAiaNCVyHo,51488
1128
+ yt_dlp-2025.11.24.232953.dev0.data/data/share/man/man1/yt-dlp.1,sha256=YyGTf0wqbKUSsZigQ_6pOsAvxMyXNv_pd-6KmDBOrIY,158932
1129
+ yt_dlp-2025.11.24.232953.dev0.data/data/share/zsh/site-functions/_yt-dlp,sha256=pNhu8tT4ZKrksLRI2mXLqarzGGhnOlm_hkCBVhSxLzg,5985
1130
+ yt_dlp-2025.11.24.232953.dev0.dist-info/METADATA,sha256=mQdADKJ3MSAjSNgjU3k03t3FRhVOfdoNfykvPH0ZvHs,179887
1131
+ yt_dlp-2025.11.24.232953.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1132
+ yt_dlp-2025.11.24.232953.dev0.dist-info/entry_points.txt,sha256=vWfetvzYgZIwDfMW6BjCe0Cy4pmTZEXRNzxAkfYlRJA,103
1133
+ yt_dlp-2025.11.24.232953.dev0.dist-info/licenses/LICENSE,sha256=fhLl30uuEsshWBuhV87SDhmGoFCN0Q0Oikq5pM-U6Fw,1211
1134
+ yt_dlp-2025.11.24.232953.dev0.dist-info/RECORD,,