yt-dlp 2025.11.3.233024.dev0__py3-none-any.whl → 2025.11.11.5312.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 (35) hide show
  1. yt_dlp/cookies.py +1 -1
  2. yt_dlp/downloader/external.py +7 -0
  3. yt_dlp/extractor/_extractors.py +5 -1
  4. yt_dlp/extractor/bunnycdn.py +20 -3
  5. yt_dlp/extractor/dplay.py +8 -7
  6. yt_dlp/extractor/firsttv.py +34 -1
  7. yt_dlp/extractor/floatplane.py +26 -27
  8. yt_dlp/extractor/goplay.py +7 -5
  9. yt_dlp/extractor/lazy_extractors.py +22 -6
  10. yt_dlp/extractor/mux.py +92 -0
  11. yt_dlp/extractor/ntvru.py +94 -57
  12. yt_dlp/extractor/tubetugraz.py +9 -2
  13. yt_dlp/extractor/twitch.py +5 -2
  14. yt_dlp/extractor/xhamster.py +31 -0
  15. yt_dlp/extractor/youtube/_base.py +1 -1
  16. yt_dlp/extractor/youtube/_tab.py +3 -2
  17. yt_dlp/extractor/youtube/_video.py +154 -30
  18. yt_dlp/extractor/youtube/jsc/_builtin/vendor/_info.py +1 -1
  19. yt_dlp/networking/_curlcffi.py +4 -1
  20. yt_dlp/networking/_requests.py +14 -9
  21. yt_dlp/networking/_urllib.py +19 -1
  22. yt_dlp/networking/common.py +6 -2
  23. yt_dlp/options.py +12 -5
  24. yt_dlp/utils/_jsruntime.py +13 -4
  25. yt_dlp/version.py +3 -3
  26. {yt_dlp-2025.11.3.233024.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/doc/yt_dlp/README.txt +26 -15
  27. {yt_dlp-2025.11.3.233024.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/fish/vendor_completions.d/yt-dlp.fish +2 -2
  28. {yt_dlp-2025.11.3.233024.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/man/man1/yt-dlp.1 +18 -9
  29. {yt_dlp-2025.11.3.233024.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/zsh/site-functions/_yt-dlp +2 -2
  30. {yt_dlp-2025.11.3.233024.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/METADATA +28 -17
  31. {yt_dlp-2025.11.3.233024.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/RECORD +35 -34
  32. {yt_dlp-2025.11.3.233024.dev0.data → yt_dlp-2025.11.11.5312.dev0.data}/data/share/bash-completion/completions/yt-dlp +0 -0
  33. {yt_dlp-2025.11.3.233024.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/WHEEL +0 -0
  34. {yt_dlp-2025.11.3.233024.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/entry_points.txt +0 -0
  35. {yt_dlp-2025.11.3.233024.dev0.dist-info → yt_dlp-2025.11.11.5312.dev0.dist-info}/licenses/LICENSE +0 -0
yt_dlp/cookies.py CHANGED
@@ -557,7 +557,7 @@ class WindowsChromeCookieDecryptor(ChromeCookieDecryptor):
557
557
 
558
558
 
559
559
  def _extract_safari_cookies(profile, logger):
560
- if sys.platform != 'darwin':
560
+ if sys.platform not in ('darwin', 'ios'):
561
561
  raise ValueError(f'unsupported platform: {sys.platform}')
562
562
 
563
563
  if profile:
@@ -560,6 +560,13 @@ class FFmpegFD(ExternalFD):
560
560
  elif isinstance(conn, str):
561
561
  args += ['-rtmp_conn', conn]
562
562
 
563
+ elif protocol == 'http_dash_segments' and info_dict.get('is_live'):
564
+ # ffmpeg may try to read past the latest available segments for
565
+ # live DASH streams unless we pass `-re`. In modern ffmpeg, this
566
+ # is an alias of `-readrate 1`, but `-readrate` was not added
567
+ # until ffmpeg 5.0, so we must stick to using `-re`
568
+ args += ['-re']
569
+
563
570
  url = fmt['url']
564
571
  if self.params.get('enable_file_urls') and url.startswith('file:'):
565
572
  # The default protocol_whitelist is 'file,crypto,data' when reading local m3u8 URLs,
@@ -640,7 +640,10 @@ from .filmon import (
640
640
  FilmOnIE,
641
641
  )
642
642
  from .filmweb import FilmwebIE
643
- from .firsttv import FirstTVIE
643
+ from .firsttv import (
644
+ FirstTVIE,
645
+ FirstTVLiveIE,
646
+ )
644
647
  from .fivetv import FiveTVIE
645
648
  from .flextv import FlexTVIE
646
649
  from .flickr import FlickrIE
@@ -1197,6 +1200,7 @@ from .musicdex import (
1197
1200
  MusicdexPlaylistIE,
1198
1201
  MusicdexSongIE,
1199
1202
  )
1203
+ from .mux import MuxIE
1200
1204
  from .mx3 import (
1201
1205
  Mx3IE,
1202
1206
  Mx3NeoIE,
@@ -16,7 +16,7 @@ from ..utils.traversal import find_element, traverse_obj
16
16
 
17
17
 
18
18
  class BunnyCdnIE(InfoExtractor):
19
- _VALID_URL = r'https?://(?:iframe\.mediadelivery\.net|video\.bunnycdn\.com)/(?:embed|play)/(?P<library_id>\d+)/(?P<id>[\da-f-]+)'
19
+ _VALID_URL = r'https?://(?:(?:iframe|player)\.mediadelivery\.net|video\.bunnycdn\.com)/(?:embed|play)/(?P<library_id>\d+)/(?P<id>[\da-f-]+)'
20
20
  _EMBED_REGEX = [rf'<iframe[^>]+src=[\'"](?P<url>{_VALID_URL}[^\'"]*)[\'"]']
21
21
  _TESTS = [{
22
22
  'url': 'https://iframe.mediadelivery.net/embed/113933/e73edec1-e381-4c8b-ae73-717a140e0924',
@@ -39,7 +39,7 @@ class BunnyCdnIE(InfoExtractor):
39
39
  'timestamp': 1691145748,
40
40
  'thumbnail': r're:^https?://.*\.b-cdn\.net/32e34c4b-0d72-437c-9abb-05e67657da34/thumbnail_9172dc16\.jpg',
41
41
  'duration': 106.0,
42
- 'description': 'md5:981a3e899a5c78352b21ed8b2f1efd81',
42
+ 'description': 'md5:11452bcb31f379ee3eaf1234d3264e44',
43
43
  'upload_date': '20230804',
44
44
  'title': 'Sanela ist Teil der #arbeitsmarktkraft',
45
45
  },
@@ -58,6 +58,23 @@ class BunnyCdnIE(InfoExtractor):
58
58
  'thumbnail': r're:^https?://.*\.b-cdn\.net/2e8545ec-509d-4571-b855-4cf0235ccd75/thumbnail\.jpg',
59
59
  },
60
60
  'params': {'skip_download': True},
61
+ }, {
62
+ # Requires any Referer
63
+ 'url': 'https://iframe.mediadelivery.net/embed/289162/6372f5a3-68df-4ef7-a115-e1110186c477',
64
+ 'info_dict': {
65
+ 'id': '6372f5a3-68df-4ef7-a115-e1110186c477',
66
+ 'ext': 'mp4',
67
+ 'title': '12-Creating Small Asset Blockouts -Timelapse.mp4',
68
+ 'description': '',
69
+ 'duration': 263.0,
70
+ 'timestamp': 1724485440,
71
+ 'upload_date': '20240824',
72
+ 'thumbnail': r're:^https?://.*\.b-cdn\.net/6372f5a3-68df-4ef7-a115-e1110186c477/thumbnail\.jpg',
73
+ },
74
+ 'params': {'skip_download': True},
75
+ }, {
76
+ 'url': 'https://player.mediadelivery.net/embed/519128/875880a9-bcc2-4038-9e05-e5024bba9b70',
77
+ 'only_matching': True,
61
78
  }]
62
79
  _WEBPAGE_TESTS = [{
63
80
  # Stream requires Referer
@@ -100,7 +117,7 @@ class BunnyCdnIE(InfoExtractor):
100
117
  video_id, library_id = self._match_valid_url(url).group('id', 'library_id')
101
118
  webpage = self._download_webpage(
102
119
  f'https://iframe.mediadelivery.net/embed/{library_id}/{video_id}', video_id,
103
- headers=traverse_obj(smuggled_data, {'Referer': 'Referer'}),
120
+ headers={'Referer': smuggled_data.get('Referer') or 'https://iframe.mediadelivery.net/'},
104
121
  query=traverse_obj(parse_qs(url), {'token': 'token', 'expires': 'expires'}))
105
122
 
106
123
  if html_title := self._html_extract_title(webpage, default=None) == '403':
yt_dlp/extractor/dplay.py CHANGED
@@ -1063,7 +1063,7 @@ class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
1063
1063
  'ext': 'mp4',
1064
1064
  'title': 'German Gold',
1065
1065
  'description': 'md5:f3073306553a8d9b40e6ac4cdbf09fc6',
1066
- 'display_id': 'german-gold',
1066
+ 'display_id': 'goldrausch-in-australien/german-gold',
1067
1067
  'episode': 'Episode 1',
1068
1068
  'episode_number': 1,
1069
1069
  'season': 'Season 5',
@@ -1112,7 +1112,7 @@ class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
1112
1112
  'ext': 'mp4',
1113
1113
  'title': '24 Stunden auf der Feuerwache 3',
1114
1114
  'description': 'md5:f3084ef6170bfb79f9a6e0c030e09330',
1115
- 'display_id': '24-stunden-auf-der-feuerwache-3',
1115
+ 'display_id': 'feuerwache-3-alarm-in-muenchen/24-stunden-auf-der-feuerwache-3',
1116
1116
  'episode': 'Episode 1',
1117
1117
  'episode_number': 1,
1118
1118
  'season': 'Season 1',
@@ -1134,7 +1134,7 @@ class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
1134
1134
  'ext': 'mp4',
1135
1135
  'title': 'Der Poltergeist im Kostümladen',
1136
1136
  'description': 'md5:20b52b9736a0a3a7873d19a238fad7fc',
1137
- 'display_id': 'der-poltergeist-im-kostumladen',
1137
+ 'display_id': 'ghost-adventures/der-poltergeist-im-kostumladen',
1138
1138
  'episode': 'Episode 1',
1139
1139
  'episode_number': 1,
1140
1140
  'season': 'Season 25',
@@ -1156,7 +1156,7 @@ class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
1156
1156
  'ext': 'mp4',
1157
1157
  'title': 'Das Geheimnis meines Bruders',
1158
1158
  'description': 'md5:3167550bb582eb9c92875c86a0a20882',
1159
- 'display_id': 'das-geheimnis-meines-bruders',
1159
+ 'display_id': 'evil-gesichter-des-boesen/das-geheimnis-meines-bruders',
1160
1160
  'episode': 'Episode 1',
1161
1161
  'episode_number': 1,
1162
1162
  'season': 'Season 1',
@@ -1175,18 +1175,19 @@ class DiscoveryNetworksDeIE(DiscoveryPlusBaseIE):
1175
1175
 
1176
1176
  def _real_extract(self, url):
1177
1177
  domain, programme, alternate_id = self._match_valid_url(url).groups()
1178
+ display_id = f'{programme}/{alternate_id}'
1178
1179
  meta = self._download_json(
1179
1180
  f'https://de-api.loma-cms.com/feloma/videos/{alternate_id}/',
1180
- alternate_id, query={
1181
+ display_id, query={
1181
1182
  'environment': domain.split('.')[0],
1182
1183
  'v': '2',
1183
1184
  'filter[show.slug]': programme,
1184
1185
  }, fatal=False)
1185
- video_id = traverse_obj(meta, ('uid', {str}, {lambda s: s[-7:]})) or alternate_id
1186
+ video_id = traverse_obj(meta, ('uid', {str}, {lambda s: s[-7:]})) or display_id
1186
1187
 
1187
1188
  disco_api_info = self._get_disco_api_info(
1188
1189
  url, video_id, 'eu1-prod.disco-api.com', domain.replace('.', ''), 'DE')
1189
- disco_api_info['display_id'] = alternate_id
1190
+ disco_api_info['display_id'] = display_id
1190
1191
  disco_api_info['categories'] = traverse_obj(meta, (
1191
1192
  'taxonomies', lambda _, v: v['category'] == 'genre', 'title', {str.strip}, filter, all, filter))
1192
1193
 
@@ -10,7 +10,7 @@ from ..utils import (
10
10
  unified_strdate,
11
11
  url_or_none,
12
12
  )
13
- from ..utils.traversal import traverse_obj
13
+ from ..utils.traversal import require, traverse_obj
14
14
 
15
15
 
16
16
  class FirstTVIE(InfoExtractor):
@@ -129,3 +129,36 @@ class FirstTVIE(InfoExtractor):
129
129
  return self.playlist_result(
130
130
  self._entries(items), display_id, self._og_search_title(webpage, default=None),
131
131
  thumbnail=self._og_search_thumbnail(webpage, default=None))
132
+
133
+
134
+ class FirstTVLiveIE(InfoExtractor):
135
+ IE_NAME = '1tv:live'
136
+ IE_DESC = 'Первый канал (прямой эфир)'
137
+ _VALID_URL = r'https?://(?:www\.)?1tv\.ru/live'
138
+
139
+ _TESTS = [{
140
+ 'url': 'https://www.1tv.ru/live',
141
+ 'info_dict': {
142
+ 'id': 'live',
143
+ 'ext': 'mp4',
144
+ 'title': r're:ПЕРВЫЙ КАНАЛ ПРЯМОЙ ЭФИР СМОТРЕТЬ ОНЛАЙН \d{4}-\d{2}-\d{2} \d{2}:\d{2}$',
145
+ 'live_status': 'is_live',
146
+ },
147
+ 'params': {'skip_download': 'livestream'},
148
+ }]
149
+
150
+ def _real_extract(self, url):
151
+ display_id = 'live'
152
+ webpage = self._download_webpage(url, display_id, fatal=False)
153
+
154
+ streams_list = self._download_json('https://stream.1tv.ru/api/playlist/1tvch-v1_as_array.json', display_id)
155
+ mpd_url = traverse_obj(streams_list, ('mpd', ..., {url_or_none}, any, {require('mpd url')}))
156
+ # FFmpeg needs to be passed -re to not seek past live window. This is handled by core
157
+ formats, _ = self._extract_mpd_formats_and_subtitles(mpd_url, display_id, mpd_id='dash')
158
+
159
+ return {
160
+ 'id': display_id,
161
+ 'title': self._html_extract_title(webpage),
162
+ 'formats': formats,
163
+ 'is_live': True,
164
+ }
@@ -6,15 +6,15 @@ from ..utils import (
6
6
  OnDemandPagedList,
7
7
  clean_html,
8
8
  determine_ext,
9
+ float_or_none,
9
10
  format_field,
10
11
  int_or_none,
11
12
  join_nonempty,
12
- parse_codecs,
13
13
  parse_iso8601,
14
14
  url_or_none,
15
15
  urljoin,
16
16
  )
17
- from ..utils.traversal import traverse_obj
17
+ from ..utils.traversal import require, traverse_obj
18
18
 
19
19
 
20
20
  class FloatplaneBaseIE(InfoExtractor):
@@ -50,37 +50,31 @@ class FloatplaneBaseIE(InfoExtractor):
50
50
  media_id = media['id']
51
51
  media_typ = media.get('type') or 'video'
52
52
 
53
- metadata = self._download_json(
54
- f'{self._BASE_URL}/api/v3/content/{media_typ}', media_id, query={'id': media_id},
55
- note=f'Downloading {media_typ} metadata', impersonate=self._IMPERSONATE_TARGET)
56
-
57
53
  stream = self._download_json(
58
- f'{self._BASE_URL}/api/v2/cdn/delivery', media_id, query={
59
- 'type': 'vod' if media_typ == 'video' else 'aod',
60
- 'guid': metadata['guid'],
61
- }, note=f'Downloading {media_typ} stream data',
54
+ f'{self._BASE_URL}/api/v3/delivery/info', media_id,
55
+ query={'scenario': 'onDemand', 'entityId': media_id},
56
+ note=f'Downloading {media_typ} stream data',
62
57
  impersonate=self._IMPERSONATE_TARGET)
63
58
 
64
- path_template = traverse_obj(stream, ('resource', 'uri', {str}))
59
+ metadata = self._download_json(
60
+ f'{self._BASE_URL}/api/v3/content/{media_typ}', media_id,
61
+ f'Downloading {media_typ} metadata', query={'id': media_id},
62
+ fatal=False, impersonate=self._IMPERSONATE_TARGET)
65
63
 
66
- def format_path(params):
67
- path = path_template
68
- for i, val in (params or {}).items():
69
- path = path.replace(f'{{qualityLevelParams.{i}}}', val)
70
- return path
64
+ cdn_base_url = traverse_obj(stream, (
65
+ 'groups', 0, 'origins', ..., 'url', {url_or_none}, any, {require('cdn base url')}))
71
66
 
72
67
  formats = []
73
- for quality in traverse_obj(stream, ('resource', 'data', 'qualityLevels', ...)):
74
- url = urljoin(stream['cdn'], format_path(traverse_obj(
75
- stream, ('resource', 'data', 'qualityLevelParams', quality['name'], {dict}))))
76
- format_id = traverse_obj(quality, ('name', {str}))
68
+ for variant in traverse_obj(stream, ('groups', 0, 'variants', lambda _, v: v['url'])):
69
+ format_url = urljoin(cdn_base_url, variant['url'])
70
+ format_id = traverse_obj(variant, ('name', {str}))
77
71
  hls_aes = {}
78
72
  m3u8_data = None
79
73
 
80
74
  # If we need impersonation for the API, then we need it for HLS keys too: extract in advance
81
75
  if self._IMPERSONATE_TARGET is not None:
82
76
  m3u8_data = self._download_webpage(
83
- url, media_id, fatal=False, impersonate=self._IMPERSONATE_TARGET, headers=self._HEADERS,
77
+ format_url, media_id, fatal=False, impersonate=self._IMPERSONATE_TARGET, headers=self._HEADERS,
84
78
  note=join_nonempty('Downloading', format_id, 'm3u8 information', delim=' '),
85
79
  errnote=join_nonempty('Failed to download', format_id, 'm3u8 information', delim=' '))
86
80
  if not m3u8_data:
@@ -98,14 +92,19 @@ class FloatplaneBaseIE(InfoExtractor):
98
92
  hls_aes['key'] = urlh.read().hex()
99
93
 
100
94
  formats.append({
101
- **traverse_obj(quality, {
95
+ **traverse_obj(variant, {
102
96
  'format_note': ('label', {str}),
103
- 'width': ('width', {int}),
104
- 'height': ('height', {int}),
97
+ 'width': ('meta', 'video', 'width', {int_or_none}),
98
+ 'height': ('meta', 'video', 'height', {int_or_none}),
99
+ 'vcodec': ('meta', 'video', 'codec', {str}),
100
+ 'acodec': ('meta', 'audio', 'codec', {str}),
101
+ 'vbr': ('meta', 'video', 'bitrate', 'average', {int_or_none(scale=1000)}),
102
+ 'abr': ('meta', 'audio', 'bitrate', 'average', {int_or_none(scale=1000)}),
103
+ 'audio_channels': ('meta', 'audio', 'channelCount', {int_or_none}),
104
+ 'fps': ('meta', 'video', 'fps', {float_or_none}),
105
105
  }),
106
- **parse_codecs(quality.get('codecs')),
107
- 'url': url,
108
- 'ext': determine_ext(url.partition('/chunk.m3u8')[0], 'mp4'),
106
+ 'url': format_url,
107
+ 'ext': determine_ext(format_url.partition('/chunk.m3u8')[0], 'mp4'),
109
108
  'format_id': format_id,
110
109
  'hls_media_playlist_data': m3u8_data,
111
110
  'hls_aes': hls_aes or None,
@@ -13,12 +13,14 @@ from ..utils.traversal import get_first, traverse_obj
13
13
 
14
14
 
15
15
  class GoPlayIE(InfoExtractor):
16
- _VALID_URL = r'https?://(www\.)?goplay\.be/video/([^/?#]+/[^/?#]+/|)(?P<id>[^/#]+)'
16
+ IE_NAME = 'play.tv'
17
+ IE_DESC = 'PLAY (formerly goplay.be)'
18
+ _VALID_URL = r'https?://(www\.)?play\.tv/video/([^/?#]+/[^/?#]+/|)(?P<id>[^/#]+)'
17
19
 
18
20
  _NETRC_MACHINE = 'goplay'
19
21
 
20
22
  _TESTS = [{
21
- 'url': 'https://www.goplay.be/video/de-slimste-mens-ter-wereld/de-slimste-mens-ter-wereld-s22/de-slimste-mens-ter-wereld-s22-aflevering-1',
23
+ 'url': 'https://www.play.tv/video/de-slimste-mens-ter-wereld/de-slimste-mens-ter-wereld-s22/de-slimste-mens-ter-wereld-s22-aflevering-1',
22
24
  'info_dict': {
23
25
  'id': '2baa4560-87a0-421b-bffc-359914e3c387',
24
26
  'ext': 'mp4',
@@ -33,7 +35,7 @@ class GoPlayIE(InfoExtractor):
33
35
  'params': {'skip_download': True},
34
36
  'skip': 'This video is only available for registered users',
35
37
  }, {
36
- 'url': 'https://www.goplay.be/video/1917',
38
+ 'url': 'https://www.play.tv/video/1917',
37
39
  'info_dict': {
38
40
  'id': '40cac41d-8d29-4ef5-aa11-75047b9f0907',
39
41
  'ext': 'mp4',
@@ -43,7 +45,7 @@ class GoPlayIE(InfoExtractor):
43
45
  'params': {'skip_download': True},
44
46
  'skip': 'This video is only available for registered users',
45
47
  }, {
46
- 'url': 'https://www.goplay.be/video/de-mol/de-mol-s11/de-mol-s11-aflevering-1#autoplay',
48
+ 'url': 'https://www.play.tv/video/de-mol/de-mol-s11/de-mol-s11-aflevering-1#autoplay',
47
49
  'info_dict': {
48
50
  'id': 'ecb79672-92b9-4cd9-a0d7-e2f0250681ee',
49
51
  'ext': 'mp4',
@@ -101,7 +103,7 @@ class GoPlayIE(InfoExtractor):
101
103
  break
102
104
 
103
105
  api = self._download_json(
104
- f'https://api.goplay.be/web/v1/videos/long-form/{video_id}',
106
+ f'https://api.play.tv/web/v1/videos/long-form/{video_id}',
105
107
  video_id, headers={
106
108
  'Authorization': f'Bearer {self._id_token}',
107
109
  **self.geo_verification_headers(),