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 +104 -199
- yt_dlp/utils/_jsruntime.py +47 -7
- yt_dlp/utils/_utils.py +13 -7
- yt_dlp/version.py +3 -3
- {yt_dlp-2025.11.23.5251.dev0.dist-info → yt_dlp-2025.11.24.232953.dev0.dist-info}/METADATA +1 -1
- {yt_dlp-2025.11.23.5251.dev0.dist-info → yt_dlp-2025.11.24.232953.dev0.dist-info}/RECORD +14 -14
- {yt_dlp-2025.11.23.5251.dev0.data → yt_dlp-2025.11.24.232953.dev0.data}/data/share/bash-completion/completions/yt-dlp +0 -0
- {yt_dlp-2025.11.23.5251.dev0.data → yt_dlp-2025.11.24.232953.dev0.data}/data/share/doc/yt_dlp/README.txt +0 -0
- {yt_dlp-2025.11.23.5251.dev0.data → yt_dlp-2025.11.24.232953.dev0.data}/data/share/fish/vendor_completions.d/yt-dlp.fish +0 -0
- {yt_dlp-2025.11.23.5251.dev0.data → yt_dlp-2025.11.24.232953.dev0.data}/data/share/man/man1/yt-dlp.1 +0 -0
- {yt_dlp-2025.11.23.5251.dev0.data → yt_dlp-2025.11.24.232953.dev0.data}/data/share/zsh/site-functions/_yt-dlp +0 -0
- {yt_dlp-2025.11.23.5251.dev0.dist-info → yt_dlp-2025.11.24.232953.dev0.dist-info}/WHEEL +0 -0
- {yt_dlp-2025.11.23.5251.dev0.dist-info → yt_dlp-2025.11.24.232953.dev0.dist-info}/entry_points.txt +0 -0
- {yt_dlp-2025.11.23.5251.dev0.dist-info → yt_dlp-2025.11.24.232953.dev0.dist-info}/licenses/LICENSE +0 -0
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://
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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')
|
|
52
|
+
episode_id, lang, is_video, is_episode=True, is_clip=episode_id[:4] == '9999')
|
|
98
53
|
|
|
99
|
-
|
|
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 =
|
|
103
|
-
series =
|
|
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':
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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/
|
|
128
|
+
'url': 'https://www3.nhk.or.jp/nhkworld/en/shows/2049165/',
|
|
172
129
|
'info_dict': {
|
|
173
|
-
'id': '
|
|
130
|
+
'id': '2049165-en',
|
|
174
131
|
'ext': 'mp4',
|
|
175
|
-
'title': 'Japan Railway Journal -
|
|
176
|
-
'description': 'md5:
|
|
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': '
|
|
135
|
+
'episode': 'Choshi Electric Railway: Fighting to Get Back on Track',
|
|
179
136
|
'series': 'Japan Railway Journal',
|
|
180
|
-
'
|
|
181
|
-
'
|
|
182
|
-
'
|
|
183
|
-
'
|
|
184
|
-
'upload_date': '
|
|
185
|
-
'
|
|
186
|
-
'release_date': '
|
|
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': '
|
|
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
|
-
'
|
|
202
|
-
'
|
|
203
|
-
'
|
|
204
|
-
'
|
|
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/
|
|
165
|
+
'url': 'https://www3.nhk.or.jp/nhkworld/en/shows/audio/livinginjapan-20240901-1/',
|
|
211
166
|
'info_dict': {
|
|
212
|
-
'id': 'livinginjapan-
|
|
167
|
+
'id': 'livinginjapan-20240901-1-en',
|
|
213
168
|
'ext': 'm4a',
|
|
214
|
-
'title': 'Living in Japan -
|
|
169
|
+
'title': 'Living in Japan - Weekend Hiking / Self-protection from crime',
|
|
215
170
|
'series': 'Living in Japan',
|
|
216
|
-
'description': 'md5:
|
|
171
|
+
'description': 'md5:4d0e14ab73bdbfedb60a53b093954ed6',
|
|
217
172
|
'thumbnail': r're:https://.+/.+\.jpg',
|
|
218
|
-
'episode': '
|
|
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
|
|
260
|
-
'url': 'https://www3.nhk.or.jp/nhkworld/en/
|
|
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': '
|
|
218
|
+
'id': '3026036-en',
|
|
263
219
|
'ext': 'mp4',
|
|
264
|
-
'title': '
|
|
265
|
-
'description': 'md5:
|
|
266
|
-
'
|
|
267
|
-
'
|
|
268
|
-
'
|
|
269
|
-
'
|
|
270
|
-
'
|
|
271
|
-
'upload_date': '
|
|
272
|
-
'
|
|
273
|
-
'
|
|
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': '
|
|
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':
|
|
289
|
-
'duration':
|
|
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
|
-
'
|
|
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': '
|
|
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
|
-
'
|
|
310
|
-
'
|
|
311
|
-
'
|
|
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':
|
|
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
|
-
|
|
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):
|
yt_dlp/utils/_jsruntime.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
880
|
-
if
|
|
881
|
-
|
|
882
|
-
shell
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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.
|
|
3
|
+
__version__ = '2025.11.24.232953'
|
|
4
4
|
|
|
5
|
-
RELEASE_GIT_HEAD = '
|
|
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.
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
1126
|
-
yt_dlp-2025.11.
|
|
1127
|
-
yt_dlp-2025.11.
|
|
1128
|
-
yt_dlp-2025.11.
|
|
1129
|
-
yt_dlp-2025.11.
|
|
1130
|
-
yt_dlp-2025.11.
|
|
1131
|
-
yt_dlp-2025.11.
|
|
1132
|
-
yt_dlp-2025.11.
|
|
1133
|
-
yt_dlp-2025.11.
|
|
1134
|
-
yt_dlp-2025.11.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{yt_dlp-2025.11.23.5251.dev0.data → yt_dlp-2025.11.24.232953.dev0.data}/data/share/man/man1/yt-dlp.1
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{yt_dlp-2025.11.23.5251.dev0.dist-info → yt_dlp-2025.11.24.232953.dev0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{yt_dlp-2025.11.23.5251.dev0.dist-info → yt_dlp-2025.11.24.232953.dev0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|