feat(youtube): enhance channel info extraction and fix api version

- Extract author verification status, tags, and tabs based on Invidious logic
- Add support for auto-generated channels lacking metadata blocks
- Update x-youtube-client-version to 2.20260722.01.00 to resolve 400 Bad Request errors
This commit is contained in:
localhost 2026-07-24 12:53:47 +02:00
parent dc7bb8e5d5
commit 9808ee19cc
1 changed files with 112 additions and 16 deletions

View File

@ -409,7 +409,7 @@ export async function fetchChannelVideos(
hl: 'en', hl: 'en',
gl: 'US', gl: 'US',
clientName: 'WEB', clientName: 'WEB',
clientVersion: '2.20250222.10.00' clientVersion: '2.20260722.01.00'
} }
}, },
continuation: token continuation: token
@ -418,7 +418,7 @@ export async function fetchChannelVideos(
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'x-youtube-client-name': '1', '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' '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; 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) { export async function fetchChannelInfo(channelId: string) {
const payload = { const payload = {
context: { context: {
@ -532,16 +573,15 @@ export async function fetchChannelInfo(channelId: string) {
hl: 'en', hl: 'en',
gl: 'US', gl: 'US',
clientName: 'WEB', clientName: 'WEB',
clientVersion: '2.20250222.10.00' clientVersion: '2.20260722.01.00'
} }
}, },
browseId: channelId browseId: channelId
}; };
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'x-youtube-client-name': '1', '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' '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}`); throw new Error(`YouTube API error ${data.error.code}: ${data.error.message}`);
} }
const renderer = data.metadata?.channelMetadataRenderer; if (data.alerts?.length && data.alerts[0]?.alertRenderer?.type === 'ERROR') {
if (!renderer) { throw new Error(data.alerts[0].alertRenderer.text?.simpleText || 'Channel error');
throw new Error('Could not find channel metadata renderer in YouTube response');
} }
const author = renderer.title || ''; const pageHeaderViewModel = data.header?.pageHeaderRenderer?.content?.pageHeaderViewModel;
const authorId = renderer.externalId || channelId;
const authorUrl = `/channel/${authorId}`;
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) : []; const authorThumbnails = rawAvatarUrl ? getAuthorThumbnails(rawAvatarUrl) : [];
// Parse subscriber and video counts // Parse subscriber and video counts
let subCount = 0; let subCount = 0;
let videoCount = 0; let videoCount = 0;
const metadataRows = data.header?.pageHeaderRenderer?.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows; const metadataRows = pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows;
if (Array.isArray(metadataRows)) { if (Array.isArray(metadataRows)) {
for (const row of metadataRows) { for (const row of metadataRows) {
if (Array.isArray(row.metadataParts)) { 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 { return {
type: 'channel', type: 'channel',
@ -601,12 +691,18 @@ export async function fetchChannelInfo(channelId: string) {
authorId, authorId,
authorUrl, authorUrl,
authorThumbnails, authorThumbnails,
authorBanners,
banner,
subCount, subCount,
videoCount, videoCount,
description, description,
descriptionHtml: description, descriptionHtml: description,
authorVerified: false, authorVerified,
autoGenerated: false autoGenerated,
isFamilyFriendly,
allowedRegions,
tabs,
tags
}; };
} }