Compare commits

..
15 Commits
Author SHA1 Message Date
seproandGitHub d925e92b71 [ie/vevo] Restore extractors (#14203)
Partially reverts 6f4c1bb593

Authored by: seproDev
2025-08-31 00:41:52 +02:00
seproandGitHub ed24640943 [ie/lrt] Fix extractors (#14193)
Closes #13501
Authored by: seproDev
2025-08-30 00:28:44 +02:00
seproandGitHub 76bb46002c Fix --id deprecation warning (#14190)
Authored by: seproDev
2025-08-29 22:06:53 +02:00
InvalidUsernameExceptionandGitHub 1e28f6bf74 [ie/kick:vod] Support ongoing livestream VODs (#14154)
Authored by: InvalidUsernameException
2025-08-28 01:26:49 +00:00
garret1317andGitHub 0b51005b48 [ie/ITVBTCC] Fix extractor (#14161)
Closes #14156
Authored by: garret1317
2025-08-28 01:19:25 +00:00
AbdulmohsenandGitHub 223baa81f6 [ie/tver] Extract more metadata (#14165)
Authored by: arabcoders
2025-08-28 01:18:10 +00:00
Gegham ZakaryanandGitHub 18fe696df9 [ie/googledrive] Fix subtitles extraction (#14139)
Authored by: zakaryan2004
2025-08-28 01:12:08 +00:00
github-actions[bot] 487a90c8ef Release 2025.08.27
Created by: bashonly

:ci skip all
2025-08-27 23:56:39 +00:00
bashonlyandGitHub 8cd37b85d4 [ie/youtube] Use alternative tv user-agent when authenticated (#14169)
Authored by: bashonly
2025-08-27 23:00:03 +00:00
bashonlyandGitHub 5c7ad68ff1 [ie/youtube] Deprioritize web_safari m3u8 formats (#14168)
Authored by: bashonly
2025-08-27 22:31:51 +00:00
seproandGitHub 1ddbd033f0 [ie/generic] Simplify invalid URL error message (#14167)
Authored by: seproDev
2025-08-27 23:27:57 +02:00
seproandGitHub fec30c56f0 [ie/generic] Use https as fallback protocol (#14160)
Authored by: seproDev
2025-08-27 22:25:35 +02:00
seproandGitHub d6950c27af [ie/skeb] Support wav files (#14147)
Closes #14146
Authored by: seproDev
2025-08-27 15:34:44 +02:00
bashonlyandGitHub 3bd9154412 [ie/youtube] Player client maintenance (#14135)
- Prioritize `tv_simply` over `tv` in default logged-out clients
- Revert `tv` client user-agent to work around 403 errors

Authored by: bashonly
2025-08-23 23:45:29 +00:00
bashonlyandGitHub 8f4a908300 [ie/youtube] Add tcc player JS variant (#14134)
Authored by: bashonly
2025-08-23 23:43:50 +00:00
18 changed files with 553 additions and 89 deletions
+13
View File
@@ -4,6 +4,19 @@
# To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master # To create a release, dispatch the https://github.com/yt-dlp/yt-dlp/actions/workflows/release.yml workflow on master
--> -->
### 2025.08.27
#### Extractor changes
- **generic**
- [Simplify invalid URL error message](https://github.com/yt-dlp/yt-dlp/commit/1ddbd033f0fd65917526b1271cea66913ac8647f) ([#14167](https://github.com/yt-dlp/yt-dlp/issues/14167)) by [seproDev](https://github.com/seproDev)
- [Use https as fallback protocol](https://github.com/yt-dlp/yt-dlp/commit/fec30c56f0e97e573ace659104ff0d72c4cc9809) ([#14160](https://github.com/yt-dlp/yt-dlp/issues/14160)) by [seproDev](https://github.com/seproDev)
- **skeb**: [Support wav files](https://github.com/yt-dlp/yt-dlp/commit/d6950c27af31908363c5c815e3b7eb4f9ff41643) ([#14147](https://github.com/yt-dlp/yt-dlp/issues/14147)) by [seproDev](https://github.com/seproDev)
- **youtube**
- [Add `tcc` player JS variant](https://github.com/yt-dlp/yt-dlp/commit/8f4a908300f55054bc96814bceeaa1034fdf4110) ([#14134](https://github.com/yt-dlp/yt-dlp/issues/14134)) by [bashonly](https://github.com/bashonly)
- [Deprioritize `web_safari` m3u8 formats](https://github.com/yt-dlp/yt-dlp/commit/5c7ad68ff1643ad80d18cef8be9db8fcab05ee6c) ([#14168](https://github.com/yt-dlp/yt-dlp/issues/14168)) by [bashonly](https://github.com/bashonly)
- [Player client maintenance](https://github.com/yt-dlp/yt-dlp/commit/3bd91544122142a87863d79e54e995c26cfd7f92) ([#14135](https://github.com/yt-dlp/yt-dlp/issues/14135)) by [bashonly](https://github.com/bashonly)
- [Use alternative `tv` user-agent when authenticated](https://github.com/yt-dlp/yt-dlp/commit/8cd37b85d492edb56a4f7506ea05527b85a6b02b) ([#14169](https://github.com/yt-dlp/yt-dlp/issues/14169)) by [bashonly](https://github.com/bashonly)
### 2025.08.22 ### 2025.08.22
#### Core changes #### Core changes
+1 -1
View File
@@ -1802,7 +1802,7 @@ The following extractors use this feature:
#### youtube #### youtube
* `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the list of supported content language codes * `lang`: Prefer translated metadata (`title`, `description` etc) of this language code (case-sensitive). By default, the video primary language metadata is preferred, with a fallback to `en` translated. See [youtube/_base.py](https://github.com/yt-dlp/yt-dlp/blob/415b4c9f955b1a0391204bd24a7132590e7b3bdb/yt_dlp/extractor/youtube/_base.py#L402-L409) for the list of supported content language codes
* `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively * `skip`: One or more of `hls`, `dash` or `translated_subs` to skip extraction of the m3u8 manifests, dash manifests and [auto-translated subtitles](https://github.com/yt-dlp/yt-dlp/issues/4090#issuecomment-1158102032) respectively
* `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `tv,tv_simply,web` is used, but `tv,web_safari,web` is used when authenticating with cookies and `tv,web_creator,web` is used with premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios` * `player_client`: Clients to extract video data from. The currently available clients are `web`, `web_safari`, `web_embedded`, `web_music`, `web_creator`, `mweb`, `ios`, `android`, `android_vr`, `tv`, `tv_simply` and `tv_embedded`. By default, `tv_simply,tv,web` is used, but `tv,web_safari,web` is used when authenticating with cookies and `tv,web_creator,web` is used with premium accounts. The `web_music` client is added for `music.youtube.com` URLs when logged-in cookies are used. The `web_embedded` client is added for age-restricted videos but only works if the video is embeddable. The `tv_embedded` and `web_creator` clients are added for age-restricted videos if account age-verification is required. Some clients, such as `web` and `web_music`, require a `po_token` for their formats to be downloadable. Some clients, such as `web_creator`, will only work with authentication. Not all clients support authentication via cookies. You can use `default` for the default clients, or you can use `all` for all clients (not recommended). You can prefix a client with `-` to exclude it, e.g. `youtube:player_client=default,-ios`
* `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details * `player_skip`: Skip some network requests that are generally needed for robust extraction. One or more of `configs` (skip client configs), `webpage` (skip initial webpage), `js` (skip js player), `initial_data` (skip initial data/next ep request). While these options can help reduce the number of requests needed or avoid some rate-limiting, they could cause issues such as missing formats or metadata. See [#860](https://github.com/yt-dlp/yt-dlp/pull/860) and [#12826](https://github.com/yt-dlp/yt-dlp/issues/12826) for more details
* `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests * `webpage_skip`: Skip extraction of embedded webpage data. One or both of `player_response`, `initial_data`. These options are for testing purposes and don't skip any network requests
* `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp. * `player_params`: YouTube player parameters to use for player requests. Will overwrite any default ones set by yt-dlp.
+9
View File
@@ -148,6 +148,11 @@ _SIG_TESTS = [
'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt', 'gN7a-hudCuAuPH6fByOk1_GNXN0yNMHShjZXS2VOgsEItAJz0tipeavEOmNdYN-wUtcEqD3bCXjc0iyKfAyZxCBGgIARwsSdQfJ2CJtt',
'ttJC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3DqEctUw-NYdNmOEvaepit2zJAsIEggOVaSXZjhSHMNy0NXNG_1kOyBf6HPuAuCduh-', 'ttJC2JfQdSswRAIgGBCxZyAfKyi0cjXCb3DqEctUw-NYdNmOEvaepit2zJAsIEggOVaSXZjhSHMNy0NXNG_1kOyBf6HPuAuCduh-',
), ),
(
'https://www.youtube.com/s/player/5ec65609/player_ias_tcc.vflset/en_US/base.js',
'AAJAJfQdSswRAIgNSN0GDUcHnCIXkKcF61yLBgDHiX1sUhOJdY4_GxunRYCIDeYNYP_16mQTPm5f1OVq3oV1ijUNYPjP4iUSMAjO9bZ',
'AJfQdSswRAIgNSN0GDUcHnCIXkKcF61ZLBgDHiX1sUhOJdY4_GxunRYCIDyYNYP_16mQTPm5f1OVq3oV1ijUNYPjP4iUSMAjO9be',
),
] ]
_NSIG_TESTS = [ _NSIG_TESTS = [
@@ -395,6 +400,10 @@ _NSIG_TESTS = [
'https://www.youtube.com/s/player/010fbc8d/player_es6.vflset/en_US/base.js', 'https://www.youtube.com/s/player/010fbc8d/player_es6.vflset/en_US/base.js',
'0hlOAlqjFszVvF4Z', 'R-H23bZGAsRFTg', '0hlOAlqjFszVvF4Z', 'R-H23bZGAsRFTg',
), ),
(
'https://www.youtube.com/s/player/5ec65609/player_ias_tcc.vflset/en_US/base.js',
'6l5CTNx4AzIqH4MXM', 'NupToduxHBew1g',
),
] ]
+1
View File
@@ -979,6 +979,7 @@ def parse_options(argv=None):
'geo_bypass': opts.geo_bypass, 'geo_bypass': opts.geo_bypass,
'geo_bypass_country': opts.geo_bypass_country, 'geo_bypass_country': opts.geo_bypass_country,
'geo_bypass_ip_block': opts.geo_bypass_ip_block, 'geo_bypass_ip_block': opts.geo_bypass_ip_block,
'useid': opts.useid or None,
'warn_when_outdated': opts.update_self is None, 'warn_when_outdated': opts.update_self is None,
'_warnings': warnings, '_warnings': warnings,
'_deprecation_warnings': deprecation_warnings, '_deprecation_warnings': deprecation_warnings,
+4
View File
@@ -2288,6 +2288,10 @@ from .varzesh3 import Varzesh3IE
from .vbox7 import Vbox7IE from .vbox7 import Vbox7IE
from .veo import VeoIE from .veo import VeoIE
from .vesti import VestiIE from .vesti import VestiIE
from .vevo import (
VevoIE,
VevoPlaylistIE,
)
from .vgtv import ( from .vgtv import (
VGTVIE, VGTVIE,
BTArticleIE, BTArticleIE,
+4
View File
@@ -90,6 +90,10 @@ class DisneyIE(InfoExtractor):
webpage, 'embed data'), video_id) webpage, 'embed data'), video_id)
video_data = page_data['video'] video_data = page_data['video']
for external in video_data.get('externals', []):
if external.get('source') == 'vevo':
return self.url_result('vevo:' + external['data_id'], 'Vevo')
video_id = video_data['id'] video_id = video_data['id']
title = video_data['title'] title = video_data['title']
+3 -5
View File
@@ -772,8 +772,8 @@ class GenericIE(InfoExtractor):
if default_search in ('auto', 'auto_warning', 'fixup_error'): if default_search in ('auto', 'auto_warning', 'fixup_error'):
if re.match(r'[^\s/]+\.[^\s/]+/', url): if re.match(r'[^\s/]+\.[^\s/]+/', url):
self.report_warning('The url doesn\'t specify the protocol, trying with http') self.report_warning('The url doesn\'t specify the protocol, trying with https')
return self.url_result('http://' + url) return self.url_result('https://' + url)
elif default_search != 'fixup_error': elif default_search != 'fixup_error':
if default_search == 'auto_warning': if default_search == 'auto_warning':
if re.match(r'^(?:url|URL)$', url): if re.match(r'^(?:url|URL)$', url):
@@ -786,9 +786,7 @@ class GenericIE(InfoExtractor):
return self.url_result('ytsearch:' + url) return self.url_result('ytsearch:' + url)
if default_search in ('error', 'fixup_error'): if default_search in ('error', 'fixup_error'):
raise ExtractorError( raise ExtractorError(f'{url!r} is not a valid URL', expected=True)
f'{url!r} is not a valid URL. '
f'Set --default-search "ytsearch" (or run yt-dlp "ytsearch:{url}" ) to search YouTube', expected=True)
else: else:
if ':' not in default_search: if ':' not in default_search:
default_search += ':' default_search += ':'
+14 -10
View File
@@ -12,6 +12,7 @@ from ..utils import (
get_element_html_by_id, get_element_html_by_id,
int_or_none, int_or_none,
lowercase_escape, lowercase_escape,
parse_qs,
try_get, try_get,
update_url_query, update_url_query,
) )
@@ -111,14 +112,18 @@ class GoogleDriveIE(InfoExtractor):
self._caption_formats_ext.append(f.attrib['fmt_code']) self._caption_formats_ext.append(f.attrib['fmt_code'])
def _get_captions_by_type(self, video_id, subtitles_id, caption_type, def _get_captions_by_type(self, video_id, subtitles_id, caption_type,
origin_lang_code=None): origin_lang_code=None, origin_lang_name=None):
if not subtitles_id or not caption_type: if not subtitles_id or not caption_type:
return return
captions = {} captions = {}
for caption_entry in self._captions_xml.findall( for caption_entry in self._captions_xml.findall(
self._CAPTIONS_ENTRY_TAG[caption_type]): self._CAPTIONS_ENTRY_TAG[caption_type]):
caption_lang_code = caption_entry.attrib.get('lang_code') caption_lang_code = caption_entry.attrib.get('lang_code')
if not caption_lang_code: caption_name = caption_entry.attrib.get('name') or origin_lang_name
if not caption_lang_code or not caption_name:
self.report_warning(f'Missing necessary caption metadata. '
f'Need lang_code and name attributes. '
f'Found: {caption_entry.attrib}')
continue continue
caption_format_data = [] caption_format_data = []
for caption_format in self._caption_formats_ext: for caption_format in self._caption_formats_ext:
@@ -129,7 +134,7 @@ class GoogleDriveIE(InfoExtractor):
'lang': (caption_lang_code if origin_lang_code is None 'lang': (caption_lang_code if origin_lang_code is None
else origin_lang_code), else origin_lang_code),
'type': 'track', 'type': 'track',
'name': '', 'name': caption_name,
'kind': '', 'kind': '',
} }
if origin_lang_code is not None: if origin_lang_code is not None:
@@ -155,14 +160,15 @@ class GoogleDriveIE(InfoExtractor):
self._download_subtitles_xml(video_id, subtitles_id, hl) self._download_subtitles_xml(video_id, subtitles_id, hl)
if not self._captions_xml: if not self._captions_xml:
return return
track = self._captions_xml.find('track') track = next((t for t in self._captions_xml.findall('track') if t.attrib.get('cantran') == 'true'), None)
if track is None: if track is None:
return return
origin_lang_code = track.attrib.get('lang_code') origin_lang_code = track.attrib.get('lang_code')
if not origin_lang_code: origin_lang_name = track.attrib.get('name')
if not origin_lang_code or not origin_lang_name:
return return
return self._get_captions_by_type( return self._get_captions_by_type(
video_id, subtitles_id, 'automatic_captions', origin_lang_code) video_id, subtitles_id, 'automatic_captions', origin_lang_code, origin_lang_name)
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
@@ -268,10 +274,8 @@ class GoogleDriveIE(InfoExtractor):
subtitles_id = None subtitles_id = None
ttsurl = get_value('ttsurl') ttsurl = get_value('ttsurl')
if ttsurl: if ttsurl:
# the video Id for subtitles will be the last value in the ttsurl # the subtitles ID is the vid param of the ttsurl query
# query string subtitles_id = parse_qs(ttsurl).get('vid', [None])[-1]
subtitles_id = ttsurl.encode().decode(
'unicode_escape').split('=')[-1]
self.cookiejar.clear(domain='.google.com', path='/', name='NID') self.cookiejar.clear(domain='.google.com', path='/', name='NID')
+3 -1
View File
@@ -18,6 +18,7 @@ from ..utils import (
url_or_none, url_or_none,
urljoin, urljoin,
) )
from ..utils.traversal import traverse_obj
class ITVIE(InfoExtractor): class ITVIE(InfoExtractor):
@@ -223,6 +224,7 @@ class ITVBTCCIE(InfoExtractor):
}, },
'playlist_count': 12, 'playlist_count': 12,
}, { }, {
# news page, can have absent `data` field
'url': 'https://www.itv.com/news/2021-10-27/i-have-to-protect-the-country-says-rishi-sunak-as-uk-faces-interest-rate-hike', 'url': 'https://www.itv.com/news/2021-10-27/i-have-to-protect-the-country-says-rishi-sunak-as-uk-faces-interest-rate-hike',
'info_dict': { 'info_dict': {
'id': 'i-have-to-protect-the-country-says-rishi-sunak-as-uk-faces-interest-rate-hike', 'id': 'i-have-to-protect-the-country-says-rishi-sunak-as-uk-faces-interest-rate-hike',
@@ -243,7 +245,7 @@ class ITVBTCCIE(InfoExtractor):
entries = [] entries = []
for video in json_map: for video in json_map:
if not any(video['data'].get(attr) == 'Brightcove' for attr in ('name', 'type')): if not any(traverse_obj(video, ('data', attr)) == 'Brightcove' for attr in ('name', 'type')):
continue continue
video_id = video['data']['id'] video_id = video['data']['id']
account_id = video['data']['accountId'] account_id = video['data']['accountId']
+34 -12
View File
@@ -95,26 +95,47 @@ class KickVODIE(KickBaseIE):
IE_NAME = 'kick:vod' IE_NAME = 'kick:vod'
_VALID_URL = r'https?://(?:www\.)?kick\.com/[\w-]+/videos/(?P<id>[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12})' _VALID_URL = r'https?://(?:www\.)?kick\.com/[\w-]+/videos/(?P<id>[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12})'
_TESTS = [{ _TESTS = [{
'url': 'https://kick.com/xqc/videos/8dd97a8d-e17f-48fb-8bc3-565f88dbc9ea', # Regular VOD
'md5': '3870f94153e40e7121a6e46c068b70cb', 'url': 'https://kick.com/xqc/videos/5c697a87-afce-4256-b01f-3c8fe71ef5cb',
'info_dict': { 'info_dict': {
'id': '8dd97a8d-e17f-48fb-8bc3-565f88dbc9ea', 'id': '5c697a87-afce-4256-b01f-3c8fe71ef5cb',
'ext': 'mp4', 'ext': 'mp4',
'title': '18+ #ad 🛑LIVE🛑CLICK🛑DRAMA🛑NEWS🛑STUFF🛑REACT🛑GET IN HHERE🛑BOP BOP🛑WEEEE WOOOO🛑', 'title': '🐗LIVE🐗CLICK🐗HERE🐗DRAMA🐗ALL DAY🐗NEWS🐗VIDEOS🐗CLIPS🐗GAMES🐗STUFF🐗WOW🐗IM HERE🐗LETS GO🐗COOL🐗VERY NICE🐗',
'description': 'THE BEST AT ABSOLUTELY EVERYTHING. THE JUICER. LEADER OF THE JUICERS.', 'description': 'THE BEST AT ABSOLUTELY EVERYTHING. THE JUICER. LEADER OF THE JUICERS.',
'channel': 'xqc',
'channel_id': '668',
'uploader': 'xQc', 'uploader': 'xQc',
'uploader_id': '676', 'uploader_id': '676',
'upload_date': '20240909', 'channel': 'xqc',
'timestamp': 1725919141, 'channel_id': '668',
'duration': 10155.0,
'thumbnail': r're:^https?://.*\.jpg',
'view_count': int, 'view_count': int,
'categories': ['Just Chatting'], 'age_limit': 18,
'age_limit': 0, 'duration': 22278.0,
'thumbnail': r're:^https?://.*\.jpg',
'categories': ['Deadlock'],
'timestamp': 1756082443,
'upload_date': '20250825',
}, },
'params': {'skip_download': 'm3u8'}, 'params': {'skip_download': 'm3u8'},
}, {
# VOD of ongoing livestream (at the time of writing the test, ID rotates every two days)
'url': 'https://kick.com/a-log-burner/videos/5230df84-ea38-46e1-be4f-f5949ae55641',
'info_dict': {
'id': '5230df84-ea38-46e1-be4f-f5949ae55641',
'ext': 'mp4',
'title': r're:😴 Cozy Fireplace ASMR 🔥 | Relax, Focus, Sleep 💤',
'description': 'md5:080bc713eac0321a7b376a1b53816d1b',
'uploader': 'A_Log_Burner',
'uploader_id': '65114691',
'channel': 'a-log-burner',
'channel_id': '63967687',
'view_count': int,
'age_limit': 18,
'thumbnail': r're:^https?://.*\.jpg',
'categories': ['Other, Watch Party'],
'timestamp': int,
'upload_date': str,
'live_status': 'is_live',
},
'skip': 'live',
}] }]
def _real_extract(self, url): def _real_extract(self, url):
@@ -137,6 +158,7 @@ class KickVODIE(KickBaseIE):
'categories': ('livestream', 'categories', ..., 'name', {str}), 'categories': ('livestream', 'categories', ..., 'name', {str}),
'view_count': ('views', {int_or_none}), 'view_count': ('views', {int_or_none}),
'age_limit': ('livestream', 'is_mature', {bool}, {lambda x: 18 if x else 0}), 'age_limit': ('livestream', 'is_mature', {bool}, {lambda x: 18 if x else 0}),
'is_live': ('livestream', 'is_live', {bool}),
}), }),
} }
+65 -49
View File
@@ -1,22 +1,14 @@
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
clean_html, clean_html,
merge_dicts,
traverse_obj,
unified_timestamp, unified_timestamp,
url_or_none, url_or_none,
urljoin, urljoin,
) )
from ..utils.traversal import traverse_obj
class LRTBaseIE(InfoExtractor): class LRTStreamIE(InfoExtractor):
def _extract_js_var(self, webpage, var_name, default=None):
return self._search_regex(
fr'{var_name}\s*=\s*(["\'])((?:(?!\1).)+)\1',
webpage, var_name.replace('_', ' '), default, group=2)
class LRTStreamIE(LRTBaseIE):
_VALID_URL = r'https?://(?:www\.)?lrt\.lt/mediateka/tiesiogiai/(?P<id>[\w-]+)' _VALID_URL = r'https?://(?:www\.)?lrt\.lt/mediateka/tiesiogiai/(?P<id>[\w-]+)'
_TESTS = [{ _TESTS = [{
'url': 'https://www.lrt.lt/mediateka/tiesiogiai/lrt-opus', 'url': 'https://www.lrt.lt/mediateka/tiesiogiai/lrt-opus',
@@ -31,86 +23,110 @@ class LRTStreamIE(LRTBaseIE):
def _real_extract(self, url): def _real_extract(self, url):
video_id = self._match_id(url) video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
streams_data = self._download_json(self._extract_js_var(webpage, 'tokenURL'), video_id)
# TODO: Use _search_nextjs_v13_data once fixed
get_stream_url = self._search_regex(
r'\\"get_streams_url\\":\\"([^"]+)\\"', webpage, 'stream URL')
streams_data = self._download_json(get_stream_url, video_id)
formats, subtitles = [], {} formats, subtitles = [], {}
for stream_url in traverse_obj(streams_data, ( for stream_url in traverse_obj(streams_data, (
'response', 'data', lambda k, _: k.startswith('content')), expected_type=url_or_none): 'response', 'data', lambda k, _: k.startswith('content'), {url_or_none})):
fmts, subs = self._extract_m3u8_formats_and_subtitles(stream_url, video_id, 'mp4', m3u8_id='hls', live=True) fmts, subs = self._extract_m3u8_formats_and_subtitles(
stream_url, video_id, 'mp4', m3u8_id='hls', live=True)
formats.extend(fmts) formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs) subtitles = self._merge_subtitles(subtitles, subs)
stream_title = self._extract_js_var(webpage, 'video_title', 'LRT')
return { return {
'id': video_id, 'id': video_id,
'formats': formats, 'formats': formats,
'subtitles': subtitles, 'subtitles': subtitles,
'is_live': True, 'is_live': True,
'title': f'{self._og_search_title(webpage)} - {stream_title}', 'title': self._og_search_title(webpage),
} }
class LRTVODIE(LRTBaseIE): class LRTVODIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?lrt\.lt(?P<path>/mediateka/irasas/(?P<id>[0-9]+))' _VALID_URL = [
r'https?://(?:(?:www|archyvai)\.)?lrt\.lt/mediateka/irasas/(?P<id>[0-9]+)',
r'https?://(?:(?:www|archyvai)\.)?lrt\.lt/mediateka/video/[^?#]+\?(?:[^#]*&)?episode=(?P<id>[0-9]+)',
]
_TESTS = [{ _TESTS = [{
# m3u8 download # m3u8 download
'url': 'https://www.lrt.lt/mediateka/irasas/2000127261/greita-ir-gardu-sicilijos-ikvepta-klasikiniu-makaronu-su-baklazanais-vakariene', 'url': 'https://www.lrt.lt/mediateka/irasas/2000127261/greita-ir-gardu-sicilijos-ikvepta-klasikiniu-makaronu-su-baklazanais-vakariene',
'info_dict': { 'info_dict': {
'id': '2000127261', 'id': '2000127261',
'ext': 'mp4', 'ext': 'mp4',
'title': 'Greita ir gardu: Sicilijos įkvėpta klasikinių makaronų su baklažanais vakarienė', 'title': 'Nustebinkite svečius klasikiniu makaronų su baklažanais receptu',
'description': 'md5:ad7d985f51b0dc1489ba2d76d7ed47fa', 'description': 'md5:ad7d985f51b0dc1489ba2d76d7ed47fa',
'duration': 3035, 'timestamp': 1604086200,
'timestamp': 1604079000,
'upload_date': '20201030', 'upload_date': '20201030',
'tags': ['LRT TELEVIZIJA', 'Beatos virtuvė', 'Beata Nicholson', 'Makaronai', 'Baklažanai', 'Vakarienė', 'Receptas'], 'tags': ['LRT TELEVIZIJA', 'Beatos virtuvė', 'Beata Nicholson', 'Makaronai', 'Baklažanai', 'Vakarienė', 'Receptas'],
'thumbnail': 'https://www.lrt.lt/img/2020/10/30/764041-126478-1287x836.jpg', 'thumbnail': 'https://www.lrt.lt/img/2020/10/30/764041-126478-1287x836.jpg',
'channel': 'Beatos virtuvė',
}, },
}, { }, {
# direct mp3 download # audio download
'url': 'http://www.lrt.lt/mediateka/irasas/1013074524/', 'url': 'https://www.lrt.lt/mediateka/irasas/1013074524/kita-tema',
'md5': '389da8ca3cad0f51d12bed0c844f6a0a', 'md5': 'fc982f10274929c66fdff65f75615cb0',
'info_dict': { 'info_dict': {
'id': '1013074524', 'id': '1013074524',
'ext': 'mp3', 'ext': 'mp4',
'title': 'Kita tema 2016-09-05 15:05', 'title': 'Kita tema',
'description': 'md5:1b295a8fc7219ed0d543fc228c931fb5', 'description': 'md5:1b295a8fc7219ed0d543fc228c931fb5',
'duration': 3008, 'channel': 'Kita tema',
'view_count': int, 'timestamp': 1473087900,
'like_count': int, 'upload_date': '20160905',
}, },
}, {
'url': 'https://www.lrt.lt/mediateka/video/auksinis-protas-vasara?episode=2000420320&season=%2Fmediateka%2Fvideo%2Fauksinis-protas-vasara%2F2025',
'info_dict': {
'id': '2000420320',
'ext': 'mp4',
'title': 'Kuris senovės romėnų poetas aprašė Narcizo mitą?',
'description': 'Intelektinė viktorina. Ved. Arūnas Valinskas ir Andrius Tapinas.',
'channel': 'Auksinis protas. Vasara',
'thumbnail': 'https://www.lrt.lt/img/2025/06/09/2094343-987905-1287x836.jpg',
'tags': ['LRT TELEVIZIJA', 'Auksinis protas'],
'timestamp': 1749851040,
'upload_date': '20250613',
},
}, {
'url': 'https://archyvai.lrt.lt/mediateka/video/ziniu-riteriai-ir-damos?episode=49685&season=%2Fmediateka%2Fvideo%2Fziniu-riteriai-ir-damos%2F2013',
'only_matching': True,
}, {
'url': 'https://archyvai.lrt.lt/mediateka/irasas/2000077058/panorama-1989-baltijos-kelias',
'only_matching': True,
}] }]
def _real_extract(self, url): def _real_extract(self, url):
path, video_id = self._match_valid_url(url).group('path', 'id') video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id) webpage = self._download_webpage(url, video_id)
media_url = self._extract_js_var(webpage, 'main_url', path) # TODO: Use _search_nextjs_v13_data once fixed
media = self._download_json(self._extract_js_var( canonical_url = (
webpage, 'media_info_url', self._search_regex(r'\\"(?:article|data)\\":{[^}]*\\"url\\":\\"(/[^"]+)\\"', webpage, 'content URL', fatal=False)
'https://www.lrt.lt/servisai/stream_url/vod/media_info/'), or self._search_regex(r'<link\s+rel="canonical"\s*href="(/[^"]+)"', webpage, 'canonical URL'))
video_id, query={'url': media_url})
media = self._download_json(
'https://www.lrt.lt/servisai/stream_url/vod/media_info/',
video_id, query={'url': canonical_url})
jw_data = self._parse_jwplayer_data( jw_data = self._parse_jwplayer_data(
media['playlist_item'], video_id, base_url=url) media['playlist_item'], video_id, base_url=url)
json_ld_data = self._search_json_ld(webpage, video_id) return {
**jw_data,
tags = [] **traverse_obj(media, {
for tag in (media.get('tags') or []): 'id': ('id', {str}),
tag_name = tag.get('name') 'title': ('title', {str}),
if not tag_name: 'description': ('content', {clean_html}),
continue 'timestamp': ('date', {lambda x: x.replace('.', '/')}, {unified_timestamp}),
tags.append(tag_name) 'tags': ('tags', ..., 'name', {str}),
}),
clean_info = {
'description': clean_html(media.get('content')),
'tags': tags,
} }
return merge_dicts(clean_info, jw_data, json_ld_data)
class LRTRadioIE(InfoExtractor):
class LRTRadioIE(LRTBaseIE):
_VALID_URL = r'https?://(?:www\.)?lrt\.lt/radioteka/irasas/(?P<id>\d+)/(?P<path>[^?#/]+)' _VALID_URL = r'https?://(?:www\.)?lrt\.lt/radioteka/irasas/(?P<id>\d+)/(?P<path>[^?#/]+)'
_TESTS = [{ _TESTS = [{
# m3u8 download # m3u8 download
+5 -1
View File
@@ -111,8 +111,12 @@ class MySpaceIE(InfoExtractor):
search_data('stream-url'), search_data('hls-stream-url'), search_data('stream-url'), search_data('hls-stream-url'),
search_data('http-stream-url')) search_data('http-stream-url'))
if not formats: if not formats:
vevo_id = search_data('vevo-id')
youtube_id = search_data('youtube-id') youtube_id = search_data('youtube-id')
if youtube_id: if vevo_id:
self.to_screen(f'Vevo video detected: {vevo_id}')
return self.url_result(f'vevo:{vevo_id}', ie='Vevo')
elif youtube_id:
self.to_screen(f'Youtube video detected: {youtube_id}') self.to_screen(f'Youtube video detected: {youtube_id}')
return self.url_result(youtube_id, ie='Youtube') return self.url_result(youtube_id, ie='Youtube')
else: else:
+16 -2
View File
@@ -51,6 +51,20 @@ class SkebIE(InfoExtractor):
}, },
'playlist_count': 2, 'playlist_count': 2,
'expected_warnings': ['Skipping unsupported extension'], 'expected_warnings': ['Skipping unsupported extension'],
}, {
'url': 'https://skeb.jp/@Yossshy_Music/works/13',
'info_dict': {
'ext': 'wav',
'id': '5566495',
'title': '13-1',
'description': 'md5:1026b8b9ae38c67c2d995970ec196550',
'uploader': 'Yossshy',
'uploader_id': 'Yossshy_Music',
'duration': 336,
'thumbnail': r're:https?://.+',
'tags': 'count:59',
'genres': ['music'],
},
}] }]
def _call_api(self, uploader_id, work_id): def _call_api(self, uploader_id, work_id):
@@ -87,7 +101,7 @@ class SkebIE(InfoExtractor):
entries = [] entries = []
for idx, preview in enumerate(traverse_obj(works, ('previews', lambda _, v: url_or_none(v['url']))), 1): for idx, preview in enumerate(traverse_obj(works, ('previews', lambda _, v: url_or_none(v['url']))), 1):
ext = traverse_obj(preview, ('information', 'extension', {str})) ext = traverse_obj(preview, ('information', 'extension', {str}))
if ext not in ('mp3', 'mp4'): if ext not in ('mp3', 'mp4', 'wav'):
self.report_warning(f'Skipping unsupported extension "{ext}"') self.report_warning(f'Skipping unsupported extension "{ext}"')
continue continue
@@ -100,7 +114,7 @@ class SkebIE(InfoExtractor):
'url': preview['vtt_url'], 'url': preview['vtt_url'],
}], }],
} if url_or_none(preview.get('vtt_url')) else None, } if url_or_none(preview.get('vtt_url')) else None,
'vcodec': 'none' if ext == 'mp3' else None, 'vcodec': 'none' if ext in ('mp3', 'wav') else None,
**info, **info,
**traverse_obj(preview, { **traverse_obj(preview, {
'id': ('id', {str_or_none}), 'id': ('id', {str_or_none}),
+6
View File
@@ -45,6 +45,8 @@ class TVerIE(StreaksBaseIE):
'release_timestamp': 1651453200, 'release_timestamp': 1651453200,
'release_date': '20220502', 'release_date': '20220502',
'_old_archive_ids': ['brightcovenew ref:baeebeac-a2a6-4dbf-9eb3-c40d59b40068'], '_old_archive_ids': ['brightcovenew ref:baeebeac-a2a6-4dbf-9eb3-c40d59b40068'],
'series_id': 'sru35hwdd2',
'season_id': 'ss2lcn4af6',
}, },
}, { }, {
# via Brightcove backend (deprecated) # via Brightcove backend (deprecated)
@@ -67,6 +69,8 @@ class TVerIE(StreaksBaseIE):
'upload_date': '20220501', 'upload_date': '20220501',
'release_timestamp': 1651453200, 'release_timestamp': 1651453200,
'release_date': '20220502', 'release_date': '20220502',
'series_id': 'sru35hwdd2',
'season_id': 'ss2lcn4af6',
}, },
'params': {'extractor_args': {'tver': {'backend': ['brightcove']}}}, 'params': {'extractor_args': {'tver': {'backend': ['brightcove']}}},
}, { }, {
@@ -202,6 +206,8 @@ class TVerIE(StreaksBaseIE):
'description': ('description', {str}), 'description': ('description', {str}),
'release_timestamp': ('viewStatus', 'startAt', {int_or_none}), 'release_timestamp': ('viewStatus', 'startAt', {int_or_none}),
'episode_number': ('no', {int_or_none}), 'episode_number': ('no', {int_or_none}),
'series_id': ('seriesID', {str}),
'season_id': ('seasonID', {str}),
}), }),
} }
+352
View File
@@ -0,0 +1,352 @@
import json
import re
from .common import InfoExtractor
from ..networking.exceptions import HTTPError
from ..utils import (
ExtractorError,
int_or_none,
parse_iso8601,
parse_qs,
)
class VevoBaseIE(InfoExtractor):
def _extract_json(self, webpage, video_id):
return self._parse_json(
self._search_regex(
r'window\.__INITIAL_STORE__\s*=\s*({.+?});\s*</script>',
webpage, 'initial store'),
video_id)
class VevoIE(VevoBaseIE):
"""
Accepts urls from vevo.com or in the format 'vevo:{id}'
(currently used by MTVIE and MySpaceIE)
"""
_VALID_URL = r'''(?x)
(?:https?://(?:www\.)?vevo\.com/watch/(?!playlist|genre)(?:[^/]+/(?:[^/]+/)?)?|
https?://cache\.vevo\.com/m/html/embed\.html\?video=|
https?://videoplayer\.vevo\.com/embed/embedded\?videoId=|
https?://embed\.vevo\.com/.*?[?&]isrc=|
https?://tv\.vevo\.com/watch/artist/(?:[^/]+)/|
vevo:)
(?P<id>[^&?#]+)'''
_EMBED_REGEX = [r'<iframe[^>]+?src=(["\'])(?P<url>(?:https?:)?//(?:cache\.)?vevo\.com/.+?)\1']
_TESTS = [{
'url': 'http://www.vevo.com/watch/hurts/somebody-to-die-for/GB1101300280',
'md5': '95ee28ee45e70130e3ab02b0f579ae23',
'info_dict': {
'id': 'GB1101300280',
'ext': 'mp4',
'title': 'Hurts - Somebody to Die For',
'timestamp': 1372057200,
'upload_date': '20130624',
'uploader': 'Hurts',
'track': 'Somebody to Die For',
'artist': 'Hurts',
'genre': 'Pop',
},
'expected_warnings': ['Unable to download SMIL file', 'Unable to download info'],
}, {
'note': 'v3 SMIL format',
'url': 'http://www.vevo.com/watch/cassadee-pope/i-wish-i-could-break-your-heart/USUV71302923',
'md5': 'f6ab09b034f8c22969020b042e5ac7fc',
'info_dict': {
'id': 'USUV71302923',
'ext': 'mp4',
'title': 'Cassadee Pope - I Wish I Could Break Your Heart',
'timestamp': 1392796919,
'upload_date': '20140219',
'uploader': 'Cassadee Pope',
'track': 'I Wish I Could Break Your Heart',
'artist': 'Cassadee Pope',
'genre': 'Country',
},
'expected_warnings': ['Unable to download SMIL file', 'Unable to download info'],
}, {
'note': 'Age-limited video',
'url': 'https://www.vevo.com/watch/justin-timberlake/tunnel-vision-explicit/USRV81300282',
'info_dict': {
'id': 'USRV81300282',
'ext': 'mp4',
'title': 'Justin Timberlake - Tunnel Vision (Explicit)',
'age_limit': 18,
'timestamp': 1372888800,
'upload_date': '20130703',
'uploader': 'Justin Timberlake',
'track': 'Tunnel Vision (Explicit)',
'artist': 'Justin Timberlake',
'genre': 'Pop',
},
'expected_warnings': ['Unable to download SMIL file', 'Unable to download info'],
}, {
'note': 'No video_info',
'url': 'http://www.vevo.com/watch/k-camp-1/Till-I-Die/USUV71503000',
'md5': '8b83cc492d72fc9cf74a02acee7dc1b0',
'info_dict': {
'id': 'USUV71503000',
'ext': 'mp4',
'title': 'K Camp ft. T.I. - Till I Die',
'age_limit': 18,
'timestamp': 1449468000,
'upload_date': '20151207',
'uploader': 'K Camp',
'track': 'Till I Die',
'artist': 'K Camp',
'genre': 'Hip-Hop',
},
'expected_warnings': ['Unable to download SMIL file', 'Unable to download info'],
}, {
'note': 'Featured test',
'url': 'https://www.vevo.com/watch/lemaitre/Wait/USUV71402190',
'md5': 'd28675e5e8805035d949dc5cf161071d',
'info_dict': {
'id': 'USUV71402190',
'ext': 'mp4',
'title': 'Lemaitre ft. LoLo - Wait',
'age_limit': 0,
'timestamp': 1413432000,
'upload_date': '20141016',
'uploader': 'Lemaitre',
'track': 'Wait',
'artist': 'Lemaitre',
'genre': 'Electronic',
},
'expected_warnings': ['Unable to download SMIL file', 'Unable to download info'],
}, {
'note': 'Only available via webpage',
'url': 'http://www.vevo.com/watch/GBUV71600656',
'md5': '67e79210613865b66a47c33baa5e37fe',
'info_dict': {
'id': 'GBUV71600656',
'ext': 'mp4',
'title': 'ABC - Viva Love',
'age_limit': 0,
'timestamp': 1461830400,
'upload_date': '20160428',
'uploader': 'ABC',
'track': 'Viva Love',
'artist': 'ABC',
'genre': 'Pop',
},
'expected_warnings': ['Failed to download video versions info'],
}, {
# no genres available
'url': 'http://www.vevo.com/watch/INS171400764',
'only_matching': True,
}, {
# Another case available only via the webpage; using streams/streamsV3 formats
# Geo-restricted to Netherlands/Germany
'url': 'http://www.vevo.com/watch/boostee/pop-corn-clip-officiel/FR1A91600909',
'only_matching': True,
}, {
'url': 'https://embed.vevo.com/?isrc=USH5V1923499&partnerId=4d61b777-8023-4191-9ede-497ed6c24647&partnerAdCode=',
'only_matching': True,
}, {
'url': 'https://tv.vevo.com/watch/artist/janet-jackson/US0450100550',
'only_matching': True,
}]
_VERSIONS = {
0: 'youtube', # only in AuthenticateVideo videoVersions
1: 'level3',
2: 'akamai',
3: 'level3',
4: 'amazon',
}
def _initialize_api(self, video_id):
webpage = self._download_webpage(
'https://accounts.vevo.com/token', None,
note='Retrieving oauth token',
errnote='Unable to retrieve oauth token',
data=json.dumps({
'client_id': 'SPupX1tvqFEopQ1YS6SS',
'grant_type': 'urn:vevo:params:oauth:grant-type:anonymous',
}).encode(),
headers={
'Content-Type': 'application/json',
})
if re.search(r'(?i)THIS PAGE IS CURRENTLY UNAVAILABLE IN YOUR REGION', webpage):
self.raise_geo_restricted(
f'{self.IE_NAME} said: This page is currently unavailable in your region')
auth_info = self._parse_json(webpage, video_id)
self._api_url_template = self.http_scheme() + '//apiv2.vevo.com/%s?token=' + auth_info['legacy_token']
def _call_api(self, path, *args, **kwargs):
try:
data = self._download_json(self._api_url_template % path, *args, **kwargs)
except ExtractorError as e:
if isinstance(e.cause, HTTPError):
errors = self._parse_json(e.cause.response.read().decode(), None)['errors']
error_message = ', '.join([error['message'] for error in errors])
raise ExtractorError(f'{self.IE_NAME} said: {error_message}', expected=True)
raise
return data
def _real_extract(self, url):
video_id = self._match_id(url)
self._initialize_api(video_id)
video_info = self._call_api(
f'video/{video_id}', video_id, 'Downloading api video info',
'Failed to download video info')
video_versions = self._call_api(
f'video/{video_id}/streams', video_id,
'Downloading video versions info',
'Failed to download video versions info',
fatal=False)
# Some videos are only available via webpage (e.g.
# https://github.com/ytdl-org/youtube-dl/issues/9366)
if not video_versions:
webpage = self._download_webpage(url, video_id)
json_data = self._extract_json(webpage, video_id)
if 'streams' in json_data.get('default', {}):
video_versions = json_data['default']['streams'][video_id][0]
else:
video_versions = [
value
for key, value in json_data['apollo']['data'].items()
if key.startswith(f'{video_id}.streams')]
uploader = None
artist = None
featured_artist = None
artists = video_info.get('artists')
for curr_artist in artists:
if curr_artist.get('role') == 'Featured':
featured_artist = curr_artist['name']
else:
artist = uploader = curr_artist['name']
formats = []
for video_version in video_versions:
version = self._VERSIONS.get(video_version.get('version'), 'generic')
version_url = video_version.get('url')
if not version_url:
continue
if '.ism' in version_url:
continue
elif '.mpd' in version_url:
formats.extend(self._extract_mpd_formats(
version_url, video_id, mpd_id=f'dash-{version}',
note=f'Downloading {version} MPD information',
errnote=f'Failed to download {version} MPD information',
fatal=False))
elif '.m3u8' in version_url:
formats.extend(self._extract_m3u8_formats(
version_url, video_id, 'mp4', 'm3u8_native',
m3u8_id=f'hls-{version}',
note=f'Downloading {version} m3u8 information',
errnote=f'Failed to download {version} m3u8 information',
fatal=False))
else:
m = re.search(r'''(?xi)
_(?P<quality>[a-z0-9]+)
_(?P<width>[0-9]+)x(?P<height>[0-9]+)
_(?P<vcodec>[a-z0-9]+)
_(?P<vbr>[0-9]+)
_(?P<acodec>[a-z0-9]+)
_(?P<abr>[0-9]+)
\.(?P<ext>[a-z0-9]+)''', version_url)
if not m:
continue
formats.append({
'url': version_url,
'format_id': f'http-{version}-{video_version.get("quality") or m.group("quality")}',
'vcodec': m.group('vcodec'),
'acodec': m.group('acodec'),
'vbr': int(m.group('vbr')),
'abr': int(m.group('abr')),
'ext': m.group('ext'),
'width': int(m.group('width')),
'height': int(m.group('height')),
})
track = video_info['title']
if featured_artist:
artist = f'{artist} ft. {featured_artist}'
title = f'{artist} - {track}' if artist else track
genres = video_info.get('genres')
genre = (
genres[0] if genres and isinstance(genres, list)
and isinstance(genres[0], str) else None)
is_explicit = video_info.get('isExplicit')
if is_explicit is True:
age_limit = 18
elif is_explicit is False:
age_limit = 0
else:
age_limit = None
return {
'id': video_id,
'title': title,
'formats': formats,
'thumbnail': video_info.get('imageUrl') or video_info.get('thumbnailUrl'),
'timestamp': parse_iso8601(video_info.get('releaseDate')),
'uploader': uploader,
'duration': int_or_none(video_info.get('duration')),
'view_count': int_or_none(video_info.get('views', {}).get('total')),
'age_limit': age_limit,
'track': track,
'artist': uploader,
'genre': genre,
}
class VevoPlaylistIE(VevoBaseIE):
_VALID_URL = r'https?://(?:www\.)?vevo\.com/watch/(?P<kind>playlist|genre)/(?P<id>[^/?#&]+)'
_TESTS = [{
'url': 'http://www.vevo.com/watch/genre/rock',
'info_dict': {
'id': 'rock',
'title': 'Rock',
},
'playlist_count': 20,
}, {
'url': 'http://www.vevo.com/watch/genre/rock?index=0',
'only_matching': True,
}]
def _real_extract(self, url):
mobj = self._match_valid_url(url)
playlist_id = mobj.group('id')
playlist_kind = mobj.group('kind')
webpage = self._download_webpage(url, playlist_id)
qs = parse_qs(url)
index = qs.get('index', [None])[0]
if index:
video_id = self._search_regex(
r'<meta[^>]+content=(["\'])vevo://video/(?P<id>.+?)\1[^>]*>',
webpage, 'video id', default=None, group='id')
if video_id:
return self.url_result(f'vevo:{video_id}', VevoIE.ie_key())
playlists = self._extract_json(webpage, playlist_id)['default'][f'{playlist_kind}s']
playlist = (next(iter(playlists.values()))
if playlist_kind == 'playlist' else playlists[playlist_id])
entries = [
self.url_result(f'vevo:{src}', VevoIE.ie_key())
for src in playlist['isrcs']]
return self.playlist_result(
entries, playlist.get('playlistId') or playlist_id,
playlist.get('name'), playlist.get('description'))
+14 -4
View File
@@ -305,12 +305,13 @@ INNERTUBE_CLIENTS = {
'client': { 'client': {
'clientName': 'TVHTML5', 'clientName': 'TVHTML5',
'clientVersion': '7.20250312.16.00', 'clientVersion': '7.20250312.16.00',
# See: https://github.com/youtube/cobalt/blob/main/cobalt/browser/user_agent/user_agent_platform_info.cc#L506 'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version',
'userAgent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/25.lts.30.1034943-gold (unlike Gecko), Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)',
}, },
}, },
'INNERTUBE_CONTEXT_CLIENT_NAME': 7, 'INNERTUBE_CONTEXT_CLIENT_NAME': 7,
'SUPPORTS_COOKIES': True, 'SUPPORTS_COOKIES': True,
# See: https://github.com/youtube/cobalt/blob/main/cobalt/browser/user_agent/user_agent_platform_info.cc#L506
'AUTHENTICATED_USER_AGENT': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/25.lts.30.1034943-gold (unlike Gecko), Unknown_TV_Unknown_0/Unknown (Unknown, Unknown)',
}, },
'tv_simply': { 'tv_simply': {
'INNERTUBE_CONTEXT': { 'INNERTUBE_CONTEXT': {
@@ -369,6 +370,7 @@ def build_innertube_clients():
ytcfg.setdefault('REQUIRE_AUTH', False) ytcfg.setdefault('REQUIRE_AUTH', False)
ytcfg.setdefault('SUPPORTS_COOKIES', False) ytcfg.setdefault('SUPPORTS_COOKIES', False)
ytcfg.setdefault('PLAYER_PARAMS', None) ytcfg.setdefault('PLAYER_PARAMS', None)
ytcfg.setdefault('AUTHENTICATED_USER_AGENT', None)
ytcfg['INNERTUBE_CONTEXT']['client'].setdefault('hl', 'en') ytcfg['INNERTUBE_CONTEXT']['client'].setdefault('hl', 'en')
_, base_client, variant = _split_innertube_client(client) _, base_client, variant = _split_innertube_client(client)
@@ -656,7 +658,14 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
_YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*=' _YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*='
def _get_default_ytcfg(self, client='web'): def _get_default_ytcfg(self, client='web'):
return copy.deepcopy(INNERTUBE_CLIENTS[client]) ytcfg = copy.deepcopy(INNERTUBE_CLIENTS[client])
# Currently, only the tv client needs to use an alternative user-agent when logged-in
if ytcfg.get('AUTHENTICATED_USER_AGENT') and self.is_authenticated:
client_context = ytcfg.setdefault('INNERTUBE_CONTEXT', {}).setdefault('client', {})
client_context['userAgent'] = ytcfg['AUTHENTICATED_USER_AGENT']
return ytcfg
def _get_innertube_host(self, client='web'): def _get_innertube_host(self, client='web'):
return INNERTUBE_CLIENTS[client]['INNERTUBE_HOST'] return INNERTUBE_CLIENTS[client]['INNERTUBE_HOST']
@@ -955,7 +964,8 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
ytcfg = self.extract_ytcfg(video_id, webpage) or {} ytcfg = self.extract_ytcfg(video_id, webpage) or {}
# Workaround for https://github.com/yt-dlp/yt-dlp/issues/12563 # Workaround for https://github.com/yt-dlp/yt-dlp/issues/12563
if client == 'tv': # But it's not effective when logged-in
if client == 'tv' and not self.is_authenticated:
config_info = traverse_obj(ytcfg, ( config_info = traverse_obj(ytcfg, (
'INNERTUBE_CONTEXT', 'client', 'configInfo', {dict})) or {} 'INNERTUBE_CONTEXT', 'client', 'configInfo', {dict})) or {}
config_info.pop('appInstallData', None) config_info.pop('appInstallData', None)
+6 -1
View File
@@ -257,7 +257,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
'401': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'}, '401': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
} }
_SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt') _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'srt', 'vtt')
_DEFAULT_CLIENTS = ('tv', 'tv_simply', 'web') _DEFAULT_CLIENTS = ('tv_simply', 'tv', 'web')
_DEFAULT_AUTHED_CLIENTS = ('tv', 'web_safari', 'web') _DEFAULT_AUTHED_CLIENTS = ('tv', 'web_safari', 'web')
# Premium does not require POT (except for subtitles) # Premium does not require POT (except for subtitles)
_DEFAULT_PREMIUM_CLIENTS = ('tv', 'web_creator', 'web') _DEFAULT_PREMIUM_CLIENTS = ('tv', 'web_creator', 'web')
@@ -1817,6 +1817,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
_PLAYER_JS_VARIANT_MAP = { _PLAYER_JS_VARIANT_MAP = {
'main': 'player_ias.vflset/en_US/base.js', 'main': 'player_ias.vflset/en_US/base.js',
'tcc': 'player_ias_tcc.vflset/en_US/base.js',
'tce': 'player_ias_tce.vflset/en_US/base.js', 'tce': 'player_ias_tce.vflset/en_US/base.js',
'es5': 'player_es5.vflset/en_US/base.js', 'es5': 'player_es5.vflset/en_US/base.js',
'es6': 'player_es6.vflset/en_US/base.js', 'es6': 'player_es6.vflset/en_US/base.js',
@@ -3610,6 +3611,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
if f.get('source_preference') is None: if f.get('source_preference') is None:
f['source_preference'] = -1 f['source_preference'] = -1
# Deprioritize since its pre-merged m3u8 formats may have lower quality audio streams
if client_name == 'web_safari' and proto == 'hls' and live_status != 'is_live':
f['source_preference'] -= 1
if missing_pot: if missing_pot:
f['format_note'] = join_nonempty(f.get('format_note'), 'MISSING POT', delim=' ') f['format_note'] = join_nonempty(f.get('format_note'), 'MISSING POT', delim=' ')
f['source_preference'] -= 20 f['source_preference'] -= 20
+3 -3
View File
@@ -1,8 +1,8 @@
# Autogenerated by devscripts/update-version.py # Autogenerated by devscripts/update-version.py
__version__ = '2025.08.22' __version__ = '2025.08.27'
RELEASE_GIT_HEAD = '5c8bcfdbc638dfde13e93157637d8521413ed774' RELEASE_GIT_HEAD = '8cd37b85d492edb56a4f7506ea05527b85a6b02b'
VARIANT = None VARIANT = None
@@ -12,4 +12,4 @@ CHANNEL = 'stable'
ORIGIN = 'yt-dlp/yt-dlp' ORIGIN = 'yt-dlp/yt-dlp'
_pkg_version = '2025.08.22' _pkg_version = '2025.08.27'