yt-dlp 2025.11.14.235840.dev0__py3-none-any.whl → 2025.11.16.232923.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 (23) hide show
  1. yt_dlp/downloader/common.py +2 -1
  2. yt_dlp/extractor/_extractors.py +11 -1
  3. yt_dlp/extractor/bitmovin.py +74 -0
  4. yt_dlp/extractor/floatplane.py +13 -0
  5. yt_dlp/extractor/frontro.py +164 -0
  6. yt_dlp/extractor/lazy_extractors.py +67 -5
  7. yt_dlp/extractor/mave.py +119 -36
  8. yt_dlp/extractor/nowcanal.py +37 -0
  9. yt_dlp/extractor/soundcloud.py +7 -1
  10. yt_dlp/extractor/yfanefa.py +67 -0
  11. yt_dlp/extractor/youtube/_video.py +12 -4
  12. yt_dlp/networking/_urllib.py +2 -0
  13. yt_dlp/version.py +3 -3
  14. {yt_dlp-2025.11.14.235840.dev0.dist-info → yt_dlp-2025.11.16.232923.dev0.dist-info}/METADATA +1 -1
  15. {yt_dlp-2025.11.14.235840.dev0.dist-info → yt_dlp-2025.11.16.232923.dev0.dist-info}/RECORD +23 -19
  16. {yt_dlp-2025.11.14.235840.dev0.data → yt_dlp-2025.11.16.232923.dev0.data}/data/share/bash-completion/completions/yt-dlp +0 -0
  17. {yt_dlp-2025.11.14.235840.dev0.data → yt_dlp-2025.11.16.232923.dev0.data}/data/share/doc/yt_dlp/README.txt +0 -0
  18. {yt_dlp-2025.11.14.235840.dev0.data → yt_dlp-2025.11.16.232923.dev0.data}/data/share/fish/vendor_completions.d/yt-dlp.fish +0 -0
  19. {yt_dlp-2025.11.14.235840.dev0.data → yt_dlp-2025.11.16.232923.dev0.data}/data/share/man/man1/yt-dlp.1 +0 -0
  20. {yt_dlp-2025.11.14.235840.dev0.data → yt_dlp-2025.11.16.232923.dev0.data}/data/share/zsh/site-functions/_yt-dlp +0 -0
  21. {yt_dlp-2025.11.14.235840.dev0.dist-info → yt_dlp-2025.11.16.232923.dev0.dist-info}/WHEEL +0 -0
  22. {yt_dlp-2025.11.14.235840.dev0.dist-info → yt_dlp-2025.11.16.232923.dev0.dist-info}/entry_points.txt +0 -0
  23. {yt_dlp-2025.11.14.235840.dev0.dist-info → yt_dlp-2025.11.16.232923.dev0.dist-info}/licenses/LICENSE +0 -0
@@ -461,7 +461,8 @@ class FileDownloader:
461
461
  min_sleep_interval = self.params.get('sleep_interval') or 0
462
462
  max_sleep_interval = self.params.get('max_sleep_interval') or 0
463
463
 
464
- if available_at := info_dict.get('available_at'):
464
+ requested_formats = info_dict.get('requested_formats') or [info_dict]
465
+ if available_at := max(f.get('available_at') or 0 for f in requested_formats):
465
466
  forced_sleep_interval = available_at - int(time.time())
466
467
  if forced_sleep_interval > min_sleep_interval:
467
468
  sleep_note = 'as required by the site'
@@ -268,6 +268,7 @@ from .bitchute import (
268
268
  BitChuteChannelIE,
269
269
  BitChuteIE,
270
270
  )
271
+ from .bitmovin import BitmovinIE
271
272
  from .blackboardcollaborate import (
272
273
  BlackboardCollaborateIE,
273
274
  BlackboardCollaborateLaunchIE,
@@ -690,6 +691,10 @@ from .frontendmasters import (
690
691
  FrontendMastersIE,
691
692
  FrontendMastersLessonIE,
692
693
  )
694
+ from .frontro import (
695
+ TheChosenGroupIE,
696
+ TheChosenIE,
697
+ )
693
698
  from .fujitv import FujiTVFODPlus7IE
694
699
  from .funk import FunkIE
695
700
  from .funker530 import Funker530IE
@@ -1093,7 +1098,10 @@ from .markiza import (
1093
1098
  from .massengeschmacktv import MassengeschmackTVIE
1094
1099
  from .masters import MastersIE
1095
1100
  from .matchtv import MatchTVIE
1096
- from .mave import MaveIE
1101
+ from .mave import (
1102
+ MaveChannelIE,
1103
+ MaveIE,
1104
+ )
1097
1105
  from .mbn import MBNIE
1098
1106
  from .mdr import MDRIE
1099
1107
  from .medaltv import MedalTVIE
@@ -1368,6 +1376,7 @@ from .nova import (
1368
1376
  NovaIE,
1369
1377
  )
1370
1378
  from .novaplay import NovaPlayIE
1379
+ from .nowcanal import NowCanalIE
1371
1380
  from .nowness import (
1372
1381
  NownessIE,
1373
1382
  NownessPlaylistIE,
@@ -2521,6 +2530,7 @@ from .yappy import (
2521
2530
  YappyIE,
2522
2531
  YappyProfileIE,
2523
2532
  )
2533
+ from .yfanefa import YfanefaIE
2524
2534
  from .yle_areena import YleAreenaIE
2525
2535
  from .youjizz import YouJizzIE
2526
2536
  from .youku import (
@@ -0,0 +1,74 @@
1
+ import re
2
+
3
+ from .common import InfoExtractor
4
+ from ..utils.traversal import traverse_obj
5
+
6
+
7
+ class BitmovinIE(InfoExtractor):
8
+ _VALID_URL = r'https?://streams\.bitmovin\.com/(?P<id>\w+)'
9
+ _EMBED_REGEX = [r'<iframe\b[^>]+\bsrc=["\'](?P<url>(?:https?:)?//streams\.bitmovin\.com/(?P<id>\w+)[^"\']+)']
10
+ _TESTS = [{
11
+ 'url': 'https://streams.bitmovin.com/cqkl1t5giv3lrce7pjbg/embed',
12
+ 'info_dict': {
13
+ 'id': 'cqkl1t5giv3lrce7pjbg',
14
+ 'ext': 'mp4',
15
+ 'title': 'Developing Osteopathic Residents as Faculty',
16
+ 'thumbnail': 'https://streams.bitmovin.com/cqkl1t5giv3lrce7pjbg/poster',
17
+ },
18
+ 'params': {'skip_download': 'm3u8'},
19
+ }, {
20
+ 'url': 'https://streams.bitmovin.com/cgl9rh94uvs51rqc8jhg/share',
21
+ 'info_dict': {
22
+ 'id': 'cgl9rh94uvs51rqc8jhg',
23
+ 'ext': 'mp4',
24
+ 'title': 'Big Buck Bunny (Streams Docs)',
25
+ 'thumbnail': 'https://streams.bitmovin.com/cgl9rh94uvs51rqc8jhg/poster',
26
+ },
27
+ 'params': {'skip_download': 'm3u8'},
28
+ }]
29
+ _WEBPAGE_TESTS = [{
30
+ # bitmovin-stream web component
31
+ 'url': 'https://www.institutionalinvestor.com/article/2bsw1in1l9k68mp9kritc/video-war-stories-over-board-games/best-case-i-get-fired-war-stories',
32
+ 'info_dict': {
33
+ 'id': 'cuiumeil6g115lc4li3g',
34
+ 'ext': 'mp4',
35
+ 'title': '[media] War Stories over Board Games: “Best Case: I Get Fired” ',
36
+ 'thumbnail': 'https://streams.bitmovin.com/cuiumeil6g115lc4li3g/poster',
37
+ },
38
+ 'params': {'skip_download': 'm3u8'},
39
+ }, {
40
+ # iframe embed
41
+ 'url': 'https://www.clearblueionizer.com/en/pool-ionizers/mineral-pool-vs-saltwater-pool/',
42
+ 'info_dict': {
43
+ 'id': 'cvpvfsm1pf7itg7cfvtg',
44
+ 'ext': 'mp4',
45
+ 'title': 'Pool Ionizer vs. Salt Chlorinator',
46
+ 'thumbnail': 'https://streams.bitmovin.com/cvpvfsm1pf7itg7cfvtg/poster',
47
+ },
48
+ 'params': {'skip_download': 'm3u8'},
49
+ }]
50
+
51
+ @classmethod
52
+ def _extract_embed_urls(cls, url, webpage):
53
+ yield from super()._extract_embed_urls(url, webpage)
54
+ for stream_id in re.findall(r'<bitmovin-stream\b[^>]*\bstream-id=["\'](?P<id>\w+)', webpage):
55
+ yield f'https://streams.bitmovin.com/{stream_id}'
56
+
57
+ def _real_extract(self, url):
58
+ video_id = self._match_id(url)
59
+
60
+ player_config = self._download_json(
61
+ f'https://streams.bitmovin.com/{video_id}/config', video_id)['sources']
62
+
63
+ formats, subtitles = self._extract_m3u8_formats_and_subtitles(
64
+ player_config['hls'], video_id, 'mp4')
65
+
66
+ return {
67
+ 'id': video_id,
68
+ 'formats': formats,
69
+ 'subtitles': subtitles,
70
+ **traverse_obj(player_config, {
71
+ 'title': ('title', {str}),
72
+ 'thumbnail': ('poster', {str}),
73
+ }),
74
+ }
@@ -109,6 +109,17 @@ class FloatplaneBaseIE(InfoExtractor):
109
109
  'hls_media_playlist_data': m3u8_data,
110
110
  'hls_aes': hls_aes or None,
111
111
  })
112
+
113
+ subtitles = {}
114
+ automatic_captions = {}
115
+ for sub_data in traverse_obj(metadata, ('textTracks', lambda _, v: url_or_none(v['src']))):
116
+ sub_lang = sub_data.get('language') or 'en'
117
+ sub_entry = {'url': sub_data['src']}
118
+ if sub_data.get('generated'):
119
+ automatic_captions.setdefault(sub_lang, []).append(sub_entry)
120
+ else:
121
+ subtitles.setdefault(sub_lang, []).append(sub_entry)
122
+
112
123
  items.append({
113
124
  **common_info,
114
125
  'id': media_id,
@@ -118,6 +129,8 @@ class FloatplaneBaseIE(InfoExtractor):
118
129
  'thumbnail': ('thumbnail', 'path', {url_or_none}),
119
130
  }),
120
131
  'formats': formats,
132
+ 'subtitles': subtitles,
133
+ 'automatic_captions': automatic_captions,
121
134
  })
122
135
 
123
136
  post_info = {
@@ -0,0 +1,164 @@
1
+ import json
2
+
3
+ from .common import InfoExtractor
4
+ from ..utils import int_or_none, parse_iso8601, url_or_none
5
+ from ..utils.traversal import traverse_obj
6
+
7
+
8
+ class FrontoBaseIE(InfoExtractor):
9
+ def _get_auth_headers(self, url):
10
+ return traverse_obj(self._get_cookies(url), {
11
+ 'authorization': ('frAccessToken', 'value', {lambda token: f'Bearer {token}' if token else None}),
12
+ })
13
+
14
+
15
+ class FrontroVideoBaseIE(FrontoBaseIE):
16
+ _CHANNEL_ID = None
17
+
18
+ def _real_extract(self, url):
19
+ video_id = self._match_id(url)
20
+
21
+ metadata = self._download_json(
22
+ 'https://api.frontrow.cc/query', video_id, data=json.dumps({
23
+ 'operationName': 'Video',
24
+ 'variables': {'channelID': self._CHANNEL_ID, 'videoID': video_id},
25
+ 'query': '''query Video($channelID: ID!, $videoID: ID!) {
26
+ video(ChannelID: $channelID, VideoID: $videoID) {
27
+ ... on Video {title description updatedAt thumbnail createdAt duration likeCount comments views url hasAccess}
28
+ }
29
+ }''',
30
+ }).encode(), headers={
31
+ 'content-type': 'application/json',
32
+ **self._get_auth_headers(url),
33
+ })['data']['video']
34
+ if not traverse_obj(metadata, 'hasAccess'):
35
+ self.raise_login_required()
36
+
37
+ formats, subtitles = self._extract_m3u8_formats_and_subtitles(metadata['url'], video_id)
38
+
39
+ return {
40
+ 'id': video_id,
41
+ 'formats': formats,
42
+ 'subtitles': subtitles,
43
+ **traverse_obj(metadata, {
44
+ 'title': ('title', {str}),
45
+ 'description': ('description', {str}),
46
+ 'thumbnail': ('thumbnail', {url_or_none}),
47
+ 'timestamp': ('createdAt', {parse_iso8601}),
48
+ 'modified_timestamp': ('updatedAt', {parse_iso8601}),
49
+ 'duration': ('duration', {int_or_none}),
50
+ 'like_count': ('likeCount', {int_or_none}),
51
+ 'comment_count': ('comments', {int_or_none}),
52
+ 'view_count': ('views', {int_or_none}),
53
+ }),
54
+ }
55
+
56
+
57
+ class FrontroGroupBaseIE(FrontoBaseIE):
58
+ _CHANNEL_ID = None
59
+ _VIDEO_EXTRACTOR = None
60
+ _VIDEO_URL_TMPL = None
61
+
62
+ def _real_extract(self, url):
63
+ group_id = self._match_id(url)
64
+
65
+ metadata = self._download_json(
66
+ 'https://api.frontrow.cc/query', group_id, note='Downloading playlist metadata',
67
+ data=json.dumps({
68
+ 'operationName': 'PaginatedStaticPageContainer',
69
+ 'variables': {'channelID': self._CHANNEL_ID, 'first': 500, 'pageContainerID': group_id},
70
+ 'query': '''query PaginatedStaticPageContainer($channelID: ID!, $pageContainerID: ID!) {
71
+ pageContainer(ChannelID: $channelID, PageContainerID: $pageContainerID) {
72
+ ... on StaticPageContainer { id title updatedAt createdAt itemRefs {edges {node {
73
+ id contentItem { ... on ItemVideo { videoItem: item {
74
+ id
75
+ }}}
76
+ }}}
77
+ }
78
+ }
79
+ }''',
80
+ }).encode(), headers={
81
+ 'content-type': 'application/json',
82
+ **self._get_auth_headers(url),
83
+ })['data']['pageContainer']
84
+
85
+ entries = []
86
+ for video_id in traverse_obj(metadata, (
87
+ 'itemRefs', 'edges', ..., 'node', 'contentItem', 'videoItem', 'id', {str}),
88
+ ):
89
+ entries.append(self.url_result(
90
+ self._VIDEO_URL_TMPL % video_id, self._VIDEO_EXTRACTOR, video_id))
91
+
92
+ return {
93
+ '_type': 'playlist',
94
+ 'id': group_id,
95
+ 'entries': entries,
96
+ **traverse_obj(metadata, {
97
+ 'title': ('title', {str}),
98
+ 'timestamp': ('createdAt', {parse_iso8601}),
99
+ 'modified_timestamp': ('updatedAt', {parse_iso8601}),
100
+ }),
101
+ }
102
+
103
+
104
+ class TheChosenIE(FrontroVideoBaseIE):
105
+ _CHANNEL_ID = '12884901895'
106
+
107
+ _VALID_URL = r'https?://(?:www\.)?watch\.thechosen\.tv/video/(?P<id>[0-9]+)'
108
+ _TESTS = [{
109
+ 'url': 'https://watch.thechosen.tv/video/184683594325',
110
+ 'md5': '3f878b689588c71b38ec9943c54ff5b0',
111
+ 'info_dict': {
112
+ 'id': '184683594325',
113
+ 'ext': 'mp4',
114
+ 'title': 'Season 3 Episode 2: Two by Two',
115
+ 'description': 'md5:174c373756ecc8df46b403f4fcfbaf8c',
116
+ 'comment_count': int,
117
+ 'view_count': int,
118
+ 'like_count': int,
119
+ 'duration': 4212,
120
+ 'thumbnail': r're:https://fastly\.frontrowcdn\.com/channels/12884901895/VIDEO_THUMBNAIL/184683594325/',
121
+ 'timestamp': 1698954546,
122
+ 'upload_date': '20231102',
123
+ 'modified_timestamp': int,
124
+ 'modified_date': str,
125
+ },
126
+ }, {
127
+ 'url': 'https://watch.thechosen.tv/video/184683596189',
128
+ 'md5': 'd581562f9d29ce82f5b7770415334151',
129
+ 'info_dict': {
130
+ 'id': '184683596189',
131
+ 'ext': 'mp4',
132
+ 'title': 'Season 4 Episode 8: Humble',
133
+ 'description': 'md5:20a57bead43da1cf77cd5b0fe29bbc76',
134
+ 'comment_count': int,
135
+ 'view_count': int,
136
+ 'like_count': int,
137
+ 'duration': 5092,
138
+ 'thumbnail': r're:https://fastly\.frontrowcdn\.com/channels/12884901895/VIDEO_THUMBNAIL/184683596189/',
139
+ 'timestamp': 1715019474,
140
+ 'upload_date': '20240506',
141
+ 'modified_timestamp': int,
142
+ 'modified_date': str,
143
+ },
144
+ }]
145
+
146
+
147
+ class TheChosenGroupIE(FrontroGroupBaseIE):
148
+ _CHANNEL_ID = '12884901895'
149
+ _VIDEO_EXTRACTOR = TheChosenIE
150
+ _VIDEO_URL_TMPL = 'https://watch.thechosen.tv/video/%s'
151
+
152
+ _VALID_URL = r'https?://(?:www\.)?watch\.thechosen\.tv/group/(?P<id>[0-9]+)'
153
+ _TESTS = [{
154
+ 'url': 'https://watch.thechosen.tv/group/309237658592',
155
+ 'info_dict': {
156
+ 'id': '309237658592',
157
+ 'title': 'Season 3',
158
+ 'timestamp': 1746203969,
159
+ 'upload_date': '20250502',
160
+ 'modified_timestamp': int,
161
+ 'modified_date': str,
162
+ },
163
+ 'playlist_count': 8,
164
+ }]