yt-dlp 2026.1.25.233128.dev0__py3-none-any.whl → 2026.1.27.233257.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 +9 -2
- yt_dlp/extractor/boosty.py +45 -16
- yt_dlp/extractor/dailymotion.py +45 -5
- yt_dlp/extractor/err.py +68 -0
- yt_dlp/extractor/facebook.py +57 -1
- yt_dlp/extractor/francetv.py +21 -5
- yt_dlp/extractor/frontro.py +4 -4
- yt_dlp/extractor/lazy_extractors.py +39 -7
- yt_dlp/extractor/lbry.py +1 -0
- yt_dlp/extractor/neteasemusic.py +32 -7
- yt_dlp/extractor/patreon.py +118 -33
- yt_dlp/extractor/pbs.py +18 -0
- yt_dlp/extractor/rumble.py +1 -1
- yt_dlp/extractor/volejtv.py +149 -22
- yt_dlp/extractor/wat.py +1 -1
- yt_dlp/version.py +3 -3
- {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/METADATA +1 -1
- {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/RECORD +26 -26
- {yt_dlp-2026.1.25.233128.dev0.data → yt_dlp-2026.1.27.233257.dev0.data}/data/share/bash-completion/completions/yt-dlp +0 -0
- {yt_dlp-2026.1.25.233128.dev0.data → yt_dlp-2026.1.27.233257.dev0.data}/data/share/doc/yt_dlp/README.txt +0 -0
- {yt_dlp-2026.1.25.233128.dev0.data → yt_dlp-2026.1.27.233257.dev0.data}/data/share/fish/vendor_completions.d/yt-dlp.fish +0 -0
- {yt_dlp-2026.1.25.233128.dev0.data → yt_dlp-2026.1.27.233257.dev0.data}/data/share/man/man1/yt-dlp.1 +0 -0
- {yt_dlp-2026.1.25.233128.dev0.data → yt_dlp-2026.1.27.233257.dev0.data}/data/share/zsh/site-functions/_yt-dlp +0 -0
- {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/WHEEL +0 -0
- {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/entry_points.txt +0 -0
- {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/licenses/LICENSE +0 -0
yt_dlp/extractor/patreon.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import itertools
|
|
3
|
-
import urllib.parse
|
|
4
3
|
|
|
5
4
|
from .common import InfoExtractor
|
|
6
5
|
from .sproutvideo import VidsIoIE
|
|
@@ -11,15 +10,23 @@ from ..utils import (
|
|
|
11
10
|
ExtractorError,
|
|
12
11
|
clean_html,
|
|
13
12
|
determine_ext,
|
|
13
|
+
extract_attributes,
|
|
14
|
+
float_or_none,
|
|
14
15
|
int_or_none,
|
|
15
16
|
mimetype2ext,
|
|
16
17
|
parse_iso8601,
|
|
17
18
|
smuggle_url,
|
|
18
19
|
str_or_none,
|
|
20
|
+
update_url_query,
|
|
19
21
|
url_or_none,
|
|
20
22
|
urljoin,
|
|
21
23
|
)
|
|
22
|
-
from ..utils.traversal import
|
|
24
|
+
from ..utils.traversal import (
|
|
25
|
+
find_elements,
|
|
26
|
+
require,
|
|
27
|
+
traverse_obj,
|
|
28
|
+
value,
|
|
29
|
+
)
|
|
23
30
|
|
|
24
31
|
|
|
25
32
|
class PatreonBaseIE(InfoExtractor):
|
|
@@ -121,6 +128,7 @@ class PatreonIE(PatreonBaseIE):
|
|
|
121
128
|
'channel_is_verified': True,
|
|
122
129
|
'chapters': 'count:4',
|
|
123
130
|
'timestamp': 1423689666,
|
|
131
|
+
'media_type': 'video',
|
|
124
132
|
},
|
|
125
133
|
'params': {
|
|
126
134
|
'noplaylist': True,
|
|
@@ -161,7 +169,7 @@ class PatreonIE(PatreonBaseIE):
|
|
|
161
169
|
'uploader_url': 'https://www.patreon.com/loish',
|
|
162
170
|
'description': 'md5:e2693e97ee299c8ece47ffdb67e7d9d2',
|
|
163
171
|
'title': 'VIDEO // sketchbook flipthrough',
|
|
164
|
-
'uploader': 'Loish
|
|
172
|
+
'uploader': 'Loish',
|
|
165
173
|
'tags': ['sketchbook', 'video'],
|
|
166
174
|
'channel_id': '1641751',
|
|
167
175
|
'channel_url': 'https://www.patreon.com/loish',
|
|
@@ -274,8 +282,73 @@ class PatreonIE(PatreonBaseIE):
|
|
|
274
282
|
'channel_id': '9346307',
|
|
275
283
|
},
|
|
276
284
|
'params': {'getcomments': True},
|
|
285
|
+
}, {
|
|
286
|
+
# Inlined media in post; uses _extract_from_media_api
|
|
287
|
+
'url': 'https://www.patreon.com/posts/scottfalco-146966245',
|
|
288
|
+
'info_dict': {
|
|
289
|
+
'id': '146966245',
|
|
290
|
+
'ext': 'mp4',
|
|
291
|
+
'title': 'scottfalco 1080',
|
|
292
|
+
'description': 'md5:a3f29bbd0a46b4821ec3400957c98aa2',
|
|
293
|
+
'uploader': 'Insanimate',
|
|
294
|
+
'uploader_id': '2828146',
|
|
295
|
+
'uploader_url': 'https://www.patreon.com/Insanimate',
|
|
296
|
+
'channel_id': '6260877',
|
|
297
|
+
'channel_url': 'https://www.patreon.com/Insanimate',
|
|
298
|
+
'channel_follower_count': int,
|
|
299
|
+
'comment_count': int,
|
|
300
|
+
'like_count': int,
|
|
301
|
+
'duration': 7.833333,
|
|
302
|
+
'timestamp': 1767061800,
|
|
303
|
+
'upload_date': '20251230',
|
|
304
|
+
},
|
|
277
305
|
}]
|
|
278
306
|
_RETURN_TYPE = 'video'
|
|
307
|
+
_HTTP_HEADERS = {
|
|
308
|
+
# Must be all-lowercase 'referer' so we can smuggle it to Generic, SproutVideo, and Vimeo.
|
|
309
|
+
# patreon.com URLs redirect to www.patreon.com; this matters when requesting mux.com m3u8s
|
|
310
|
+
'referer': 'https://www.patreon.com/',
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
def _extract_from_media_api(self, media_id):
|
|
314
|
+
attributes = traverse_obj(
|
|
315
|
+
self._call_api(f'media/{media_id}', media_id, fatal=False),
|
|
316
|
+
('data', 'attributes', {dict}))
|
|
317
|
+
if not attributes:
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
info_dict = traverse_obj(attributes, {
|
|
321
|
+
'title': ('file_name', {lambda x: x.rpartition('.')[0]}),
|
|
322
|
+
'timestamp': ('created_at', {parse_iso8601}),
|
|
323
|
+
'duration': ('display', 'duration', {float_or_none}),
|
|
324
|
+
})
|
|
325
|
+
info_dict['id'] = media_id
|
|
326
|
+
|
|
327
|
+
playback_url = traverse_obj(
|
|
328
|
+
attributes, ('display', (None, 'viewer_playback_data'), 'url', {url_or_none}, any))
|
|
329
|
+
download_url = traverse_obj(attributes, ('download_url', {url_or_none}))
|
|
330
|
+
|
|
331
|
+
if playback_url and mimetype2ext(attributes.get('mimetype')) == 'm3u8':
|
|
332
|
+
info_dict['formats'], info_dict['subtitles'] = self._extract_m3u8_formats_and_subtitles(
|
|
333
|
+
playback_url, media_id, 'mp4', fatal=False, headers=self._HTTP_HEADERS)
|
|
334
|
+
for f in info_dict['formats']:
|
|
335
|
+
f['http_headers'] = self._HTTP_HEADERS
|
|
336
|
+
if transcript_url := traverse_obj(attributes, ('display', 'transcript_url', {url_or_none})):
|
|
337
|
+
info_dict['subtitles'].setdefault('en', []).append({
|
|
338
|
+
'url': transcript_url,
|
|
339
|
+
'ext': 'vtt',
|
|
340
|
+
})
|
|
341
|
+
elif playback_url or download_url:
|
|
342
|
+
info_dict['formats'] = [{
|
|
343
|
+
# If playback_url is available, download_url is a duplicate lower resolution format
|
|
344
|
+
'url': playback_url or download_url,
|
|
345
|
+
'vcodec': 'none' if attributes.get('media_type') != 'video' else None,
|
|
346
|
+
}]
|
|
347
|
+
|
|
348
|
+
if not info_dict.get('formats'):
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
return info_dict
|
|
279
352
|
|
|
280
353
|
def _real_extract(self, url):
|
|
281
354
|
video_id = self._match_id(url)
|
|
@@ -299,6 +372,7 @@ class PatreonIE(PatreonBaseIE):
|
|
|
299
372
|
'comment_count': ('comment_count', {int_or_none}),
|
|
300
373
|
})
|
|
301
374
|
|
|
375
|
+
seen_media_ids = set()
|
|
302
376
|
entries = []
|
|
303
377
|
idx = 0
|
|
304
378
|
for include in traverse_obj(post, ('included', lambda _, v: v['type'])):
|
|
@@ -320,6 +394,8 @@ class PatreonIE(PatreonBaseIE):
|
|
|
320
394
|
'url': download_url,
|
|
321
395
|
'alt_title': traverse_obj(media_attributes, ('file_name', {str})),
|
|
322
396
|
})
|
|
397
|
+
if media_id := traverse_obj(include, ('id', {str})):
|
|
398
|
+
seen_media_ids.add(media_id)
|
|
323
399
|
|
|
324
400
|
elif include_type == 'user':
|
|
325
401
|
info.update(traverse_obj(include, {
|
|
@@ -340,34 +416,29 @@ class PatreonIE(PatreonBaseIE):
|
|
|
340
416
|
'channel_follower_count': ('attributes', 'patron_count', {int_or_none}),
|
|
341
417
|
}))
|
|
342
418
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
'description', self._webpage_read_content(urlh, embed_url, video_id, fatal=False), default=None))
|
|
367
|
-
# Password-protected vids.io embeds return 403 errors w/o --video-password or session cookie
|
|
368
|
-
if ((urlh.status != 403 and meta_description != 'Sorry, we couldn’t find that page')
|
|
369
|
-
or VidsIoIE.suitable(embed_url)):
|
|
370
|
-
entries.append(self.url_result(smuggle_url(embed_url, headers)))
|
|
419
|
+
if embed_url := traverse_obj(attributes, ('embed', 'url', {url_or_none})):
|
|
420
|
+
# Convert useless vimeo.com URLs to useful player.vimeo.com embed URLs
|
|
421
|
+
vimeo_id, vimeo_hash = self._search_regex(
|
|
422
|
+
r'//vimeo\.com/(\d+)(?:/([\da-f]+))?', embed_url,
|
|
423
|
+
'vimeo id', group=(1, 2), default=(None, None))
|
|
424
|
+
if vimeo_id:
|
|
425
|
+
embed_url = update_url_query(
|
|
426
|
+
f'https://player.vimeo.com/video/{vimeo_id}',
|
|
427
|
+
{'h': vimeo_hash or []})
|
|
428
|
+
if VimeoIE.suitable(embed_url):
|
|
429
|
+
entry = self.url_result(
|
|
430
|
+
VimeoIE._smuggle_referrer(embed_url, self._HTTP_HEADERS['referer']),
|
|
431
|
+
VimeoIE, url_transparent=True)
|
|
432
|
+
else:
|
|
433
|
+
entry = self.url_result(smuggle_url(embed_url, self._HTTP_HEADERS))
|
|
434
|
+
|
|
435
|
+
if urlh := self._request_webpage(
|
|
436
|
+
embed_url, video_id, 'Checking embed URL', headers=self._HTTP_HEADERS,
|
|
437
|
+
fatal=False, errnote=False, expected_status=(403, 429), # Ignore Vimeo 429's
|
|
438
|
+
):
|
|
439
|
+
# Password-protected vids.io embeds return 403 errors w/o --video-password or session cookie
|
|
440
|
+
if VidsIoIE.suitable(embed_url) or urlh.status != 403:
|
|
441
|
+
entries.append(entry)
|
|
371
442
|
|
|
372
443
|
post_file = traverse_obj(attributes, ('post_file', {dict}))
|
|
373
444
|
if post_file:
|
|
@@ -381,13 +452,27 @@ class PatreonIE(PatreonBaseIE):
|
|
|
381
452
|
})
|
|
382
453
|
elif name == 'video' or determine_ext(post_file.get('url')) == 'm3u8':
|
|
383
454
|
formats, subtitles = self._extract_m3u8_formats_and_subtitles(
|
|
384
|
-
post_file['url'], video_id, headers=
|
|
455
|
+
post_file['url'], video_id, headers=self._HTTP_HEADERS)
|
|
456
|
+
for f in formats:
|
|
457
|
+
f['http_headers'] = self._HTTP_HEADERS
|
|
385
458
|
entries.append({
|
|
386
459
|
'id': video_id,
|
|
387
460
|
'formats': formats,
|
|
388
461
|
'subtitles': subtitles,
|
|
389
|
-
'http_headers': headers,
|
|
390
462
|
})
|
|
463
|
+
if media_id := traverse_obj(post_file, ('media_id', {int}, {str_or_none})):
|
|
464
|
+
seen_media_ids.add(media_id)
|
|
465
|
+
|
|
466
|
+
for media_id in traverse_obj(attributes, (
|
|
467
|
+
'content', {find_elements(attr='data-media-id', value=r'\d+', regex=True, html=True)},
|
|
468
|
+
..., {extract_attributes}, 'data-media-id',
|
|
469
|
+
)):
|
|
470
|
+
# Inlined media may be duplicates of what was extracted above
|
|
471
|
+
if media_id in seen_media_ids:
|
|
472
|
+
continue
|
|
473
|
+
if media := self._extract_from_media_api(media_id):
|
|
474
|
+
entries.append(media)
|
|
475
|
+
seen_media_ids.add(media_id)
|
|
391
476
|
|
|
392
477
|
can_view_post = traverse_obj(attributes, 'current_user_can_view')
|
|
393
478
|
comments = None
|
yt_dlp/extractor/pbs.py
CHANGED
|
@@ -453,6 +453,23 @@ class PBSIE(InfoExtractor):
|
|
|
453
453
|
'url': 'https://player.pbs.org/portalplayer/3004638221/?uid=',
|
|
454
454
|
'only_matching': True,
|
|
455
455
|
},
|
|
456
|
+
{
|
|
457
|
+
# Next.js v13+, see https://github.com/yt-dlp/yt-dlp/issues/13299
|
|
458
|
+
'url': 'https://www.pbs.org/video/caregiving',
|
|
459
|
+
'info_dict': {
|
|
460
|
+
'id': '3101776876',
|
|
461
|
+
'ext': 'mp4',
|
|
462
|
+
'title': 'Caregiving - Caregiving',
|
|
463
|
+
'description': 'A documentary revealing America’s caregiving crisis through intimate stories and expert insight.',
|
|
464
|
+
'display_id': 'caregiving',
|
|
465
|
+
'duration': 6783,
|
|
466
|
+
'thumbnail': 'https://image.pbs.org/video-assets/BSrSkcc-asset-mezzanine-16x9-nlcxQts.jpg',
|
|
467
|
+
'chapters': [],
|
|
468
|
+
},
|
|
469
|
+
'params': {
|
|
470
|
+
'skip_download': True,
|
|
471
|
+
},
|
|
472
|
+
},
|
|
456
473
|
]
|
|
457
474
|
_ERRORS = {
|
|
458
475
|
101: 'We\'re sorry, but this video is not yet available.',
|
|
@@ -506,6 +523,7 @@ class PBSIE(InfoExtractor):
|
|
|
506
523
|
r"(?s)window\.PBS\.playerConfig\s*=\s*{.*?id\s*:\s*'([0-9]+)',",
|
|
507
524
|
r'<div[^>]+\bdata-cove-id=["\'](\d+)"', # http://www.pbs.org/wgbh/roadshow/watch/episode/2105-indianapolis-hour-2/
|
|
508
525
|
r'<iframe[^>]+\bsrc=["\'](?:https?:)?//video\.pbs\.org/widget/partnerplayer/(\d+)', # https://www.pbs.org/wgbh/masterpiece/episodes/victoria-s2-e1/
|
|
526
|
+
r'\\"videoTPMediaId\\":\\\"(\d+)\\"', # Next.js v13, e.g. https://www.pbs.org/video/caregiving
|
|
509
527
|
r'\bhttps?://player\.pbs\.org/[\w-]+player/(\d+)', # last pattern to avoid false positives
|
|
510
528
|
]
|
|
511
529
|
|
yt_dlp/extractor/rumble.py
CHANGED
|
@@ -405,7 +405,7 @@ class RumbleChannelIE(InfoExtractor):
|
|
|
405
405
|
for video_url in traverse_obj(
|
|
406
406
|
get_elements_html_by_class('videostream__link', webpage), (..., {extract_attributes}, 'href'),
|
|
407
407
|
):
|
|
408
|
-
yield self.url_result(urljoin('https://rumble.com', video_url))
|
|
408
|
+
yield self.url_result(urljoin('https://rumble.com', video_url), RumbleIE)
|
|
409
409
|
|
|
410
410
|
def _real_extract(self, url):
|
|
411
411
|
url, playlist_id = self._match_valid_url(url).groups()
|
yt_dlp/extractor/volejtv.py
CHANGED
|
@@ -1,40 +1,167 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
1
3
|
from .common import InfoExtractor
|
|
4
|
+
from ..utils import (
|
|
5
|
+
InAdvancePagedList,
|
|
6
|
+
int_or_none,
|
|
7
|
+
join_nonempty,
|
|
8
|
+
orderedSet,
|
|
9
|
+
str_or_none,
|
|
10
|
+
strftime_or_none,
|
|
11
|
+
unified_timestamp,
|
|
12
|
+
url_or_none,
|
|
13
|
+
)
|
|
14
|
+
from ..utils.traversal import (
|
|
15
|
+
require,
|
|
16
|
+
traverse_obj,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VolejTVBaseIE(InfoExtractor):
|
|
21
|
+
TBR_HEIGHT_MAPPING = {
|
|
22
|
+
'6000': 1080,
|
|
23
|
+
'2400': 720,
|
|
24
|
+
'1500': 480,
|
|
25
|
+
'800': 360,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def _call_api(self, endpoint, display_id, query=None):
|
|
29
|
+
return self._download_json(
|
|
30
|
+
f'https://api-volejtv-prod.apps.okd4.devopsie.cloud/api/{endpoint}',
|
|
31
|
+
display_id, query=query)
|
|
2
32
|
|
|
3
33
|
|
|
4
|
-
class VolejTVIE(
|
|
5
|
-
|
|
34
|
+
class VolejTVIE(VolejTVBaseIE):
|
|
35
|
+
IE_NAME = 'volejtv:match'
|
|
36
|
+
_VALID_URL = r'https?://volej\.tv/match/(?P<id>\d+)'
|
|
6
37
|
_TESTS = [{
|
|
7
|
-
'url': 'https://volej.tv/
|
|
38
|
+
'url': 'https://volej.tv/match/270579',
|
|
8
39
|
'info_dict': {
|
|
9
|
-
'id': '
|
|
40
|
+
'id': '270579',
|
|
10
41
|
'ext': 'mp4',
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
42
|
+
'title': 'SWE-CZE (2024-06-16)',
|
|
43
|
+
'categories': ['ženy'],
|
|
44
|
+
'series': 'ZLATÁ EVROPSKÁ VOLEJBALOVÁ LIGA',
|
|
45
|
+
'season': '2023-2024',
|
|
46
|
+
'timestamp': 1718553600,
|
|
47
|
+
'upload_date': '20240616',
|
|
14
48
|
},
|
|
15
49
|
}, {
|
|
16
|
-
'url': 'https://volej.tv/
|
|
50
|
+
'url': 'https://volej.tv/match/487520',
|
|
17
51
|
'info_dict': {
|
|
18
|
-
'id': '
|
|
52
|
+
'id': '487520',
|
|
19
53
|
'ext': 'mp4',
|
|
20
|
-
'thumbnail': 'https
|
|
21
|
-
'title': '
|
|
22
|
-
'
|
|
54
|
+
'thumbnail': r're:https://.+\.(png|jpeg)',
|
|
55
|
+
'title': 'FRA-CZE (2024-09-06)',
|
|
56
|
+
'categories': ['mládež'],
|
|
57
|
+
'series': 'Mistrovství Evropy do 20 let',
|
|
58
|
+
'season': '2024-2025',
|
|
59
|
+
'timestamp': 1725627600,
|
|
60
|
+
'upload_date': '20240906',
|
|
61
|
+
|
|
23
62
|
},
|
|
24
63
|
}]
|
|
25
64
|
|
|
26
65
|
def _real_extract(self, url):
|
|
27
66
|
video_id = self._match_id(url)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
67
|
+
json_data = self._call_api(f'match/{video_id}', video_id)
|
|
68
|
+
|
|
69
|
+
formats = []
|
|
70
|
+
for video in traverse_obj(json_data, ('videos', 0, 'qualities', lambda _, v: url_or_none(v['cloud_front_path']))):
|
|
71
|
+
formats.append(traverse_obj(video, {
|
|
72
|
+
'url': 'cloud_front_path',
|
|
73
|
+
'tbr': ('quality', {int_or_none}),
|
|
74
|
+
'format_id': ('id', {str_or_none}),
|
|
75
|
+
'height': ('quality', {self.TBR_HEIGHT_MAPPING.get}),
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
data = {
|
|
34
79
|
'id': video_id,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
80
|
+
**traverse_obj(json_data, {
|
|
81
|
+
'series': ('competition_name', {str}),
|
|
82
|
+
'season': ('season', {str}),
|
|
83
|
+
'timestamp': ('match_time', {unified_timestamp}),
|
|
84
|
+
'categories': ('category', ('title'), {str}, filter, all, filter),
|
|
85
|
+
'thumbnail': ('poster', {url_or_none}),
|
|
86
|
+
}),
|
|
38
87
|
'formats': formats,
|
|
39
|
-
'subtitles': subtitle,
|
|
40
88
|
}
|
|
89
|
+
|
|
90
|
+
teams = orderedSet(traverse_obj(json_data, ('teams', ..., 'shortcut', {str})))
|
|
91
|
+
if len(teams) > 2 and 'FIN' in teams:
|
|
92
|
+
teams.remove('FIN')
|
|
93
|
+
|
|
94
|
+
data['title'] = join_nonempty(
|
|
95
|
+
join_nonempty(*teams, delim='-'),
|
|
96
|
+
strftime_or_none(data.get('timestamp'), '(%Y-%m-%d)'),
|
|
97
|
+
delim=' ')
|
|
98
|
+
|
|
99
|
+
return data
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class VolejTVPlaylistBaseIE(VolejTVBaseIE):
|
|
103
|
+
"""Subclasses must set _API_FILTER, _PAGE_SIZE"""
|
|
104
|
+
|
|
105
|
+
def _get_page(self, playlist_id, page):
|
|
106
|
+
return self._call_api(
|
|
107
|
+
f'match/{self._API_FILTER}/{playlist_id}', playlist_id,
|
|
108
|
+
query={'page': page + 1, 'take': self._PAGE_SIZE, 'order': 'DESC'})
|
|
109
|
+
|
|
110
|
+
def _entries(self, playlist_id, first_page_data, page):
|
|
111
|
+
entries = first_page_data if page == 0 else self._get_page(playlist_id, page)
|
|
112
|
+
for match_id in traverse_obj(entries, ('data', ..., 'id')):
|
|
113
|
+
yield self.url_result(f'https://volej.tv/match/{match_id}', VolejTVIE)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class VolejTVClubPlaylistIE(VolejTVPlaylistBaseIE):
|
|
117
|
+
IE_NAME = 'volejtv:club'
|
|
118
|
+
_VALID_URL = r'https?://volej\.tv/klub/(?P<id>\d+)'
|
|
119
|
+
_TESTS = [{
|
|
120
|
+
'url': 'https://volej.tv/klub/1173',
|
|
121
|
+
'info_dict': {
|
|
122
|
+
'id': '1173',
|
|
123
|
+
'title': 'VK Jihostroj České Budějovice',
|
|
124
|
+
},
|
|
125
|
+
'playlist_mincount': 30,
|
|
126
|
+
}]
|
|
127
|
+
_API_FILTER = 'by-team-id-paginated'
|
|
128
|
+
_PAGE_SIZE = 6
|
|
129
|
+
|
|
130
|
+
def _real_extract(self, url):
|
|
131
|
+
playlist_id = self._match_id(url)
|
|
132
|
+
title = self._call_api(f'team/show/{playlist_id}', playlist_id)['title']
|
|
133
|
+
first_page_data = self._get_page(playlist_id, 0)
|
|
134
|
+
total_pages = traverse_obj(first_page_data, ('meta', 'pageCount', {int}, {require('page count')}))
|
|
135
|
+
return self.playlist_result(InAdvancePagedList(
|
|
136
|
+
functools.partial(self._entries, playlist_id, first_page_data),
|
|
137
|
+
total_pages, self._PAGE_SIZE), playlist_id, title)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class VolejTVCategoryPlaylistIE(VolejTVPlaylistBaseIE):
|
|
141
|
+
IE_NAME = 'volejtv:category'
|
|
142
|
+
_VALID_URL = r'https?://volej\.tv/kategorie/(?P<id>[^/$?]+)'
|
|
143
|
+
_TESTS = [{
|
|
144
|
+
'url': 'https://volej.tv/kategorie/chance-cesky-pohar',
|
|
145
|
+
'info_dict': {
|
|
146
|
+
'id': 'chance-cesky-pohar',
|
|
147
|
+
'title': 'Chance Český pohár',
|
|
148
|
+
},
|
|
149
|
+
'playlist_mincount': 30,
|
|
150
|
+
}]
|
|
151
|
+
_API_FILTER = 'by-category-id-paginated'
|
|
152
|
+
_PAGE_SIZE = 10
|
|
153
|
+
|
|
154
|
+
def _get_category(self, playlist_id):
|
|
155
|
+
categories = self._call_api('category', playlist_id)
|
|
156
|
+
for category in traverse_obj(categories, (lambda _, v: v['slug'] and v['id'] and v['title'])):
|
|
157
|
+
if category['slug'] == playlist_id:
|
|
158
|
+
return category['id'], category['title']
|
|
159
|
+
|
|
160
|
+
def _real_extract(self, url):
|
|
161
|
+
playlist_id = self._match_id(url)
|
|
162
|
+
category_id, title = self._get_category(playlist_id)
|
|
163
|
+
first_page_data = self._get_page(category_id, 0)
|
|
164
|
+
total_pages = traverse_obj(first_page_data, ('meta', 'pageCount', {int}, {require('page count')}))
|
|
165
|
+
return self.playlist_result(InAdvancePagedList(
|
|
166
|
+
functools.partial(self._entries, category_id, first_page_data),
|
|
167
|
+
total_pages, self._PAGE_SIZE), playlist_id, title)
|
yt_dlp/extractor/wat.py
CHANGED
|
@@ -76,7 +76,7 @@ class WatIE(InfoExtractor):
|
|
|
76
76
|
if error_code == 'GEOBLOCKED':
|
|
77
77
|
self.raise_geo_restricted(error_desc, video_info.get('geoList'))
|
|
78
78
|
elif error_code == 'DELIVERY_ERROR':
|
|
79
|
-
if traverse_obj(video_data, ('delivery', 'code'))
|
|
79
|
+
if traverse_obj(video_data, ('delivery', 'code')) in (403, 500):
|
|
80
80
|
self.report_drm(video_id)
|
|
81
81
|
error_desc = join_nonempty(
|
|
82
82
|
error_desc, traverse_obj(video_data, ('delivery', 'error', {str})), delim=': ')
|
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.27.233257'
|
|
4
4
|
|
|
5
|
-
RELEASE_GIT_HEAD = '
|
|
5
|
+
RELEASE_GIT_HEAD = '1c739bf53e673e06d2a43feddb5a31ee8496fa6e'
|
|
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.27.233257dev'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yt-dlp
|
|
3
|
-
Version: 2026.1.
|
|
3
|
+
Version: 2026.1.27.233257.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
|