mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-12-20 23:18:57 +00:00
Merge remote-tracking branch 'origin' into yt-live-from-start-range
This commit is contained in:
@@ -258,7 +258,7 @@ def build_innertube_clients():
|
||||
THIRD_PARTY = {
|
||||
'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
|
||||
}
|
||||
BASE_CLIENTS = ('android', 'web', 'tv', 'ios', 'mweb')
|
||||
BASE_CLIENTS = ('ios', 'android', 'web', 'tv', 'mweb')
|
||||
priority = qualities(BASE_CLIENTS[::-1])
|
||||
|
||||
for client, ytcfg in tuple(INNERTUBE_CLIENTS.items()):
|
||||
@@ -292,6 +292,7 @@ class BadgeType(enum.Enum):
|
||||
AVAILABILITY_PREMIUM = enum.auto()
|
||||
AVAILABILITY_SUBSCRIPTION = enum.auto()
|
||||
LIVE_NOW = enum.auto()
|
||||
VERIFIED = enum.auto()
|
||||
|
||||
|
||||
class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
@@ -791,17 +792,26 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
def _extract_and_report_alerts(self, data, *args, **kwargs):
|
||||
return self._report_alerts(self._extract_alerts(data), *args, **kwargs)
|
||||
|
||||
def _extract_badges(self, renderer: dict):
|
||||
privacy_icon_map = {
|
||||
def _extract_badges(self, badge_list: list):
|
||||
"""
|
||||
Extract known BadgeType's from a list of badge renderers.
|
||||
@returns [{'type': BadgeType}]
|
||||
"""
|
||||
icon_type_map = {
|
||||
'PRIVACY_UNLISTED': BadgeType.AVAILABILITY_UNLISTED,
|
||||
'PRIVACY_PRIVATE': BadgeType.AVAILABILITY_PRIVATE,
|
||||
'PRIVACY_PUBLIC': BadgeType.AVAILABILITY_PUBLIC
|
||||
'PRIVACY_PUBLIC': BadgeType.AVAILABILITY_PUBLIC,
|
||||
'CHECK_CIRCLE_THICK': BadgeType.VERIFIED,
|
||||
'OFFICIAL_ARTIST_BADGE': BadgeType.VERIFIED,
|
||||
'CHECK': BadgeType.VERIFIED,
|
||||
}
|
||||
|
||||
badge_style_map = {
|
||||
'BADGE_STYLE_TYPE_MEMBERS_ONLY': BadgeType.AVAILABILITY_SUBSCRIPTION,
|
||||
'BADGE_STYLE_TYPE_PREMIUM': BadgeType.AVAILABILITY_PREMIUM,
|
||||
'BADGE_STYLE_TYPE_LIVE_NOW': BadgeType.LIVE_NOW
|
||||
'BADGE_STYLE_TYPE_LIVE_NOW': BadgeType.LIVE_NOW,
|
||||
'BADGE_STYLE_TYPE_VERIFIED': BadgeType.VERIFIED,
|
||||
'BADGE_STYLE_TYPE_VERIFIED_ARTIST': BadgeType.VERIFIED,
|
||||
}
|
||||
|
||||
label_map = {
|
||||
@@ -809,13 +819,15 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
'private': BadgeType.AVAILABILITY_PRIVATE,
|
||||
'members only': BadgeType.AVAILABILITY_SUBSCRIPTION,
|
||||
'live': BadgeType.LIVE_NOW,
|
||||
'premium': BadgeType.AVAILABILITY_PREMIUM
|
||||
'premium': BadgeType.AVAILABILITY_PREMIUM,
|
||||
'verified': BadgeType.VERIFIED,
|
||||
'official artist channel': BadgeType.VERIFIED,
|
||||
}
|
||||
|
||||
badges = []
|
||||
for badge in traverse_obj(renderer, ('badges', ..., 'metadataBadgeRenderer')):
|
||||
for badge in traverse_obj(badge_list, (..., lambda key, _: re.search(r'[bB]adgeRenderer$', key))):
|
||||
badge_type = (
|
||||
privacy_icon_map.get(traverse_obj(badge, ('icon', 'iconType'), expected_type=str))
|
||||
icon_type_map.get(traverse_obj(badge, ('icon', 'iconType'), expected_type=str))
|
||||
or badge_style_map.get(traverse_obj(badge, 'style'))
|
||||
)
|
||||
if badge_type:
|
||||
@@ -823,11 +835,12 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
continue
|
||||
|
||||
# fallback, won't work in some languages
|
||||
label = traverse_obj(badge, 'label', expected_type=str, default='')
|
||||
label = traverse_obj(
|
||||
badge, 'label', ('accessibilityData', 'label'), 'tooltip', 'iconTooltip', get_all=False, expected_type=str, default='')
|
||||
for match, label_badge_type in label_map.items():
|
||||
if match in label.lower():
|
||||
badges.append({'type': badge_type})
|
||||
continue
|
||||
badges.append({'type': label_badge_type})
|
||||
break
|
||||
|
||||
return badges
|
||||
|
||||
@@ -1020,8 +1033,8 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
overlay_style = traverse_obj(
|
||||
renderer, ('thumbnailOverlays', ..., 'thumbnailOverlayTimeStatusRenderer', 'style'),
|
||||
get_all=False, expected_type=str)
|
||||
badges = self._extract_badges(renderer)
|
||||
|
||||
badges = self._extract_badges(traverse_obj(renderer, 'badges'))
|
||||
owner_badges = self._extract_badges(traverse_obj(renderer, 'ownerBadges'))
|
||||
navigation_url = urljoin('https://www.youtube.com/', traverse_obj(
|
||||
renderer, ('navigationEndpoint', 'commandMetadata', 'webCommandMetadata', 'url'),
|
||||
expected_type=str)) or ''
|
||||
@@ -1079,7 +1092,8 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||
needs_subscription=self._has_badge(badges, BadgeType.AVAILABILITY_SUBSCRIPTION) or None,
|
||||
is_unlisted=self._has_badge(badges, BadgeType.AVAILABILITY_UNLISTED) or None),
|
||||
view_count_field: view_count,
|
||||
'live_status': live_status
|
||||
'live_status': live_status,
|
||||
'channel_is_verified': True if self._has_badge(owner_badges, BadgeType.VERIFIED) else None
|
||||
}
|
||||
|
||||
|
||||
@@ -1332,6 +1346,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Philipp Hagemeister',
|
||||
'uploader_url': 'https://www.youtube.com/@PhilippHagemeister',
|
||||
'uploader_id': '@PhilippHagemeister',
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1415,6 +1430,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'The Witcher',
|
||||
'uploader_url': 'https://www.youtube.com/@thewitcher',
|
||||
'uploader_id': '@thewitcher',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1444,6 +1462,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader_url': 'https://www.youtube.com/@FlyingKitty900',
|
||||
'uploader_id': '@FlyingKitty900',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1577,6 +1596,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Olympics',
|
||||
'uploader_url': 'https://www.youtube.com/@Olympics',
|
||||
'uploader_id': '@Olympics',
|
||||
'channel_is_verified': True,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': 'requires avconv',
|
||||
@@ -1894,6 +1914,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Bernie Sanders',
|
||||
'uploader_url': 'https://www.youtube.com/@BernieSanders',
|
||||
'uploader_id': '@BernieSanders',
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -1955,6 +1977,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Vsauce',
|
||||
'uploader_url': 'https://www.youtube.com/@Vsauce',
|
||||
'uploader_id': '@Vsauce',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -2147,6 +2171,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'kudvenkat',
|
||||
'uploader_url': 'https://www.youtube.com/@Csharp-video-tutorialsBlogspot',
|
||||
'uploader_id': '@Csharp-video-tutorialsBlogspot',
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -2227,6 +2253,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'CBS Mornings',
|
||||
'uploader_url': 'https://www.youtube.com/@CBSMornings',
|
||||
'uploader_id': '@CBSMornings',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -2297,6 +2325,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'colinfurze',
|
||||
'uploader_url': 'https://www.youtube.com/@colinfurze',
|
||||
'uploader_id': '@colinfurze',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {
|
||||
'format': '17', # 3gp format available on android
|
||||
@@ -2342,6 +2373,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'SciShow',
|
||||
'uploader_url': 'https://www.youtube.com/@SciShow',
|
||||
'uploader_id': '@SciShow',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
}, 'params': {'format': 'mhtml', 'skip_download': True}
|
||||
}, {
|
||||
# Ensure video upload_date is in UTC timezone (video was uploaded 1641170939)
|
||||
@@ -2370,6 +2404,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Leon Nguyen',
|
||||
'uploader_url': 'https://www.youtube.com/@LeonNguyen',
|
||||
'uploader_id': '@LeonNguyen',
|
||||
'heatmap': 'count:100',
|
||||
}
|
||||
}, {
|
||||
# Same video as above, but with --compat-opt no-youtube-prefer-utc-upload-date
|
||||
@@ -2398,6 +2433,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Leon Nguyen',
|
||||
'uploader_url': 'https://www.youtube.com/@LeonNguyen',
|
||||
'uploader_id': '@LeonNguyen',
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {'compat_opts': ['no-youtube-prefer-utc-upload-date']}
|
||||
}, {
|
||||
@@ -2428,6 +2464,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Quackity',
|
||||
'uploader_id': '@Quackity',
|
||||
'uploader_url': 'https://www.youtube.com/@Quackity',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
}
|
||||
},
|
||||
{ # continuous livestream. Microformat upload date should be preferred.
|
||||
@@ -2594,6 +2633,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'MrBeast',
|
||||
'uploader_url': 'https://www.youtube.com/@MrBeast',
|
||||
'uploader_id': '@MrBeast',
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {'extractor_args': {'youtube': {'player_client': ['ios']}}, 'format': '233-1'},
|
||||
}, {
|
||||
@@ -2655,6 +2697,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'さなちゃんねる',
|
||||
'uploader_url': 'https://www.youtube.com/@sana_natori',
|
||||
'uploader_id': '@sana_natori',
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -2684,6 +2728,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'thumbnail': r're:^https?://.*\.webp',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCxzC4EngIsMrPmbm6Nxvb-A',
|
||||
'playable_in_embed': True,
|
||||
'comment_count': int,
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {
|
||||
'extractor_args': {'youtube': {'player_client': ['android'], 'player_skip': ['webpage']}},
|
||||
@@ -2720,6 +2767,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'uploader': 'Christopher Sykes',
|
||||
'uploader_url': 'https://www.youtube.com/@ChristopherSykesDocumentaries',
|
||||
'uploader_id': '@ChristopherSykesDocumentaries',
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -3121,7 +3169,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
return funcname
|
||||
|
||||
return json.loads(js_to_json(self._search_regex(
|
||||
rf'var {re.escape(funcname)}\s*=\s*(\[.+?\]);', jscode,
|
||||
rf'var {re.escape(funcname)}\s*=\s*(\[.+?\])[,;]', jscode,
|
||||
f'Initial JS player n function list ({funcname}.{idx})')))[int(idx)]
|
||||
|
||||
def _extract_n_function_code(self, video_id, player_url):
|
||||
@@ -3337,14 +3385,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
info['author_is_uploader'] = author_is_uploader
|
||||
|
||||
comment_abr = traverse_obj(
|
||||
comment_renderer, ('actionsButtons', 'commentActionButtonsRenderer'), expected_type=dict)
|
||||
comment_renderer, ('actionButtons', 'commentActionButtonsRenderer'), expected_type=dict)
|
||||
if comment_abr is not None:
|
||||
info['is_favorited'] = 'creatorHeart' in comment_abr
|
||||
|
||||
comment_ab_icontype = traverse_obj(
|
||||
comment_renderer, ('authorCommentBadge', 'authorCommentBadgeRenderer', 'icon', 'iconType'))
|
||||
if comment_ab_icontype is not None:
|
||||
info['author_is_verified'] = comment_ab_icontype in ('CHECK_CIRCLE_THICK', 'OFFICIAL_ARTIST_BADGE')
|
||||
badges = self._extract_badges([traverse_obj(comment_renderer, 'authorCommentBadge')])
|
||||
if self._has_badge(badges, BadgeType.VERIFIED):
|
||||
info['author_is_verified'] = True
|
||||
|
||||
is_pinned = traverse_obj(comment_renderer, 'pinnedCommentBadge')
|
||||
if is_pinned:
|
||||
@@ -3581,7 +3628,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
def _is_unplayable(player_response):
|
||||
return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
|
||||
|
||||
_STORY_PLAYER_PARAMS = '8AEB'
|
||||
_PLAYER_PARAMS = 'CgIQBg=='
|
||||
|
||||
def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg, player_url, initial_pr, smuggled_data):
|
||||
|
||||
@@ -3595,7 +3642,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'videoId': video_id,
|
||||
}
|
||||
if smuggled_data.get('is_story') or _split_innertube_client(client)[0] == 'android':
|
||||
yt_query['params'] = self._STORY_PLAYER_PARAMS
|
||||
yt_query['params'] = self._PLAYER_PARAMS
|
||||
|
||||
yt_query.update(self._generate_player_context(sts))
|
||||
return self._extract_response(
|
||||
@@ -3607,7 +3654,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
|
||||
def _get_requested_clients(self, url, smuggled_data):
|
||||
requested_clients = []
|
||||
default = ['android', 'web']
|
||||
default = ['ios', 'android', 'web']
|
||||
allowed_clients = sorted(
|
||||
(client for client in INNERTUBE_CLIENTS.keys() if client[:1] != '_'),
|
||||
key=lambda client: INNERTUBE_CLIENTS[client]['priority'], reverse=True)
|
||||
@@ -3830,6 +3877,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
f'{video_id}: Some formats are possibly damaged. They will be deprioritized', only_once=True)
|
||||
|
||||
client_name = fmt.get(STREAMING_DATA_CLIENT_NAME)
|
||||
name = fmt.get('qualityLabel') or quality.replace('audio_quality_', '') or ''
|
||||
fps = int_or_none(fmt.get('fps')) or 0
|
||||
dct = {
|
||||
'asr': int_or_none(fmt.get('audioSampleRate')),
|
||||
'filesize': int_or_none(fmt.get('contentLength')),
|
||||
@@ -3837,16 +3886,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
'format_note': join_nonempty(
|
||||
join_nonempty(audio_track.get('displayName'),
|
||||
language_preference > 0 and ' (default)', delim=''),
|
||||
fmt.get('qualityLabel') or quality.replace('audio_quality_', ''),
|
||||
fmt.get('isDrc') and 'DRC',
|
||||
name, fmt.get('isDrc') and 'DRC',
|
||||
try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
|
||||
try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
|
||||
throttled and 'THROTTLED', is_damaged and 'DAMAGED',
|
||||
(self.get_param('verbose') or all_formats) and client_name,
|
||||
delim=', '),
|
||||
# Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372
|
||||
'source_preference': -10 if throttled else -5 if itag == '22' else -1,
|
||||
'fps': int_or_none(fmt.get('fps')) or None,
|
||||
'source_preference': ((-10 if throttled else -5 if itag == '22' else -1)
|
||||
+ (100 if 'Premium' in name else 0)),
|
||||
'fps': fps if fps > 1 else None, # For some formats, fps is wrongly returned as 1
|
||||
'audio_channels': fmt.get('audioChannels'),
|
||||
'height': height,
|
||||
'quality': q(quality) - bool(fmt.get('isDrc')) / 2,
|
||||
@@ -3915,11 +3964,17 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
elif itag:
|
||||
f['format_id'] = itag
|
||||
|
||||
if itag in ('616', '235'):
|
||||
f['format_note'] = join_nonempty(f.get('format_note'), 'Premium', delim=' ')
|
||||
f['source_preference'] = (f.get('source_preference') or -1) + 100
|
||||
|
||||
f['quality'] = q(itag_qualities.get(try_get(f, lambda f: f['format_id'].split('-')[0]), -1))
|
||||
if f['quality'] == -1 and f.get('height'):
|
||||
f['quality'] = q(res_qualities[min(res_qualities, key=lambda x: abs(x - f['height']))])
|
||||
if self.get_param('verbose'):
|
||||
if self.get_param('verbose') or all_formats:
|
||||
f['format_note'] = join_nonempty(f.get('format_note'), client_name, delim=', ')
|
||||
if f.get('fps') and f['fps'] <= 1:
|
||||
del f['fps']
|
||||
return True
|
||||
|
||||
subtitles = {}
|
||||
@@ -3992,8 +4047,8 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
webpage = None
|
||||
if 'webpage' not in self._configuration_arg('player_skip'):
|
||||
query = {'bpctr': '9999999999', 'has_verified': '1'}
|
||||
if smuggled_data.get('is_story'):
|
||||
query['pp'] = self._STORY_PLAYER_PARAMS
|
||||
if smuggled_data.get('is_story'): # XXX: Deprecated
|
||||
query['pp'] = self._PLAYER_PARAMS
|
||||
webpage = self._download_webpage(
|
||||
webpage_url, video_id, fatal=False, query=query)
|
||||
|
||||
@@ -4297,9 +4352,13 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
continue
|
||||
trans_code += f'-{lang_code}'
|
||||
trans_name += format_field(lang_name, None, ' from %s')
|
||||
# Add an "-orig" label to the original language so that it can be distinguished.
|
||||
# The subs are returned without "-orig" as well for compatibility
|
||||
if lang_code == f'a-{orig_trans_code}':
|
||||
# Set audio language based on original subtitles
|
||||
for f in formats:
|
||||
if f.get('acodec') != 'none' and not f.get('language'):
|
||||
f['language'] = orig_trans_code
|
||||
# Add an "-orig" label to the original language so that it can be distinguished.
|
||||
# The subs are returned without "-orig" as well for compatibility
|
||||
process_language(
|
||||
automatic_captions, base_url, f'{trans_code}-orig', f'{trans_name} (Original)', {})
|
||||
# Setting tlang=lang returns damaged subtitles.
|
||||
@@ -4319,15 +4378,21 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
info[d_k] = parse_duration(query[k][0])
|
||||
|
||||
# Youtube Music Auto-generated description
|
||||
if video_description:
|
||||
if (video_description or '').strip().endswith('\nAuto-generated by YouTube.'):
|
||||
# XXX: Causes catastrophic backtracking if description has "·"
|
||||
# E.g. https://www.youtube.com/watch?v=DoPaAxMQoiI
|
||||
# Simulating atomic groups: (?P<a>[^xy]+)x => (?=(?P<a>[^xy]+))(?P=a)x
|
||||
# reduces it, but does not fully fix it. https://regex101.com/r/8Ssf2h/2
|
||||
mobj = re.search(
|
||||
r'''(?xs)
|
||||
(?P<track>[^·\n]+)·(?P<artist>[^\n]+)\n+
|
||||
(?P<album>[^\n]+)
|
||||
(?=(?P<track>[^\n·]+))(?P=track)·
|
||||
(?=(?P<artist>[^\n]+))(?P=artist)\n+
|
||||
(?=(?P<album>[^\n]+))(?P=album)\n
|
||||
(?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?
|
||||
(?:.+?Released on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?
|
||||
(.+?\nArtist\s*:\s*(?P<clean_artist>[^\n]+))?
|
||||
.+\nAuto-generated\ by\ YouTube\.\s*$
|
||||
(.+?\nArtist\s*:\s*
|
||||
(?=(?P<clean_artist>[^\n]+))(?P=clean_artist)\n
|
||||
)?.+\nAuto-generated\ by\ YouTube\.\s*$
|
||||
''', video_description)
|
||||
if mobj:
|
||||
release_year = mobj.group('release_year')
|
||||
@@ -4488,6 +4553,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
info['artist'] = mrr_contents_text
|
||||
elif mrr_title == 'Song':
|
||||
info['track'] = mrr_contents_text
|
||||
owner_badges = self._extract_badges(traverse_obj(vsir, ('owner', 'videoOwnerRenderer', 'badges')))
|
||||
if self._has_badge(owner_badges, BadgeType.VERIFIED):
|
||||
info['channel_is_verified'] = True
|
||||
|
||||
info.update({
|
||||
'uploader': info.get('channel'),
|
||||
@@ -4505,7 +4573,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
and 'no-youtube-prefer-utc-upload-date' not in self.get_param('compat_opts', [])
|
||||
):
|
||||
upload_date = strftime_or_none(
|
||||
self._parse_time_text(self._get_text(vpir, 'dateText')), '%Y%m%d') or upload_date
|
||||
self._parse_time_text(self._get_text(vpir, 'dateText'))) or upload_date
|
||||
info['upload_date'] = upload_date
|
||||
|
||||
for s_k, d_k in [('artist', 'creator'), ('track', 'alt_title')]:
|
||||
@@ -4513,7 +4581,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
|
||||
if v:
|
||||
info[d_k] = v
|
||||
|
||||
badges = self._extract_badges(traverse_obj(contents, (..., 'videoPrimaryInfoRenderer'), get_all=False))
|
||||
badges = self._extract_badges(traverse_obj(vpir, 'badges'))
|
||||
|
||||
is_private = (self._has_badge(badges, BadgeType.AVAILABILITY_PRIVATE)
|
||||
or get_first(video_details, 'isPrivate', expected_type=bool))
|
||||
@@ -4586,13 +4654,14 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
||||
channel_id = self.ucid_or_none(renderer['channelId'])
|
||||
title = self._get_text(renderer, 'title')
|
||||
channel_url = format_field(channel_id, None, 'https://www.youtube.com/channel/%s', default=None)
|
||||
# As of 2023-03-01 YouTube doesn't use the channel handles on these renderers yet.
|
||||
# However we can expect them to change that in the future.
|
||||
channel_handle = self.handle_from_url(
|
||||
traverse_obj(renderer, (
|
||||
'navigationEndpoint', (('commandMetadata', 'webCommandMetadata', 'url'),
|
||||
('browseEndpoint', 'canonicalBaseUrl')),
|
||||
{str}), get_all=False))
|
||||
if not channel_handle:
|
||||
# As of 2023-06-01, YouTube sets subscriberCountText to the handle in search
|
||||
channel_handle = self.handle_or_none(self._get_text(renderer, 'subscriberCountText'))
|
||||
return {
|
||||
'_type': 'url',
|
||||
'url': channel_url,
|
||||
@@ -4605,10 +4674,18 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
||||
'title': title,
|
||||
'uploader_id': channel_handle,
|
||||
'uploader_url': format_field(channel_handle, None, 'https://www.youtube.com/%s', default=None),
|
||||
'channel_follower_count': self._get_count(renderer, 'subscriberCountText'),
|
||||
# See above. YouTube sets videoCountText to the subscriber text in search channel renderers.
|
||||
# However, in feed/channels this is set correctly to the subscriber count
|
||||
'channel_follower_count': traverse_obj(
|
||||
renderer, 'subscriberCountText', 'videoCountText', expected_type=self._get_count),
|
||||
'thumbnails': self._extract_thumbnails(renderer, 'thumbnail'),
|
||||
'playlist_count': self._get_count(renderer, 'videoCountText'),
|
||||
'playlist_count': (
|
||||
# videoCountText may be the subscriber count
|
||||
self._get_count(renderer, 'videoCountText')
|
||||
if self._get_count(renderer, 'subscriberCountText') is not None else None),
|
||||
'description': self._get_text(renderer, 'descriptionSnippet'),
|
||||
'channel_is_verified': True if self._has_badge(
|
||||
self._extract_badges(traverse_obj(renderer, 'ownerBadges')), BadgeType.VERIFIED) else None,
|
||||
}
|
||||
|
||||
def _grid_entries(self, grid_renderer):
|
||||
@@ -5024,6 +5101,10 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
||||
'uploader_id': channel_handle,
|
||||
'uploader_url': format_field(channel_handle, None, 'https://www.youtube.com/%s', default=None),
|
||||
})
|
||||
|
||||
channel_badges = self._extract_badges(traverse_obj(data, ('header', ..., 'badges'), get_all=False))
|
||||
if self._has_badge(channel_badges, BadgeType.VERIFIED):
|
||||
info['channel_is_verified'] = True
|
||||
# Playlist stats is a text runs array containing [video count, view count, last updated].
|
||||
# last updated or (view count and last updated) may be missing.
|
||||
playlist_stats = get_first(
|
||||
@@ -5032,7 +5113,7 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
||||
last_updated_unix = self._parse_time_text(
|
||||
self._get_text(playlist_stats, 2) # deprecated, remove when old layout discontinued
|
||||
or self._get_text(playlist_header_renderer, ('byline', 1, 'playlistBylineRenderer', 'text')))
|
||||
info['modified_date'] = strftime_or_none(last_updated_unix, '%Y%m%d')
|
||||
info['modified_date'] = strftime_or_none(last_updated_unix)
|
||||
|
||||
info['view_count'] = self._get_count(playlist_stats, 1)
|
||||
if info['view_count'] is None: # 0 is allowed
|
||||
@@ -5132,7 +5213,7 @@ class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor):
|
||||
playlist_header_renderer = traverse_obj(data, ('header', 'playlistHeaderRenderer')) or {}
|
||||
player_header_privacy = playlist_header_renderer.get('privacy')
|
||||
|
||||
badges = self._extract_badges(sidebar_renderer)
|
||||
badges = self._extract_badges(traverse_obj(sidebar_renderer, 'badges'))
|
||||
|
||||
# Personal playlists, when authenticated, have a dropdown visibility selector instead of a badge
|
||||
privacy_setting_icon = get_first(
|
||||
@@ -5382,7 +5463,8 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'uploader_url': 'https://www.youtube.com/@3blue1brown',
|
||||
'uploader': '3Blue1Brown',
|
||||
'tags': ['Mathematics'],
|
||||
'channel_follower_count': int
|
||||
'channel_follower_count': int,
|
||||
'channel_is_verified': True,
|
||||
},
|
||||
}, {
|
||||
'note': 'playlists, singlepage',
|
||||
@@ -5559,6 +5641,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'uploader_url': 'https://www.youtube.com/@3blue1brown',
|
||||
'uploader_id': '@3blue1brown',
|
||||
'uploader': '3Blue1Brown',
|
||||
'channel_is_verified': True,
|
||||
},
|
||||
}, {
|
||||
'url': 'https://invidio.us/channel/UCmlqkdCBesrv2Lak1mF_MxA',
|
||||
@@ -5722,7 +5805,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
}, {
|
||||
'url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ/live',
|
||||
'info_dict': {
|
||||
'id': 'AlTsmyW4auo', # This will keep changing
|
||||
'id': 'hGkQjiJLjWQ', # This will keep changing
|
||||
'ext': 'mp4',
|
||||
'title': str,
|
||||
'upload_date': r're:\d{8}',
|
||||
@@ -5746,6 +5829,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'uploader_url': 'https://www.youtube.com/@SkyNews',
|
||||
'uploader_id': '@SkyNews',
|
||||
'uploader': 'Sky News',
|
||||
'channel_is_verified': True,
|
||||
},
|
||||
'params': {
|
||||
'skip_download': True,
|
||||
@@ -6234,7 +6318,8 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'channel': str,
|
||||
'uploader': str,
|
||||
'uploader_url': str,
|
||||
'uploader_id': str
|
||||
'uploader_id': str,
|
||||
'channel_is_verified': bool, # this will keep changing
|
||||
}
|
||||
}],
|
||||
'params': {'extract_flat': True, 'playlist_items': '1'},
|
||||
@@ -6270,6 +6355,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'uploader': 'PewDiePie',
|
||||
'uploader_url': 'https://www.youtube.com/@PewDiePie',
|
||||
'uploader_id': '@PewDiePie',
|
||||
'channel_is_verified': True,
|
||||
}
|
||||
}],
|
||||
'params': {'extract_flat': True},
|
||||
@@ -6288,6 +6374,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'uploader_url': 'https://www.youtube.com/@3blue1brown',
|
||||
'uploader_id': '@3blue1brown',
|
||||
'uploader': '3Blue1Brown',
|
||||
'channel_is_verified': True,
|
||||
},
|
||||
'playlist_count': 0,
|
||||
}, {
|
||||
@@ -6322,6 +6409,7 @@ class YoutubeTabIE(YoutubeTabBaseInfoExtractor):
|
||||
'description': 'I make music',
|
||||
'channel_url': 'https://www.youtube.com/channel/UCgFwu-j5-xNJml2FtTrrB3A',
|
||||
'channel_follower_count': int,
|
||||
'channel_is_verified': True,
|
||||
},
|
||||
'playlist_mincount': 10,
|
||||
}]
|
||||
@@ -6897,12 +6985,15 @@ class YoutubeSearchURLIE(YoutubeTabBaseInfoExtractor):
|
||||
'description': 'md5:4ae48dfa9505ffc307dad26342d06bfc',
|
||||
'title': 'Kurzgesagt – In a Nutshell',
|
||||
'channel_id': 'UCsXVk37bltHxD1rDPwtNM8Q',
|
||||
'playlist_count': int, # XXX: should have a way of saying > 1
|
||||
# No longer available for search as it is set to the handle.
|
||||
# 'playlist_count': int,
|
||||
'channel_url': 'https://www.youtube.com/channel/UCsXVk37bltHxD1rDPwtNM8Q',
|
||||
'thumbnails': list,
|
||||
'uploader_id': '@kurzgesagt',
|
||||
'uploader_url': 'https://www.youtube.com/@kurzgesagt',
|
||||
'uploader': 'Kurzgesagt – In a Nutshell',
|
||||
'channel_is_verified': True,
|
||||
'channel_follower_count': int,
|
||||
}
|
||||
}],
|
||||
'params': {'extract_flat': True, 'playlist_items': '1'},
|
||||
@@ -7166,6 +7257,8 @@ class YoutubeClipIE(YoutubeTabBaseInfoExtractor):
|
||||
'live_status': 'not_live',
|
||||
'channel_follower_count': int,
|
||||
'chapters': 'count:20',
|
||||
'comment_count': int,
|
||||
'heatmap': 'count:100',
|
||||
}
|
||||
}]
|
||||
|
||||
@@ -7226,6 +7319,8 @@ class YoutubeConsentRedirectIE(YoutubeBaseInfoExtractor):
|
||||
'channel': 'さなちゃんねる',
|
||||
'description': 'md5:6aebf95cc4a1d731aebc01ad6cc9806d',
|
||||
'uploader': 'さなちゃんねる',
|
||||
'channel_is_verified': True,
|
||||
'heatmap': 'count:100',
|
||||
},
|
||||
'add_ie': ['Youtube'],
|
||||
'params': {'skip_download': 'Youtube'},
|
||||
|
||||
Reference in New Issue
Block a user