diff options
| author | Astounds <kirito@disroot.org> | 2026-03-31 21:38:51 -0500 |
|---|---|---|
| committer | Astounds <kirito@disroot.org> | 2026-03-31 21:38:51 -0500 |
| commit | 06051dd127fd9805442ee10b569f36a611dbbc8e (patch) | |
| tree | af6c8c40fb4189d7ef4748a1b47b14959cc7def1 /youtube/yt_data_extract/common.py | |
| parent | 7c64630be1fd781f5964799da04d43cf191c61c3 (diff) | |
| download | yt-local-06051dd127fd9805442ee10b569f36a611dbbc8e.tar.lz yt-local-06051dd127fd9805442ee10b569f36a611dbbc8e.tar.xz yt-local-06051dd127fd9805442ee10b569f36a611dbbc8e.zip | |
fix: support YouTube 2024+ data formats for playlists, podcasts and channels
- Add PODCAST content type support in lockupViewModel extraction
- Extract thumbnails and episode count from thumbnail overlay badges
- Migrate playlist page fetching from pbj=1 to innertube API (youtubei/v1/browse)
- Support new pageHeaderRenderer format in playlist metadata extraction
- Fix subscriber count extraction when YouTube returns handle instead of count
- Hide "None subscribers" in template when data is unavailable
Diffstat (limited to 'youtube/yt_data_extract/common.py')
| -rw-r--r-- | youtube/yt_data_extract/common.py | 50 |
1 files changed, 40 insertions, 10 deletions
diff --git a/youtube/yt_data_extract/common.py b/youtube/yt_data_extract/common.py index 7d44fae..9a940ea 100644 --- a/youtube/yt_data_extract/common.py +++ b/youtube/yt_data_extract/common.py @@ -241,7 +241,7 @@ def extract_lockup_view_model_info(item, additional_info={}): info['title'] = title_data.get('content', '') # Determine type based on contentType - if 'PLAYLIST' in content_type: + if 'PLAYLIST' in content_type or 'PODCAST' in content_type: info['type'] = 'playlist' info['playlist_type'] = 'playlist' info['id'] = content_id @@ -253,7 +253,7 @@ def extract_lockup_view_model_info(item, additional_info={}): for row in metadata_rows.get('contentMetadataViewModel', {}).get('metadataRows', []): for part in row.get('metadataParts', []): text = part.get('text', {}).get('content', '') - if 'video' in text.lower(): + if 'video' in text.lower() or 'episode' in text.lower(): info['video_count'] = extract_int(text) elif 'VIDEO' in content_type: info['type'] = 'video' @@ -276,25 +276,48 @@ def extract_lockup_view_model_info(item, additional_info={}): info['type'] = 'channel' info['id'] = content_id info['approx_subscriber_count'] = None + info['video_count'] = None + + # Extract subscriber count and video count from metadata rows + metadata_rows = lockup_metadata.get('metadata', {}) + for row in metadata_rows.get('contentMetadataViewModel', {}).get('metadataRows', []): + for part in row.get('metadataParts', []): + text = part.get('text', {}).get('content', '') + if 'subscriber' in text.lower(): + info['approx_subscriber_count'] = extract_approx_int(text) + elif 'video' in text.lower(): + info['video_count'] = extract_int(text) else: info['type'] = 'unsupported' return info # Extract thumbnail from contentImage content_image = item.get('contentImage', {}) - collection_thumb = content_image.get('collectionThumbnailViewModel', {}) - primary_thumb = collection_thumb.get('primaryThumbnail', {}) - thumb_vm = primary_thumb.get('thumbnailViewModel', {}) - image_sources = thumb_vm.get('image', {}).get('sources', []) - if image_sources: - info['thumbnail'] = image_sources[0].get('url', '') - else: - info['thumbnail'] = '' + info['thumbnail'] = normalize_url(multi_deep_get(content_image, + # playlists with collection thumbnail + ['collectionThumbnailViewModel', 'primaryThumbnail', 'thumbnailViewModel', 'image', 'sources', 0, 'url'], + # single thumbnail (some playlists, videos) + ['thumbnailViewModel', 'image', 'sources', 0, 'url'], + )) or '' + + # Extract video/episode count from thumbnail overlay badges + # (podcasts and some playlists put the count here instead of metadata rows) + thumb_vm = multi_deep_get(content_image, + ['collectionThumbnailViewModel', 'primaryThumbnail', 'thumbnailViewModel'], + ['thumbnailViewModel'], + ) or {} + for overlay in thumb_vm.get('overlays', []): + for badge in deep_get(overlay, 'thumbnailOverlayBadgeViewModel', 'thumbnailBadges', default=[]): + badge_text = deep_get(badge, 'thumbnailBadgeViewModel', 'text', default='') + if badge_text and not info.get('video_count'): + conservative_update(info, 'video_count', extract_int(badge_text)) # Extract author info if available info['author'] = None info['author_id'] = None info['author_url'] = None + info['description'] = None + info['badges'] = [] # Try to get first video ID from inline player data item_playback = item.get('itemPlayback', {}) @@ -463,6 +486,13 @@ def extract_item_info(item, additional_info={}): elif primary_type == 'channel': info['id'] = item.get('channelId') info['approx_subscriber_count'] = extract_approx_int(item.get('subscriberCountText')) + # YouTube sometimes puts the handle (@name) in subscriberCountText + # instead of the actual count. Fall back to accessibility data. + if not info['approx_subscriber_count']: + acc_label = deep_get(item, 'subscriberCountText', + 'accessibility', 'accessibilityData', 'label', default='') + if 'subscriber' in acc_label.lower(): + info['approx_subscriber_count'] = extract_approx_int(acc_label) elif primary_type == 'show': info['id'] = deep_get(item, 'navigationEndpoint', 'watchEndpoint', 'playlistId') info['first_video_id'] = deep_get(item, 'navigationEndpoint', |
