diff --git a/utils/youtube-channel.ts b/utils/youtube-channel.ts index 9703bd2..b893a47 100644 --- a/utils/youtube-channel.ts +++ b/utils/youtube-channel.ts @@ -409,7 +409,7 @@ export async function fetchChannelVideos( hl: 'en', gl: 'US', clientName: 'WEB', - clientVersion: '2.20250222.10.00' + clientVersion: '2.20260722.01.00' } }, continuation: token @@ -418,7 +418,7 @@ export async function fetchChannelVideos( const headers = { 'Content-Type': 'application/json', 'x-youtube-client-name': '1', - 'x-youtube-client-version': '2.20250222.10.00', + 'x-youtube-client-version': '2.20260722.01.00', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' }; @@ -525,6 +525,47 @@ export function relativeTimeToTimestamp(publishedText: string): number { return (now - diff) * 1000; } +interface YouTubeBrowseResponse { + error?: { code: string | number; message: string }; + alerts?: Array<{ alertRenderer?: { type?: string; text?: { simpleText?: string } } }>; + header?: { + pageHeaderRenderer?: { + content?: { + pageHeaderViewModel?: { + title?: { dynamicTextViewModel?: { text?: { content?: string; attachmentRuns?: Array<{ element?: { type?: { imageType?: { image?: { sources?: Array<{ clientResource?: { imageName?: string } }> } } } } }> } } }; + image?: { imageViewModel?: { image?: { sources?: Array<{ url: string }> } } }; + animatedImage?: { contentPreviewImageViewModel?: { image?: { sources?: Array<{ url: string }> } } }; + metadata?: { contentMetadataViewModel?: { metadataRows?: Array<{ metadataParts?: Array<{ text?: { content?: string } }> }> } }; + banner?: { imageBannerViewModel?: { image?: { sources?: Array<{ url: string; width: number; height: number }> } } }; + } + } + }; + c4TabbedHeaderRenderer?: { + banner?: { thumbnails?: Array<{ url: string; width: number; height: number }> }; + }; + }; + metadata?: { + channelMetadataRenderer?: { + title?: string; + externalId?: string; + avatar?: { thumbnails?: Array<{ url: string }> }; + description?: string; + }; + }; + contents?: { + twoColumnBrowseResultsRenderer?: { + tabs?: Array<{ tabRenderer?: { title?: string } }>; + }; + }; + microformat?: { + microformatDataRenderer?: { + tags?: string[]; + familySafe?: boolean; + availableCountries?: string[]; + }; + }; +} + export async function fetchChannelInfo(channelId: string) { const payload = { context: { @@ -532,16 +573,15 @@ export async function fetchChannelInfo(channelId: string) { hl: 'en', gl: 'US', clientName: 'WEB', - clientVersion: '2.20250222.10.00' + clientVersion: '2.20260722.01.00' } }, browseId: channelId }; - const headers = { 'Content-Type': 'application/json', 'x-youtube-client-name': '1', - 'x-youtube-client-version': '2.20250222.10.00', + 'x-youtube-client-version': '2.20260722.01.00', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' }; @@ -560,22 +600,42 @@ export async function fetchChannelInfo(channelId: string) { throw new Error(`YouTube API error ${data.error.code}: ${data.error.message}`); } - const renderer = data.metadata?.channelMetadataRenderer; - if (!renderer) { - throw new Error('Could not find channel metadata renderer in YouTube response'); + if (data.alerts?.length && data.alerts[0]?.alertRenderer?.type === 'ERROR') { + throw new Error(data.alerts[0].alertRenderer.text?.simpleText || 'Channel error'); } - const author = renderer.title || ''; - const authorId = renderer.externalId || channelId; - const authorUrl = `/channel/${authorId}`; + const pageHeaderViewModel = data.header?.pageHeaderRenderer?.content?.pageHeaderViewModel; - const rawAvatarUrl = renderer.avatar?.thumbnails?.[0]?.url || ''; + let author = ''; + let authorId = channelId; + let rawAvatarUrl = ''; + let description = ''; + let autoGenerated = false; + + if (data.metadata?.channelMetadataRenderer) { + const renderer = data.metadata.channelMetadataRenderer; + author = renderer.title || ''; + authorId = renderer.externalId || channelId; + rawAvatarUrl = renderer.avatar?.thumbnails?.[0]?.url || ''; + description = renderer.description || ''; + } else { + // Auto-generated channels often have no metadata + autoGenerated = true; + author = pageHeaderViewModel?.title?.dynamicTextViewModel?.text?.content || ''; + + // Extract avatar for auto-generated channels (Gaming, Music, etc.) + rawAvatarUrl = pageHeaderViewModel?.image?.imageViewModel?.image?.sources?.[0]?.url + || pageHeaderViewModel?.animatedImage?.contentPreviewImageViewModel?.image?.sources?.[0]?.url + || ''; + } + + const authorUrl = `/channel/${authorId}`; const authorThumbnails = rawAvatarUrl ? getAuthorThumbnails(rawAvatarUrl) : []; // Parse subscriber and video counts let subCount = 0; let videoCount = 0; - const metadataRows = data.header?.pageHeaderRenderer?.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows; + const metadataRows = pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows; if (Array.isArray(metadataRows)) { for (const row of metadataRows) { if (Array.isArray(row.metadataParts)) { @@ -593,7 +653,37 @@ export async function fetchChannelInfo(channelId: string) { } } - const description = renderer.description || ''; + // Check for verified badge + let authorVerified = false; + const badgeName = pageHeaderViewModel?.title?.dynamicTextViewModel?.text?.attachmentRuns?.[0]?.element?.type?.imageType?.image?.sources?.[0]?.clientResource?.imageName; + if (badgeName === 'CHECK_CIRCLE_FILLED' || badgeName === 'AUDIO_BADGE') { + authorVerified = true; + } + + // Extract tabs + const tabsRaw = data.contents?.twoColumnBrowseResultsRenderer?.tabs; + let tabs: string[] = []; + if (Array.isArray(tabsRaw)) { + tabs = tabsRaw + .map((t: { tabRenderer?: { title?: string } }) => t.tabRenderer?.title?.toLowerCase()) + .filter((title): title is string => Boolean(title)) + .map((name: string) => name === 'live' ? 'streams' : name); + } + + // Extract microformat data + const tags = data.microformat?.microformatDataRenderer?.tags || []; + const isFamilyFriendly = data.microformat?.microformatDataRenderer?.familySafe ?? true; + const allowedRegions = data.microformat?.microformatDataRenderer?.availableCountries || []; + + // Extract banners + let authorBanners = []; + const bannerSources = pageHeaderViewModel?.banner?.imageBannerViewModel?.image?.sources; + if (Array.isArray(bannerSources)) { + authorBanners = bannerSources; + } else if (data.header?.c4TabbedHeaderRenderer?.banner?.thumbnails) { + authorBanners = data.header.c4TabbedHeaderRenderer.banner.thumbnails; + } + const banner = authorBanners.length > 0 ? authorBanners[authorBanners.length - 1].url : null; return { type: 'channel', @@ -601,12 +691,18 @@ export async function fetchChannelInfo(channelId: string) { authorId, authorUrl, authorThumbnails, + authorBanners, + banner, subCount, videoCount, description, descriptionHtml: description, - authorVerified: false, - autoGenerated: false + authorVerified, + autoGenerated, + isFamilyFriendly, + allowedRegions, + tabs, + tags }; }