-
Notifications
You must be signed in to change notification settings - Fork 10.2k
fix(route/youtube): handle LockupView format in youtubei API responses #22662
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Array.prototype.flatMap(), Array.prototype.map() and Array.prototype.find() instead. |
||
| return undefined; | ||
| }; | ||
|
Comment on lines
+45
to
+156
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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`, | ||
|
|
@@ -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, | ||
| ], | ||
| }; | ||
| }) | ||
| ), | ||
| }; | ||
| }; | ||
|
|
@@ -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 | ||
| ? [ | ||
|
|
@@ -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), | ||
| }, | ||
| ], | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Array.prototype.flatMap(), Array.prototype.map() and Array.prototype.find() instead.