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:
parent
dc7bb8e5d5
commit
9808ee19cc
|
|
@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue