mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-08-28 14:53:40 +03:00
Merge branch 'master' into jsi
This commit is contained in:
@@ -417,6 +417,8 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
|
||||
_NETRC_MACHINE = 'youtube'
|
||||
|
||||
_COOKIE_HOWTO_WIKI_URL = 'https://github.com/yt-dlp/yt-dlp/wiki/Extractors#exporting-youtube-cookies'
|
||||
|
||||
def ucid_or_none(self, ucid):
|
||||
return self._search_regex(rf'^({self._YT_CHANNEL_UCID_RE})$', ucid, 'UC-id', default=None)
|
||||
|
||||
@@ -451,17 +453,15 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
return preferred_lang
|
||||
|
||||
def _initialize_consent(self):
|
||||
cookies = self._get_cookies('https://www.youtube.com/')
|
||||
if cookies.get('__Secure-3PSID'):
|
||||
if self._has_auth_cookies:
|
||||
return
|
||||
socs = cookies.get('SOCS')
|
||||
socs = self._youtube_cookies.get('SOCS')
|
||||
if socs and not socs.value.startswith('CAA'): # not consented
|
||||
return
|
||||
self._set_cookie('.youtube.com', 'SOCS', 'CAI', secure=True) # accept all (required for mixes)
|
||||
|
||||
def _initialize_pref(self):
|
||||
cookies = self._get_cookies('https://www.youtube.com/')
|
||||
pref_cookie = cookies.get('PREF')
|
||||
pref_cookie = self._youtube_cookies.get('PREF')
|
||||
pref = {}
|
||||
if pref_cookie:
|
||||
try:
|
||||
@@ -472,8 +472,9 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
self._set_cookie('.youtube.com', name='PREF', value=urllib.parse.urlencode(pref))
|
||||
|
||||
def _initialize_cookie_auth(self):
|
||||
yt_sapisid, yt_1psapisid, yt_3psapisid = self._get_sid_cookies()
|
||||
if yt_sapisid or yt_1psapisid or yt_3psapisid:
|
||||
self._passed_auth_cookies = False
|
||||
if self._has_auth_cookies:
|
||||
self._passed_auth_cookies = True
|
||||
self.write_debug('Found YouTube account cookies')
|
||||
|
||||
def _real_initialize(self):
|
||||
@@ -492,8 +493,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
|
||||
@property
|
||||
def _youtube_login_hint(self):
|
||||
return (f'{self._login_hint(method="cookies")}. Also see '
|
||||
'https://github.com/yt-dlp/yt-dlp/wiki/Extractors#exporting-youtube-cookies '
|
||||
return (f'{self._login_hint(method="cookies")}. Also see {self._COOKIE_HOWTO_WIKI_URL} '
|
||||
'for tips on effectively exporting YouTube cookies')
|
||||
|
||||
def _check_login_required(self):
|
||||
@@ -553,12 +553,16 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
|
||||
return f'{scheme} {"_".join(parts)}'
|
||||
|
||||
@property
|
||||
def _youtube_cookies(self):
|
||||
return self._get_cookies('https://www.youtube.com')
|
||||
|
||||
def _get_sid_cookies(self):
|
||||
"""
|
||||
Get SAPISID, 1PSAPISID, 3PSAPISID cookie values
|
||||
@returns sapisid, 1psapisid, 3psapisid
|
||||
"""
|
||||
yt_cookies = self._get_cookies('https://www.youtube.com')
|
||||
yt_cookies = self._youtube_cookies
|
||||
yt_sapisid = try_call(lambda: yt_cookies['SAPISID'].value)
|
||||
yt_3papisid = try_call(lambda: yt_cookies['__Secure-3PAPISID'].value)
|
||||
yt_1papisid = try_call(lambda: yt_cookies['__Secure-1PAPISID'].value)
|
||||
@@ -595,6 +599,31 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
|
||||
return ' '.join(authorizations)
|
||||
|
||||
@property
|
||||
def is_authenticated(self):
|
||||
return self._has_auth_cookies
|
||||
|
||||
@property
|
||||
def _has_auth_cookies(self):
|
||||
yt_sapisid, yt_1psapisid, yt_3psapisid = self._get_sid_cookies()
|
||||
# YouTube doesn't appear to clear 3PSAPISID when rotating cookies (as of 2025-04-26)
|
||||
# But LOGIN_INFO is cleared and should exist if logged in
|
||||
has_login_info = 'LOGIN_INFO' in self._youtube_cookies
|
||||
return bool(has_login_info and (yt_sapisid or yt_1psapisid or yt_3psapisid))
|
||||
|
||||
def _request_webpage(self, *args, **kwargs):
|
||||
response = super()._request_webpage(*args, **kwargs)
|
||||
|
||||
# Check that we are still logged-in and cookies have not rotated after every request
|
||||
if getattr(self, '_passed_auth_cookies', None) and not self._has_auth_cookies:
|
||||
self.report_warning(
|
||||
'The provided YouTube account cookies are no longer valid. '
|
||||
'They have likely been rotated in the browser as a security measure. '
|
||||
f'For tips on how to effectively export YouTube cookies, refer to {self._COOKIE_HOWTO_WIKI_URL} .',
|
||||
only_once=False)
|
||||
|
||||
return response
|
||||
|
||||
def _call_api(self, ep, query, video_id, fatal=True, headers=None,
|
||||
note='Downloading API JSON', errnote='Unable to download API page',
|
||||
context=None, api_key=None, api_hostname=None, default_client='web'):
|
||||
@@ -695,10 +724,6 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
args, [('VISITOR_DATA', ('INNERTUBE_CONTEXT', 'client', 'visitorData'), ('responseContext', 'visitorData'))],
|
||||
expected_type=str)
|
||||
|
||||
@functools.cached_property
|
||||
def is_authenticated(self):
|
||||
return bool(self._get_sid_authorization_header())
|
||||
|
||||
def extract_ytcfg(self, video_id, webpage):
|
||||
if not webpage:
|
||||
return {}
|
||||
|
||||
@@ -37,6 +37,7 @@ class YoutubeClipIE(YoutubeTabBaseInfoExtractor):
|
||||
'chapters': 'count:20',
|
||||
'comment_count': int,
|
||||
'heatmap': 'count:100',
|
||||
'media_type': 'clip',
|
||||
},
|
||||
}]
|
||||
|
||||
@@ -59,6 +60,7 @@ class YoutubeClipIE(YoutubeTabBaseInfoExtractor):
|
||||
'url': f'https://www.youtube.com/watch?v={video_id}',
|
||||
'ie_key': YoutubeIE.ie_key(),
|
||||
'id': clip_id,
|
||||
'media_type': 'clip',
|
||||
'section_start': int(clip_data['startTimeMs']) / 1000,
|
||||
'section_end': int(clip_data['endTimeMs']) / 1000,
|
||||
'_format_sort_fields': ( # https protocol is prioritized for ffmpeg compatibility
|
||||
|
||||
@@ -35,6 +35,7 @@ class YoutubeYtBeIE(YoutubeBaseInfoExtractor):
|
||||
'duration': 59,
|
||||
'comment_count': int,
|
||||
'channel_follower_count': int,
|
||||
'media_type': 'short',
|
||||
},
|
||||
'params': {
|
||||
'noplaylist': True,
|
||||
|
||||
@@ -524,10 +524,16 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
||||
response = self._extract_response(
|
||||
item_id=f'{item_id} page {page_num}',
|
||||
query=continuation, headers=headers, ytcfg=ytcfg,
|
||||
check_get_keys=('continuationContents', 'onResponseReceivedActions', 'onResponseReceivedEndpoints'))
|
||||
check_get_keys=(
|
||||
'continuationContents', 'onResponseReceivedActions', 'onResponseReceivedEndpoints',
|
||||
# Playlist recommendations may return with no data - ignore
|
||||
('responseContext', 'serviceTrackingParams', ..., 'params', ..., lambda k, v: k == 'key' and v == 'GetRecommendedMusicPlaylists_rid'),
|
||||
))
|
||||
|
||||
if not response:
|
||||
break
|
||||
|
||||
continuation = None
|
||||
# Extracting updated visitor data is required to prevent an infinite extraction loop in some cases
|
||||
# See: https://github.com/ytdl-org/youtube-dl/issues/28702
|
||||
visitor_data = self._extract_visitor_data(response) or visitor_data
|
||||
@@ -564,7 +570,13 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
||||
yield from func(video_items_renderer)
|
||||
continuation = continuation_list[0] or self._extract_continuation(video_items_renderer)
|
||||
|
||||
if not video_items_renderer:
|
||||
# In the case only a continuation is returned, try to follow it.
|
||||
# We extract this after trying to extract non-continuation items as otherwise this
|
||||
# may be prioritized over other continuations.
|
||||
# see: https://github.com/yt-dlp/yt-dlp/issues/12933
|
||||
continuation = continuation or self._extract_continuation({'contents': [continuation_item]})
|
||||
|
||||
if not continuation and not video_items_renderer:
|
||||
break
|
||||
|
||||
@staticmethod
|
||||
@@ -999,14 +1011,14 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_mincount': 94,
|
||||
'info_dict': {
|
||||
'id': 'UCqj7Cz7revf5maW9g5pgNcg',
|
||||
'title': 'Igor Kleiner Ph.D. - Playlists',
|
||||
'title': 'Igor Kleiner - Playlists',
|
||||
'description': 'md5:15d7dd9e333cb987907fcb0d604b233a',
|
||||
'uploader': 'Igor Kleiner Ph.D.',
|
||||
'uploader': 'Igor Kleiner ',
|
||||
'uploader_id': '@IgorDataScience',
|
||||
'uploader_url': 'https://www.youtube.com/@IgorDataScience',
|
||||
'channel': 'Igor Kleiner Ph.D.',
|
||||
'channel': 'Igor Kleiner ',
|
||||
'channel_id': 'UCqj7Cz7revf5maW9g5pgNcg',
|
||||
'tags': ['критическое мышление', 'наука просто', 'математика', 'анализ данных'],
|
||||
'tags': 'count:23',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCqj7Cz7revf5maW9g5pgNcg',
|
||||
'channel_follower_count': int,
|
||||
},
|
||||
@@ -1016,18 +1028,19 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_mincount': 94,
|
||||
'info_dict': {
|
||||
'id': 'UCqj7Cz7revf5maW9g5pgNcg',
|
||||
'title': 'Igor Kleiner Ph.D. - Playlists',
|
||||
'title': 'Igor Kleiner - Playlists',
|
||||
'description': 'md5:15d7dd9e333cb987907fcb0d604b233a',
|
||||
'uploader': 'Igor Kleiner Ph.D.',
|
||||
'uploader': 'Igor Kleiner ',
|
||||
'uploader_id': '@IgorDataScience',
|
||||
'uploader_url': 'https://www.youtube.com/@IgorDataScience',
|
||||
'tags': ['критическое мышление', 'наука просто', 'математика', 'анализ данных'],
|
||||
'tags': 'count:23',
|
||||
'channel_id': 'UCqj7Cz7revf5maW9g5pgNcg',
|
||||
'channel': 'Igor Kleiner Ph.D.',
|
||||
'channel': 'Igor Kleiner ',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCqj7Cz7revf5maW9g5pgNcg',
|
||||
'channel_follower_count': int,
|
||||
},
|
||||
}, {
|
||||
# TODO: fix channel_is_verified extraction
|
||||
'note': 'playlists, series',
|
||||
'url': 'https://www.youtube.com/c/3blue1brown/playlists?view=50&sort=dd&shelf_id=3',
|
||||
'playlist_mincount': 5,
|
||||
@@ -1066,22 +1079,23 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'url': 'https://www.youtube.com/c/ChristophLaimer/playlists',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# TODO: fix availability extraction
|
||||
'note': 'basic, single video playlist',
|
||||
'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
|
||||
'url': 'https://www.youtube.com/playlist?list=PLt5yu3-wZAlSLRHmI1qNm0wjyVNWw1pCU',
|
||||
'info_dict': {
|
||||
'id': 'PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
|
||||
'title': 'youtube-dl public playlist',
|
||||
'id': 'PLt5yu3-wZAlSLRHmI1qNm0wjyVNWw1pCU',
|
||||
'title': 'single video playlist',
|
||||
'description': '',
|
||||
'tags': [],
|
||||
'view_count': int,
|
||||
'modified_date': '20201130',
|
||||
'channel': 'Sergey M.',
|
||||
'channel_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
|
||||
'modified_date': '20250417',
|
||||
'channel': 'cole-dlp-test-acc',
|
||||
'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
|
||||
'availability': 'public',
|
||||
'uploader': 'Sergey M.',
|
||||
'uploader_url': 'https://www.youtube.com/@sergeym.6173',
|
||||
'uploader_id': '@sergeym.6173',
|
||||
'uploader': 'cole-dlp-test-acc',
|
||||
'uploader_url': 'https://www.youtube.com/@coletdjnz',
|
||||
'uploader_id': '@coletdjnz',
|
||||
},
|
||||
'playlist_count': 1,
|
||||
}, {
|
||||
@@ -1171,11 +1185,11 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
},
|
||||
'playlist_mincount': 17,
|
||||
}, {
|
||||
'note': 'Community tab',
|
||||
'note': 'Posts tab',
|
||||
'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/community',
|
||||
'info_dict': {
|
||||
'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
|
||||
'title': 'lex will - Community',
|
||||
'title': 'lex will - Posts',
|
||||
'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
|
||||
'channel': 'lex will',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
|
||||
@@ -1188,30 +1202,14 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
},
|
||||
'playlist_mincount': 18,
|
||||
}, {
|
||||
'note': 'Channels tab',
|
||||
'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/channels',
|
||||
'info_dict': {
|
||||
'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
|
||||
'title': 'lex will - Channels',
|
||||
'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
|
||||
'channel': 'lex will',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
|
||||
'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
|
||||
'tags': ['bible', 'history', 'prophesy'],
|
||||
'channel_follower_count': int,
|
||||
'uploader_url': 'https://www.youtube.com/@lexwill718',
|
||||
'uploader_id': '@lexwill718',
|
||||
'uploader': 'lex will',
|
||||
},
|
||||
'playlist_mincount': 12,
|
||||
}, {
|
||||
# TODO: fix channel_is_verified extraction
|
||||
'note': 'Search tab',
|
||||
'url': 'https://www.youtube.com/c/3blue1brown/search?query=linear%20algebra',
|
||||
'playlist_mincount': 40,
|
||||
'info_dict': {
|
||||
'id': 'UCYO_jab_esuFRV4b17AJtAw',
|
||||
'title': '3Blue1Brown - Search - linear algebra',
|
||||
'description': 'md5:4d1da95432004b7ba840ebc895b6b4c9',
|
||||
'description': 'md5:602e3789e6a0cb7d9d352186b720e395',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
|
||||
'tags': ['Mathematics'],
|
||||
'channel': '3Blue1Brown',
|
||||
@@ -1232,6 +1230,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'url': 'https://music.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# TODO: fix availability extraction
|
||||
'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
|
||||
'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
|
||||
'info_dict': {
|
||||
@@ -1294,24 +1293,25 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
},
|
||||
'playlist_mincount': 21,
|
||||
}, {
|
||||
# TODO: fix availability extraction
|
||||
'note': 'Playlist with "show unavailable videos" button',
|
||||
'url': 'https://www.youtube.com/playlist?list=UUTYLiWFZy8xtPwxFwX9rV7Q',
|
||||
'url': 'https://www.youtube.com/playlist?list=PLYwq8WOe86_xGmR7FrcJq8Sb7VW8K3Tt2',
|
||||
'info_dict': {
|
||||
'title': 'Uploads from Phim Siêu Nhân Nhật Bản',
|
||||
'id': 'UUTYLiWFZy8xtPwxFwX9rV7Q',
|
||||
'title': 'The Memes Of 2010s.....',
|
||||
'id': 'PLYwq8WOe86_xGmR7FrcJq8Sb7VW8K3Tt2',
|
||||
'view_count': int,
|
||||
'channel': 'Phim Siêu Nhân Nhật Bản',
|
||||
'channel': "I'm Not JiNxEd",
|
||||
'tags': [],
|
||||
'description': '',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCTYLiWFZy8xtPwxFwX9rV7Q',
|
||||
'channel_id': 'UCTYLiWFZy8xtPwxFwX9rV7Q',
|
||||
'description': 'md5:44dc3b315ba69394feaafa2f40e7b2a1',
|
||||
'channel_url': 'https://www.youtube.com/channel/UC5H5H85D1QE5-fuWWQ1hdNg',
|
||||
'channel_id': 'UC5H5H85D1QE5-fuWWQ1hdNg',
|
||||
'modified_date': r're:\d{8}',
|
||||
'availability': 'public',
|
||||
'uploader_url': 'https://www.youtube.com/@phimsieunhannhatban',
|
||||
'uploader_id': '@phimsieunhannhatban',
|
||||
'uploader': 'Phim Siêu Nhân Nhật Bản',
|
||||
'uploader_url': 'https://www.youtube.com/@imnotjinxed1998',
|
||||
'uploader_id': '@imnotjinxed1998',
|
||||
'uploader': "I'm Not JiNxEd",
|
||||
},
|
||||
'playlist_mincount': 200,
|
||||
'playlist_mincount': 150,
|
||||
'expected_warnings': [r'[Uu]navailable videos (are|will be) hidden'],
|
||||
}, {
|
||||
'note': 'Playlist with unavailable videos in page 7',
|
||||
@@ -1334,6 +1334,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_mincount': 1000,
|
||||
'expected_warnings': [r'[Uu]navailable videos (are|will be) hidden'],
|
||||
}, {
|
||||
# TODO: fix availability extraction
|
||||
'note': 'https://github.com/ytdl-org/youtube-dl/issues/21844',
|
||||
'url': 'https://www.youtube.com/playlist?list=PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
|
||||
'info_dict': {
|
||||
@@ -1384,7 +1385,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
}, {
|
||||
'url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ/live',
|
||||
'info_dict': {
|
||||
'id': 'hGkQjiJLjWQ', # This will keep changing
|
||||
'id': 'YDvsBbKfLPA', # This will keep changing
|
||||
'ext': 'mp4',
|
||||
'title': str,
|
||||
'upload_date': r're:\d{8}',
|
||||
@@ -1409,6 +1410,8 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'uploader_id': '@SkyNews',
|
||||
'uploader': 'Sky News',
|
||||
'channel_is_verified': True,
|
||||
'media_type': 'livestream',
|
||||
'timestamp': int,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1496,6 +1499,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'url': 'https://music.youtube.com/browse/UC1a8OFewdjuLq6KlF8M_8Ng',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# TODO: fix availability extraction
|
||||
'note': 'VLPL, should redirect to playlist?list=PL...',
|
||||
'url': 'https://music.youtube.com/browse/VLPLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
|
||||
'info_dict': {
|
||||
@@ -1537,6 +1541,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
}, {
|
||||
# Destination channel with only a hidden self tab (tab id is UCtFRv9O2AHqOZjjynzrv-xg)
|
||||
# Treat as a general feed
|
||||
# TODO: fix extraction
|
||||
'url': 'https://www.youtube.com/channel/UCtFRv9O2AHqOZjjynzrv-xg',
|
||||
'info_dict': {
|
||||
'id': 'UCtFRv9O2AHqOZjjynzrv-xg',
|
||||
@@ -1560,21 +1565,21 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'expected_warnings': ['YouTube Music is not directly supported'],
|
||||
}, {
|
||||
'note': 'unlisted single video playlist',
|
||||
'url': 'https://www.youtube.com/playlist?list=PLwL24UFy54GrB3s2KMMfjZscDi1x5Dajf',
|
||||
'url': 'https://www.youtube.com/playlist?list=PLt5yu3-wZAlQLfIN0MMgp0wVV6MP3bM4_',
|
||||
'info_dict': {
|
||||
'id': 'PLwL24UFy54GrB3s2KMMfjZscDi1x5Dajf',
|
||||
'title': 'yt-dlp unlisted playlist test',
|
||||
'id': 'PLt5yu3-wZAlQLfIN0MMgp0wVV6MP3bM4_',
|
||||
'title': 'unlisted playlist',
|
||||
'availability': 'unlisted',
|
||||
'tags': [],
|
||||
'modified_date': '20220418',
|
||||
'channel': 'colethedj',
|
||||
'modified_date': '20250417',
|
||||
'channel': 'cole-dlp-test-acc',
|
||||
'view_count': int,
|
||||
'description': '',
|
||||
'channel_id': 'UC9zHu_mHU96r19o-wV5Qs1Q',
|
||||
'channel_url': 'https://www.youtube.com/channel/UC9zHu_mHU96r19o-wV5Qs1Q',
|
||||
'uploader_url': 'https://www.youtube.com/@colethedj1894',
|
||||
'uploader_id': '@colethedj1894',
|
||||
'uploader': 'colethedj',
|
||||
'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
|
||||
'uploader_url': 'https://www.youtube.com/@coletdjnz',
|
||||
'uploader_id': '@coletdjnz',
|
||||
'uploader': 'cole-dlp-test-acc',
|
||||
},
|
||||
'playlist': [{
|
||||
'info_dict': {
|
||||
@@ -1596,6 +1601,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_count': 1,
|
||||
'params': {'extract_flat': True},
|
||||
}, {
|
||||
# By default, recommended is always empty.
|
||||
'note': 'API Fallback: Recommended - redirects to home page. Requires visitorData',
|
||||
'url': 'https://www.youtube.com/feed/recommended',
|
||||
'info_dict': {
|
||||
@@ -1603,7 +1609,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'title': 'recommended',
|
||||
'tags': [],
|
||||
},
|
||||
'playlist_mincount': 50,
|
||||
'playlist_count': 0,
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
'extractor_args': {'youtubetab': {'skip': ['webpage']}},
|
||||
@@ -1628,6 +1634,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
},
|
||||
'skip': 'Query for sorting no longer works',
|
||||
}, {
|
||||
# TODO: fix 'unviewable' issue with this playlist when reloading with unavailable videos
|
||||
'note': 'API Fallback: Topic, should redirect to playlist?list=UU...',
|
||||
'url': 'https://music.youtube.com/browse/UC9ALqqC4aIeG5iDs7i90Bfw',
|
||||
'info_dict': {
|
||||
@@ -1654,11 +1661,12 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'url': 'https://www.youtube.com/channel/UCwVVpHQ2Cs9iGJfpdFngePQ',
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# TODO: fix metadata extraction
|
||||
'note': 'collaborative playlist (uploader name in the form "by <uploader> and x other(s)")',
|
||||
'url': 'https://www.youtube.com/playlist?list=PLx-_-Kk4c89oOHEDQAojOXzEzemXxoqx6',
|
||||
'info_dict': {
|
||||
'id': 'PLx-_-Kk4c89oOHEDQAojOXzEzemXxoqx6',
|
||||
'modified_date': '20220407',
|
||||
'modified_date': '20250115',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCKcqXmCcyqnhgpA5P0oHH_Q',
|
||||
'tags': [],
|
||||
'availability': 'unlisted',
|
||||
@@ -1692,6 +1700,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'expected_warnings': ['Preferring "ja"'],
|
||||
}, {
|
||||
# XXX: this should really check flat playlist entries, but the test suite doesn't support that
|
||||
# TODO: fix availability extraction
|
||||
'note': 'preferred lang set with playlist with translated video titles',
|
||||
'url': 'https://www.youtube.com/playlist?list=PLt5yu3-wZAlQAaPZ5Z-rJoTdbT-45Q7c0',
|
||||
'info_dict': {
|
||||
@@ -1714,6 +1723,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
}, {
|
||||
# shorts audio pivot for 2GtVksBMYFM.
|
||||
'url': 'https://www.youtube.com/feed/sfv_audio_pivot?bp=8gUrCikSJwoLMkd0VmtzQk1ZRk0SCzJHdFZrc0JNWUZNGgsyR3RWa3NCTVlGTQ==',
|
||||
# TODO: fix extraction
|
||||
'info_dict': {
|
||||
'id': 'sfv_audio_pivot',
|
||||
'title': 'sfv_audio_pivot',
|
||||
@@ -1751,6 +1761,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_mincount': 8,
|
||||
}, {
|
||||
# Should get three playlists for videos, shorts and streams tabs
|
||||
# TODO: fix channel_is_verified extraction
|
||||
'url': 'https://www.youtube.com/channel/UCK9V2B22uJYu3N7eR_BT9QA',
|
||||
'info_dict': {
|
||||
'id': 'UCK9V2B22uJYu3N7eR_BT9QA',
|
||||
@@ -1758,7 +1769,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'UCK9V2B22uJYu3N7eR_BT9QA',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCK9V2B22uJYu3N7eR_BT9QA',
|
||||
'description': 'md5:49809d8bf9da539bc48ed5d1f83c33f2',
|
||||
'description': 'md5:01e53f350ab8ad6fcf7c4fedb3c1b99f',
|
||||
'channel': 'Polka Ch. 尾丸ポルカ',
|
||||
'tags': 'count:35',
|
||||
'uploader_url': 'https://www.youtube.com/@OmaruPolka',
|
||||
@@ -1769,14 +1780,14 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_count': 3,
|
||||
}, {
|
||||
# Shorts tab with channel with handle
|
||||
# TODO: fix channel description
|
||||
# TODO: fix channel_is_verified extraction
|
||||
'url': 'https://www.youtube.com/@NotJustBikes/shorts',
|
||||
'info_dict': {
|
||||
'id': 'UC0intLFzLaudFG-xAvUEO-A',
|
||||
'title': 'Not Just Bikes - Shorts',
|
||||
'tags': 'count:10',
|
||||
'channel_url': 'https://www.youtube.com/channel/UC0intLFzLaudFG-xAvUEO-A',
|
||||
'description': 'md5:5e82545b3a041345927a92d0585df247',
|
||||
'description': 'md5:1d9fc1bad7f13a487299d1fe1712e031',
|
||||
'channel_follower_count': int,
|
||||
'channel_id': 'UC0intLFzLaudFG-xAvUEO-A',
|
||||
'channel': 'Not Just Bikes',
|
||||
@@ -1797,7 +1808,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'channel_url': 'https://www.youtube.com/channel/UC3eYAvjCVwNHgkaGbXX3sig',
|
||||
'channel': '中村悠一',
|
||||
'channel_follower_count': int,
|
||||
'description': 'md5:e744f6c93dafa7a03c0c6deecb157300',
|
||||
'description': 'md5:e8fd705073a594f27d6d6d020da560dc',
|
||||
'uploader_url': 'https://www.youtube.com/@Yuichi-Nakamura',
|
||||
'uploader_id': '@Yuichi-Nakamura',
|
||||
'uploader': '中村悠一',
|
||||
@@ -1815,6 +1826,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'only_matching': True,
|
||||
}, {
|
||||
# No videos tab but has a shorts tab
|
||||
# TODO: fix metadata extraction
|
||||
'url': 'https://www.youtube.com/c/TKFShorts',
|
||||
'info_dict': {
|
||||
'id': 'UCgJ5_1F6yJhYLnyMszUdmUg',
|
||||
@@ -1851,6 +1863,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
}, {
|
||||
# Shorts url result in shorts tab
|
||||
# TODO: Fix channel id extraction
|
||||
# TODO: fix test suite, 208163447408c78673b08c172beafe5c310fb167 broke this test
|
||||
'url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA/shorts',
|
||||
'info_dict': {
|
||||
'id': 'UCiu-3thuViMebBjw_5nWYrA',
|
||||
@@ -1879,6 +1892,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'params': {'extract_flat': True},
|
||||
}, {
|
||||
# Live video status should be extracted
|
||||
# TODO: fix test suite, 208163447408c78673b08c172beafe5c310fb167 broke this test
|
||||
'url': 'https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/live',
|
||||
'info_dict': {
|
||||
'id': 'UCQvWX73GQygcwXOTSf_VDVg',
|
||||
@@ -1907,6 +1921,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_mincount': 1,
|
||||
}, {
|
||||
# Channel renderer metadata. Contains number of videos on the channel
|
||||
# TODO: channels tab removed, change this test to use another page with channel renderer
|
||||
'url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA/channels',
|
||||
'info_dict': {
|
||||
'id': 'UCiu-3thuViMebBjw_5nWYrA',
|
||||
@@ -1940,7 +1955,9 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
},
|
||||
}],
|
||||
'params': {'extract_flat': True},
|
||||
'skip': 'channels tab removed',
|
||||
}, {
|
||||
# TODO: fix channel_is_verified extraction
|
||||
'url': 'https://www.youtube.com/@3blue1brown/about',
|
||||
'info_dict': {
|
||||
'id': '@3blue1brown',
|
||||
@@ -1950,7 +1967,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',
|
||||
'channel': '3Blue1Brown',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
|
||||
'description': 'md5:4d1da95432004b7ba840ebc895b6b4c9',
|
||||
'description': 'md5:602e3789e6a0cb7d9d352186b720e395',
|
||||
'uploader_url': 'https://www.youtube.com/@3blue1brown',
|
||||
'uploader_id': '@3blue1brown',
|
||||
'uploader': '3Blue1Brown',
|
||||
@@ -1976,6 +1993,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_count': 5,
|
||||
}, {
|
||||
# Releases tab, with rich entry playlistRenderers (same as Podcasts tab)
|
||||
# TODO: fix channel_is_verified extraction
|
||||
'url': 'https://www.youtube.com/@AHimitsu/releases',
|
||||
'info_dict': {
|
||||
'id': 'UCgFwu-j5-xNJml2FtTrrB3A',
|
||||
@@ -2015,6 +2033,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'playlist_mincount': 100,
|
||||
'expected_warnings': [r'[Uu]navailable videos (are|will be) hidden'],
|
||||
}, {
|
||||
# TODO: fix channel_is_verified extraction
|
||||
'note': 'Tags containing spaces',
|
||||
'url': 'https://www.youtube.com/channel/UC7_YxT-KID8kRbqZo7MyscQ',
|
||||
'playlist_count': 3,
|
||||
@@ -2035,6 +2054,24 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'challenges', 'sketches', 'scary games', 'funny games', 'rage games',
|
||||
'mark fischbach'],
|
||||
},
|
||||
}, {
|
||||
# https://github.com/yt-dlp/yt-dlp/issues/12933
|
||||
'note': 'streams tab, some scheduled streams. Empty intermediate response with only continuation - must follow',
|
||||
'url': 'https://www.youtube.com/@sbcitygov/streams',
|
||||
'playlist_mincount': 150,
|
||||
'info_dict': {
|
||||
'id': 'UCH6-qfQwlUgz9SAf05jvc_w',
|
||||
'channel': 'sbcitygov',
|
||||
'channel_id': 'UCH6-qfQwlUgz9SAf05jvc_w',
|
||||
'title': 'sbcitygov - Live',
|
||||
'channel_follower_count': int,
|
||||
'description': 'md5:ca1a92059835c071e33b3db52f4a6d67',
|
||||
'uploader_id': '@sbcitygov',
|
||||
'uploader_url': 'https://www.youtube.com/@sbcitygov',
|
||||
'uploader': 'sbcitygov',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCH6-qfQwlUgz9SAf05jvc_w',
|
||||
'tags': [],
|
||||
},
|
||||
}]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -375,6 +375,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Afrojack',
|
||||
'uploader_url': 'https://www.youtube.com/@Afrojack',
|
||||
'uploader_id': '@Afrojack',
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'youtube_include_dash_manifest': True,
|
||||
@@ -412,10 +413,11 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
'timestamp': 1401991663,
|
||||
'media_type': 'video',
|
||||
},
|
||||
},
|
||||
{
|
||||
'note': 'Age-gate video with embed allowed in public site',
|
||||
'note': 'Formerly an age-gate video with embed allowed in public site',
|
||||
'url': 'https://youtube.com/watch?v=HsUATh_Nc2U',
|
||||
'info_dict': {
|
||||
'id': 'HsUATh_Nc2U',
|
||||
@@ -423,8 +425,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'title': 'Godzilla 2 (Official Video)',
|
||||
'description': 'md5:bf77e03fcae5529475e500129b05668a',
|
||||
'upload_date': '20200408',
|
||||
'age_limit': 18,
|
||||
'availability': 'needs_auth',
|
||||
'age_limit': 0,
|
||||
'availability': 'public',
|
||||
'channel_id': 'UCYQT13AtrJC0gsM1far_zJg',
|
||||
'channel': 'FlyingKitty',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCYQT13AtrJC0gsM1far_zJg',
|
||||
@@ -442,8 +444,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_id': '@FlyingKitty900',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'skip': 'Age-restricted; requires authentication',
|
||||
},
|
||||
{
|
||||
'note': 'Age-gate video embedable only with clientScreen=EMBED',
|
||||
@@ -506,6 +508,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Herr Lurik',
|
||||
'uploader_url': 'https://www.youtube.com/@HerrLurik',
|
||||
'uploader_id': '@HerrLurik',
|
||||
'media_type': 'video',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -545,6 +548,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'deadmau5',
|
||||
'uploader_url': 'https://www.youtube.com/@deadmau5',
|
||||
'uploader_id': '@deadmau5',
|
||||
'media_type': 'video',
|
||||
},
|
||||
'expected_warnings': [
|
||||
'DASH manifest missing',
|
||||
@@ -580,6 +584,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_id': '@Olympics',
|
||||
'channel_is_verified': True,
|
||||
'timestamp': 1440707674,
|
||||
'media_type': 'livestream',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': 'requires avconv',
|
||||
@@ -614,6 +619,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_url': 'https://www.youtube.com/@AllenMeow',
|
||||
'uploader_id': '@AllenMeow',
|
||||
'timestamp': 1299776999,
|
||||
'media_type': 'video',
|
||||
},
|
||||
},
|
||||
# url_encoded_fmt_stream_map is empty string
|
||||
@@ -808,6 +814,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'like_count': int,
|
||||
'age_limit': 0,
|
||||
'channel_follower_count': int,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -867,6 +874,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_id': '@BKCHarvard',
|
||||
'uploader_url': 'https://www.youtube.com/@BKCHarvard',
|
||||
'timestamp': 1422422076,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -903,6 +911,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
'timestamp': 1447987198,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -967,6 +976,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'timestamp': 1484761047,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1069,6 +1079,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'tags': 'count:11',
|
||||
'live_status': 'not_live',
|
||||
'channel_follower_count': int,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1123,6 +1134,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_url': 'https://www.youtube.com/@ElevageOrVert',
|
||||
'uploader_id': '@ElevageOrVert',
|
||||
'timestamp': 1497343210,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1162,6 +1174,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
'timestamp': 1377976349,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1206,6 +1219,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'channel_follower_count': int,
|
||||
'uploader': 'The Cinematic Orchestra',
|
||||
'comment_count': int,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1274,6 +1288,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_url': 'https://www.youtube.com/@walkaroundjapan7124',
|
||||
'uploader_id': '@walkaroundjapan7124',
|
||||
'timestamp': 1605884416,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1370,6 +1385,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
'timestamp': 1395685455,
|
||||
'media_type': 'video',
|
||||
}, 'params': {'format': 'mhtml', 'skip_download': True},
|
||||
}, {
|
||||
# Ensure video upload_date is in UTC timezone (video was uploaded 1641170939)
|
||||
@@ -1400,6 +1416,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_id': '@LeonNguyen',
|
||||
'heatmap': 'count:100',
|
||||
'timestamp': 1641170939,
|
||||
'media_type': 'video',
|
||||
},
|
||||
}, {
|
||||
# date text is premiered video, ensure upload date in UTC (published 1641172509)
|
||||
@@ -1433,6 +1450,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
'timestamp': 1641172509,
|
||||
'media_type': 'video',
|
||||
},
|
||||
},
|
||||
{ # continuous livestream.
|
||||
@@ -1494,6 +1512,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Lesmiscore',
|
||||
'uploader_url': 'https://www.youtube.com/@lesmiscore',
|
||||
'timestamp': 1648005313,
|
||||
'media_type': 'short',
|
||||
},
|
||||
}, {
|
||||
# Prefer primary title+description language metadata by default
|
||||
@@ -1522,6 +1541,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_id': '@coletdjnz',
|
||||
'uploader': 'cole-dlp-test-acc',
|
||||
'timestamp': 1662677394,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {'skip_download': True},
|
||||
}, {
|
||||
@@ -1550,6 +1570,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'cole-dlp-test-acc',
|
||||
'timestamp': 1659073275,
|
||||
'like_count': int,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {'skip_download': True, 'extractor_args': {'youtube': {'lang': ['fr']}}},
|
||||
'expected_warnings': [r'Preferring "fr" translated fields'],
|
||||
@@ -1586,6 +1607,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {'extractor_args': {'youtube': {'player_client': ['ios']}}, 'format': '233-1'},
|
||||
}, {
|
||||
@@ -1686,6 +1708,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'extractor_args': {'youtube': {'player_client': ['ios'], 'player_skip': ['webpage']}},
|
||||
@@ -1718,6 +1741,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'channel_follower_count': int,
|
||||
'categories': ['People & Blogs'],
|
||||
'tags': [],
|
||||
'media_type': 'short',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -1753,6 +1777,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_id': '@ChristopherSykesDocumentaries',
|
||||
'heatmap': 'count:100',
|
||||
'timestamp': 1211825920,
|
||||
'media_type': 'video',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1818,6 +1843,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
else:
|
||||
retry.error = f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}'
|
||||
continue
|
||||
|
||||
# Formats from ended premieres will be missing a manifest_url
|
||||
# See https://github.com/yt-dlp/yt-dlp/issues/8543
|
||||
if not f.get('manifest_url'):
|
||||
break
|
||||
|
||||
return f['manifest_url'], f['manifest_stream_number'], is_live
|
||||
return None
|
||||
|
||||
@@ -1981,7 +2012,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
def _player_js_cache_key(self, player_url):
|
||||
player_id = self._extract_player_info(player_url)
|
||||
player_path = remove_start(urllib.parse.urlparse(player_url).path, f'/s/player/{player_id}/')
|
||||
variant = self._INVERSE_PLAYER_JS_VARIANT_MAP.get(player_path)
|
||||
variant = self._INVERSE_PLAYER_JS_VARIANT_MAP.get(player_path) or next((
|
||||
v for k, v in self._INVERSE_PLAYER_JS_VARIANT_MAP.items()
|
||||
if re.fullmatch(re.escape(k).replace('en_US', r'[a-zA-Z0-9_]+'), player_path)), None)
|
||||
if not variant:
|
||||
self.write_debug(
|
||||
f'Unable to determine player JS variant\n'
|
||||
@@ -2119,23 +2152,23 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
return ret
|
||||
return inner
|
||||
|
||||
def _load_nsig_code_from_cache(self, player_url):
|
||||
cache_id = ('youtube-nsig', self._player_js_cache_key(player_url))
|
||||
def _load_player_data_from_cache(self, name, player_url):
|
||||
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url))
|
||||
|
||||
if func_code := self._player_cache.get(cache_id):
|
||||
return func_code
|
||||
if data := self._player_cache.get(cache_id):
|
||||
return data
|
||||
|
||||
func_code = self.cache.load(*cache_id, min_ver='2025.03.31')
|
||||
if func_code:
|
||||
self._player_cache[cache_id] = func_code
|
||||
data = self.cache.load(*cache_id, min_ver='2025.03.31')
|
||||
if data:
|
||||
self._player_cache[cache_id] = data
|
||||
|
||||
return func_code
|
||||
return data
|
||||
|
||||
def _store_nsig_code_to_cache(self, player_url, func_code):
|
||||
cache_id = ('youtube-nsig', self._player_js_cache_key(player_url))
|
||||
def _store_player_data_to_cache(self, name, player_url, data):
|
||||
cache_id = (f'youtube-{name}', self._player_js_cache_key(player_url))
|
||||
if cache_id not in self._player_cache:
|
||||
self.cache.store(*cache_id, func_code)
|
||||
self._player_cache[cache_id] = func_code
|
||||
self.cache.store(*cache_id, data)
|
||||
self._player_cache[cache_id] = data
|
||||
|
||||
def _decrypt_signature(self, s, video_id, player_url):
|
||||
"""Turn the encrypted s field into a working signature"""
|
||||
@@ -2178,7 +2211,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
self.write_debug(f'Decrypted nsig {s} => {ret}')
|
||||
# Only cache nsig func JS code to disk if successful, and only once
|
||||
self._store_nsig_code_to_cache(player_url, func_code)
|
||||
self._store_player_data_to_cache('nsig', player_url, func_code)
|
||||
return ret
|
||||
|
||||
def _extract_n_function_name(self, jscode, player_url=None):
|
||||
@@ -2297,7 +2330,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
def _extract_n_function_code(self, video_id, player_url):
|
||||
player_id = self._extract_player_info(player_url)
|
||||
func_code = self._load_nsig_code_from_cache(player_url)
|
||||
func_code = self._load_player_data_from_cache('nsig', player_url)
|
||||
jscode = func_code or self._load_player(video_id, player_url)
|
||||
jsi = JSInterpreter(jscode)
|
||||
|
||||
@@ -2333,23 +2366,27 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
Extract signatureTimestamp (sts)
|
||||
Required to tell API what sig/player version is in use.
|
||||
"""
|
||||
sts = None
|
||||
if isinstance(ytcfg, dict):
|
||||
sts = int_or_none(ytcfg.get('STS'))
|
||||
if sts := traverse_obj(ytcfg, ('STS', {int_or_none})):
|
||||
return sts
|
||||
|
||||
if not player_url:
|
||||
error_msg = 'Cannot extract signature timestamp without player url'
|
||||
if fatal:
|
||||
raise ExtractorError(error_msg)
|
||||
self.report_warning(error_msg)
|
||||
return None
|
||||
|
||||
sts = self._load_player_data_from_cache('sts', player_url)
|
||||
if sts:
|
||||
return sts
|
||||
|
||||
if code := self._load_player(video_id, player_url, fatal=fatal):
|
||||
sts = int_or_none(self._search_regex(
|
||||
r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
|
||||
'JS player signature timestamp', group='sts', fatal=fatal))
|
||||
if sts:
|
||||
self._store_player_data_to_cache('sts', player_url, sts)
|
||||
|
||||
if not sts:
|
||||
# Attempt to extract from player
|
||||
if player_url is None:
|
||||
error_msg = 'Cannot extract signature timestamp without player_url.'
|
||||
if fatal:
|
||||
raise ExtractorError(error_msg)
|
||||
self.report_warning(error_msg)
|
||||
return
|
||||
code = self._load_player(video_id, player_url, fatal=fatal)
|
||||
if code:
|
||||
sts = int_or_none(self._search_regex(
|
||||
r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
|
||||
'JS player signature timestamp', group='sts', fatal=fatal))
|
||||
return sts
|
||||
|
||||
def _mark_watched(self, video_id, player_responses):
|
||||
@@ -3102,9 +3139,19 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
else:
|
||||
prs.append(pr)
|
||||
|
||||
# web_embedded can work around age-gate and age-verification for some embeddable videos
|
||||
if self._is_agegated(pr) and variant != 'web_embedded':
|
||||
append_client(f'web_embedded.{base_client}')
|
||||
# Unauthenticated users will only get web_embedded client formats if age-gated
|
||||
if self._is_agegated(pr) and not self.is_authenticated:
|
||||
self.to_screen(
|
||||
f'{video_id}: This video is age-restricted; some formats may be missing '
|
||||
f'without authentication. {self._youtube_login_hint}', only_once=True)
|
||||
|
||||
# EU countries require age-verification for accounts to access age-restricted videos
|
||||
# If account is not age-verified, _is_agegated() will be truthy for non-embedded clients
|
||||
if self.is_authenticated and self._is_agegated(pr):
|
||||
embedding_is_disabled = variant == 'web_embedded' and self._is_unplayable(pr)
|
||||
if self.is_authenticated and (self._is_agegated(pr) or embedding_is_disabled):
|
||||
self.to_screen(
|
||||
f'{video_id}: This video is age-restricted and YouTube is requiring '
|
||||
'account age-verification; some formats may be missing', only_once=True)
|
||||
@@ -3231,12 +3278,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
fmt_url = url_or_none(try_get(sc, lambda x: x['url'][0]))
|
||||
encrypted_sig = try_get(sc, lambda x: x['s'][0])
|
||||
if not all((sc, fmt_url, player_url, encrypted_sig)):
|
||||
self.report_warning(
|
||||
f'Some {client_name} client https formats have been skipped as they are missing a url. '
|
||||
f'{"Your account" if self.is_authenticated else "The current session"} may have '
|
||||
f'the SSAP (server-side ads) experiment which interferes with yt-dlp. '
|
||||
f'Please see https://github.com/yt-dlp/yt-dlp/issues/12482 for more details.',
|
||||
video_id, only_once=True)
|
||||
msg = f'Some {client_name} client https formats have been skipped as they are missing a url. '
|
||||
if client_name == 'web':
|
||||
msg += 'YouTube is forcing SABR streaming for this client. '
|
||||
else:
|
||||
msg += (
|
||||
f'YouTube may have enabled the SABR-only or Server-Side Ad Placement experiment for '
|
||||
f'{"your account" if self.is_authenticated else "the current session"}. '
|
||||
)
|
||||
msg += 'See https://github.com/yt-dlp/yt-dlp/issues/12482 for more details'
|
||||
self.report_warning(msg, video_id, only_once=True)
|
||||
continue
|
||||
try:
|
||||
fmt_url += '&{}={}'.format(
|
||||
@@ -3323,8 +3374,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'width': int_or_none(fmt.get('width')),
|
||||
'language': join_nonempty(language_code, 'desc' if is_descriptive else '') or None,
|
||||
'language_preference': PREFERRED_LANG_VALUE if is_original else 5 if is_default else -10 if is_descriptive else -1,
|
||||
# Strictly de-prioritize broken, damaged and 3gp formats
|
||||
'preference': -20 if require_po_token else -10 if is_damaged else -2 if itag == '17' else None,
|
||||
# Strictly de-prioritize damaged and 3gp formats
|
||||
'preference': -10 if is_damaged else -2 if itag == '17' else None,
|
||||
}
|
||||
mime_mobj = re.match(
|
||||
r'((?:[^/]+)/(?:[^;]+))(?:;\s*codecs="([^"]+)")?', fmt.get('mimeType') or '')
|
||||
@@ -3645,6 +3696,15 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if 'sign in' in reason.lower():
|
||||
reason = remove_end(reason, 'This helps protect our community. Learn more')
|
||||
reason = f'{remove_end(reason.strip(), ".")}. {self._youtube_login_hint}'
|
||||
elif get_first(playability_statuses, ('errorScreen', 'playerCaptchaViewModel', {dict})):
|
||||
reason += '. YouTube is requiring a captcha challenge before playback'
|
||||
elif "This content isn't available, try again later" in reason:
|
||||
reason = (
|
||||
f'{remove_end(reason.strip(), ".")}. {"Your account" if self.is_authenticated else "The current session"} '
|
||||
f'has been rate-limited by YouTube for up to an hour. It is recommended to use `-t sleep` to add a delay '
|
||||
f'between video requests to avoid exceeding the rate limit. For more information, refer to '
|
||||
f'https://github.com/yt-dlp/yt-dlp/wiki/Extractors#this-content-isnt-available-try-again-later'
|
||||
)
|
||||
self.raise_no_formats(reason, expected=True)
|
||||
|
||||
keywords = get_first(video_details, 'keywords', expected_type=list) or []
|
||||
@@ -3751,7 +3811,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'tags': keywords,
|
||||
'playable_in_embed': get_first(playability_statuses, 'playableInEmbed'),
|
||||
'live_status': live_status,
|
||||
'media_type': 'livestream' if get_first(video_details, 'isLiveContent') else None,
|
||||
'media_type': (
|
||||
'livestream' if get_first(video_details, 'isLiveContent')
|
||||
else 'short' if get_first(microformats, 'isShortsEligible')
|
||||
else 'video'),
|
||||
'release_timestamp': live_start_time,
|
||||
'_format_sort_fields': ( # source_preference is lower for potentially damaged formats
|
||||
'quality', 'res', 'fps', 'hdr:12', 'source', 'vcodec', 'channels', 'acodec', 'lang', 'proto'),
|
||||
@@ -3873,7 +3936,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if not traverse_obj(initial_data, 'contents'):
|
||||
self.report_warning('Incomplete data received in embedded initial data; re-fetching using API.')
|
||||
initial_data = None
|
||||
if not initial_data:
|
||||
if not initial_data and 'initial_data' not in self._configuration_arg('player_skip'):
|
||||
query = {'videoId': video_id}
|
||||
query.update(self._get_checkok_params())
|
||||
initial_data = self._extract_response(
|
||||
|
||||
Reference in New Issue
Block a user