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.
Files changed (26) hide show
  1. yt_dlp/extractor/_extractors.py +9 -2
  2. yt_dlp/extractor/boosty.py +45 -16
  3. yt_dlp/extractor/dailymotion.py +45 -5
  4. yt_dlp/extractor/err.py +68 -0
  5. yt_dlp/extractor/facebook.py +57 -1
  6. yt_dlp/extractor/francetv.py +21 -5
  7. yt_dlp/extractor/frontro.py +4 -4
  8. yt_dlp/extractor/lazy_extractors.py +39 -7
  9. yt_dlp/extractor/lbry.py +1 -0
  10. yt_dlp/extractor/neteasemusic.py +32 -7
  11. yt_dlp/extractor/patreon.py +118 -33
  12. yt_dlp/extractor/pbs.py +18 -0
  13. yt_dlp/extractor/rumble.py +1 -1
  14. yt_dlp/extractor/volejtv.py +149 -22
  15. yt_dlp/extractor/wat.py +1 -1
  16. yt_dlp/version.py +3 -3
  17. {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/METADATA +1 -1
  18. {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/RECORD +26 -26
  19. {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
  20. {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
  21. {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
  22. {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
  23. {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
  24. {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/WHEEL +0 -0
  25. {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/entry_points.txt +0 -0
  26. {yt_dlp-2026.1.25.233128.dev0.dist-info → yt_dlp-2026.1.27.233257.dev0.dist-info}/licenses/LICENSE +0 -0
@@ -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 require, traverse_obj, value
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
- # Must be all-lowercase 'referer' so we can smuggle it to Generic, SproutVideo, and Vimeo.
344
- # patreon.com URLs redirect to www.patreon.com; this matters when requesting mux.com m3u8s
345
- headers = {'referer': 'https://www.patreon.com/'}
346
-
347
- # handle Vimeo embeds
348
- if traverse_obj(attributes, ('embed', 'provider')) == 'Vimeo':
349
- v_url = urllib.parse.unquote(self._html_search_regex(
350
- r'(https(?:%3A%2F%2F|://)player\.vimeo\.com.+app_id(?:=|%3D)+\d+)',
351
- traverse_obj(attributes, ('embed', 'html', {str})), 'vimeo url', fatal=False) or '')
352
- if url_or_none(v_url) and self._request_webpage(
353
- v_url, video_id, 'Checking Vimeo embed URL', headers=headers,
354
- fatal=False, errnote=False, expected_status=429): # 429 is TLS fingerprint rejection
355
- entries.append(self.url_result(
356
- VimeoIE._smuggle_referrer(v_url, headers['referer']),
357
- VimeoIE, url_transparent=True))
358
-
359
- embed_url = traverse_obj(attributes, ('embed', 'url', {url_or_none}))
360
- if embed_url and (urlh := self._request_webpage(
361
- embed_url, video_id, 'Checking embed URL', headers=headers,
362
- fatal=False, errnote=False, expected_status=403)):
363
- # Vimeo's Cloudflare anti-bot protection will return HTTP status 200 for 404, so we need
364
- # to check for "Sorry, we couldn’t find that page" in the meta description tag
365
- meta_description = clean_html(self._html_search_meta(
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=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
 
@@ -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()
@@ -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(InfoExtractor):
5
- _VALID_URL = r'https?://volej\.tv/video/(?P<id>\d+)'
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/video/725742/',
38
+ 'url': 'https://volej.tv/match/270579',
8
39
  'info_dict': {
9
- 'id': '725742',
40
+ 'id': '270579',
10
41
  'ext': 'mp4',
11
- 'description': 'Zápas VK Královo Pole vs VK Prostějov 10.12.2022 v 19:00 na Volej.TV',
12
- 'thumbnail': 'https://volej.tv/images/og/16/17186/og.png',
13
- 'title': 'VK Královo Pole vs VK Prostějov',
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/video/725605/',
50
+ 'url': 'https://volej.tv/match/487520',
17
51
  'info_dict': {
18
- 'id': '725605',
52
+ 'id': '487520',
19
53
  'ext': 'mp4',
20
- 'thumbnail': 'https://volej.tv/images/og/15/17185/og.png',
21
- 'title': 'VK Lvi Praha vs VK Euro Sitex Příbram',
22
- 'description': 'Zápas VK Lvi Praha vs VK Euro Sitex Příbram 11.12.2022 v 19:00 na Volej.TV',
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
- webpage = self._download_webpage(url, video_id)
29
- json_data = self._search_json(
30
- r'<\s*!\[CDATA[^=]+=', webpage, 'CDATA', video_id)
31
- formats, subtitle = self._extract_m3u8_formats_and_subtitles(
32
- json_data['urls']['hls'], video_id)
33
- return {
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
- 'title': self._html_search_meta(['og:title', 'twitter:title'], webpage),
36
- 'thumbnail': self._html_search_meta(['og:image', 'twitter:image'], webpage),
37
- 'description': self._html_search_meta(['description', 'og:description', 'twitter:description'], webpage),
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')) == 500:
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.25.233128'
3
+ __version__ = '2026.01.27.233257'
4
4
 
5
- RELEASE_GIT_HEAD = 'e3f0d8b731b40176bcc632bf92cfe5149402b202'
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.25.233128dev'
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.25.233128.dev0
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