Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 156 additions & 32 deletions lib/routes/youtube/api/youtubei.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,131 @@ export const getDataByUsername = async ({ username, embed, filterShorts, isJsonF
return getDataByChannelId({ channelId, embed, filterShorts, isJsonFeed });
};

/**
* Extract video ID from either legacy Video/GridVideo objects (video_id)
* or the newer LockupView objects (content_id).
*/
const extractVideoId = (video: any): string | undefined => {
if ('video_id' in video) {
return video.video_id;
}
if ('content_id' in video && video.content_type === 'VIDEO') {
return video.content_id;
}
if ('id' in video) {
return video.id;
}
return undefined;
};

/**
* Extract the relative-time publish string.
* Legacy format: video.published.text
* LockupView format: metadata.metadata.metadata_rows[].metadata_parts[].text.text
*/
const extractPublishedText = (video: any): string | undefined => {
if ('published' in video && video.published?.text) {
return video.published.text;
}
const rows = video.metadata?.metadata?.metadata_rows;
if (Array.isArray(rows)) {
for (const row of rows) {
const parts = row?.metadata_parts;
if (Array.isArray(parts)) {
for (const part of parts) {
const text = part?.text?.text;
if (typeof text === 'string' && /ago$/i.test(text)) {
return text;
}
}
}
}
}
Comment on lines +67 to +80

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return undefined;
};

/**
* Extract the best available thumbnail URL.
* Legacy: best_thumbnail.url or thumbnails[0].url
* LockupView: content_image.image[0].url
*/
const extractThumbnail = (video: any): string | undefined => {
if ('best_thumbnail' in video) {
return video.best_thumbnail?.url;
}
if ('thumbnails' in video) {
return video.thumbnails?.[0]?.url;
}
const images = video.content_image?.image;
if (Array.isArray(images) && images.length > 0) {
return images[0].url;
}
return undefined;
};

/**
* Extract title text.
* Legacy: video.title.text
* LockupView: video.metadata.title.text
*/
const extractTitle = (video: any, videoId: string): string => {
if (video.title?.text) {
return video.title.text;
}
if (video.metadata?.title?.text) {
return video.metadata.title.text;
}
return `YouTube Video ${videoId}`;
};

const extractAuthor = (video: any): string | undefined => {
if (typeof video.author === 'string') {
return video.author;
}
if (video.author && video.author.name !== 'N/A') {
return video.author.name;
}
return undefined;
};

/**
* Extract duration in seconds.
* Legacy: video.duration.seconds
* LockupView: parse from content_image.overlays[].badges[].text (e.g. "3:22")
*/
const extractDurationSeconds = (video: any): number | undefined => {
if (video.duration && 'seconds' in video.duration) {
return video.duration.seconds;
}
const overlays = video.content_image?.overlays;
if (Array.isArray(overlays)) {
for (const overlay of overlays) {
const badges = overlay?.badges;
if (Array.isArray(badges)) {
for (const badge of badges) {
const text = badge?.text;
if (typeof text === 'string' && /^\d+:\d{2}(?::\d{2})?$/.test(text)) {
const parts = text.split(':').map(Number);
if (parts.length === 3) {
return parts[0] * 3600 + parts[1] * 60 + parts[2];
}
return parts[0] * 60 + parts[1];
}
}
}
}
}
Comment on lines +137 to +154

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return undefined;
};
Comment on lines +45 to +156

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed the response format from Video/GridVideo to LockupView objects

If what you said is true, then why haven't the codes for the old formats that you know never execute been removed? If you have found an example that still uses the old formats, please provide it.


export const getDataByChannelId = async ({ channelId, embed, isJsonFeed }: { channelId: string; embed: boolean; filterShorts: boolean; isJsonFeed: boolean }): Promise<Data> => {
const innertube = await getInnertube();
const channel = await innertube.getChannel(channelId);
const videos = await channel.getVideos();
const videoSubtitles = isJsonFeed ? await getSrtAttachmentBatch(videos.videos.filter((video) => 'video_id' in video).map((video) => video.video_id)) : {};

const validVideos = videos.videos.filter((video) => extractVideoId(video) !== undefined);
const videoIds = validVideos.map((video) => extractVideoId(video)!);
const videoSubtitles = isJsonFeed ? await getSrtAttachmentBatch(videoIds) : {};

return {
title: `${channel.metadata.title || channelId} - YouTube`,
Expand All @@ -51,29 +171,30 @@ export const getDataByChannelId = async ({ channelId, embed, isJsonFeed }: { cha
description: channel.metadata.description,

item: await Promise.all(
videos.videos
.filter((video) => 'video_id' in video)
.map((video) => {
const srtAttachments = isJsonFeed ? videoSubtitles[video.video_id] || [] : [];
const img = 'best_thumbnail' in video ? video.best_thumbnail?.url : 'thumbnails' in video ? video.thumbnails?.[0]?.url : undefined;

return {
title: video.title.text || `YouTube Video ${video.video_id}`,
description: 'description_snippet' in video ? utils.renderDescription(embed, video.video_id, img, utils.formatDescription(video.description_snippet?.toHTML())) : null,
link: `https://www.youtube.com/watch?v=${video.video_id}`,
author: typeof video.author === 'string' ? video.author : video.author.name === 'N/A' ? undefined : video.author.name,
image: img,
pubDate: 'published' in video && video.published?.text ? parseRelativeDate(video.published.text) : undefined,
attachments: [
{
url: getVideoUrl(video.video_id),
mime_type: 'text/html',
duration_in_seconds: video.duration && 'seconds' in video.duration ? video.duration.seconds : undefined,
},
...srtAttachments,
],
};
})
validVideos.map((video) => {
const videoId = extractVideoId(video)!;
const srtAttachments = isJsonFeed ? videoSubtitles[videoId] || [] : [];
const img = extractThumbnail(video);
const title = extractTitle(video, videoId);
const publishedText = extractPublishedText(video);

return {
title,
description: 'description_snippet' in video ? utils.renderDescription(embed, videoId, img, utils.formatDescription(video.description_snippet?.toHTML())) : utils.renderDescription(embed, videoId, img, ''),
link: `https://www.youtube.com/watch?v=${videoId}`,
author: extractAuthor(video),
image: img,
pubDate: publishedText ? parseRelativeDate(publishedText) : undefined,
attachments: [
{
url: getVideoUrl(videoId),
mime_type: 'text/html',
duration_in_seconds: extractDurationSeconds(video),
},
...srtAttachments,
],
};
})
),
};
};
Expand All @@ -90,15 +211,18 @@ export const getDataByPlaylistId = async ({ playlistId, embed }: { playlistId: s
description: playlist.info.description || `${playlist.info.title} by ${playlist.info.author.name}`,

item: videos
.filter((video) => 'id' in video)
.filter((video) => extractVideoId(video) !== undefined)
.map((video) => {
const img = 'best_thumbnail' in video ? video.best_thumbnail?.url : video.thumbnails?.[0]?.url;
const videoId = extractVideoId(video)!;
const img = extractThumbnail(video);
const title = extractTitle(video, videoId);
const publishedText = extractPublishedText(video);

return {
title: video.title.text || `YouTube Video ${video.id}`,
description: utils.renderDescription(embed, video.id, img, ''),
link: `https://www.youtube.com/watch?v=${video.id}`,
pubDate: 'published' in video && video.published?.text ? parseRelativeDate(video.published.text) : undefined,
title,
description: utils.renderDescription(embed, videoId, img, ''),
link: `https://www.youtube.com/watch?v=${videoId}`,
pubDate: publishedText ? parseRelativeDate(publishedText) : undefined,
author:
'author' in video
? [
Expand All @@ -112,9 +236,9 @@ export const getDataByPlaylistId = async ({ playlistId, embed }: { playlistId: s
image: img,
attachments: [
{
url: getVideoUrl(video.id),
url: getVideoUrl(videoId),
mime_type: 'text/html',
duration_in_seconds: 'duration' in video && video.duration && 'seconds' in video.duration ? video.duration.seconds : undefined,
duration_in_seconds: extractDurationSeconds(video),
},
],
};
Expand Down
Loading