diff --git a/README.md b/README.md index a3c77b054..b30944ff5 100644 --- a/README.md +++ b/README.md @@ -248,7 +248,7 @@ const yt = await Innertube.create({ Methods

- * [.getInfo(video_id, client?)](#getinfo) + * [.getInfo(target, client?)](#getinfo) * [.getBasicInfo(video_id, client?)](#getbasicinfo) * [.search(query, filters?)](#search) * [.getSearchSuggestions(query)](#getsearchsuggestions) @@ -273,7 +273,7 @@ const yt = await Innertube.create({ -### getInfo(video_id, client?) +### getInfo(target, client?) Retrieves video info, including playback data and even layout elements such as menus, buttons, etc — all nicely parsed. @@ -281,7 +281,7 @@ Retrieves video info, including playback data and even layout elements such as m | Param | Type | Description | | --- | --- | --- | -| video_id | `string` | The id of the video | +| target | `string` \| `NavigationEndpoint` | If `string`, the id of the video. If `NavigationEndpoint`, the endpoint of watchable elements such as `Video`, `Mix` and `Playlist`. To clarify, valid endpoints have payloads containing at least `videoId` and optionally `playlistId`, `params` and `index`. | | client? | `InnerTubeClient` | `WEB`, `ANDROID`, `YTMUSIC`, `YTMUSIC_ANDROID` or `TV_EMBEDDED` |

@@ -321,6 +321,9 @@ Retrieves video info, including playback data and even layout elements such as m - `#addToWatchHistory()` - Adds the video to the watch history. +- `#autoplay_video_endpoint` + - Returns the endpoint of the video for Autoplay. + - `#page` - Returns original InnerTube response (sanitized). diff --git a/src/Innertube.ts b/src/Innertube.ts index 18fd36c4e..7bbcf7dcb 100644 --- a/src/Innertube.ts +++ b/src/Innertube.ts @@ -31,7 +31,7 @@ import type Format from './parser/classes/misc/Format.js'; import type { ApiResponse } from './core/Actions.js'; import type { IBrowseResponse, IParsedResponse } from './parser/types/index.js'; import type { DownloadOptions, FormatOptions } from './utils/FormatUtils.js'; -import { generateRandomString, throwIfMissing } from './utils/Utils.js'; +import { generateRandomString, InnertubeError, throwIfMissing } from './utils/Utils.js'; export type InnertubeConfig = SessionOptions; @@ -72,16 +72,48 @@ class Innertube { /** * Retrieves video info. - * @param video_id - The video id. + * @param target - The video id or `NavigationEndpoint`. * @param client - The client to use. */ - async getInfo(video_id: string, client?: InnerTubeClient): Promise { - throwIfMissing({ video_id }); + async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise { + throwIfMissing({ target }); + + let payload: { + videoId: string, + playlistId?: string, + params?: string, + playlistIndex?: number + }; + + if (target instanceof NavigationEndpoint) { + const video_id = target.payload?.videoId; + if (!video_id) { + throw new InnertubeError('Missing video id in endpoint payload.', target); + } + payload = { + videoId: video_id + }; + if (target.payload.playlistId) { + payload.playlistId = target.payload.playlistId; + } + if (target.payload.params) { + payload.params = target.payload.params; + } + if (target.payload.index) { + payload.playlistIndex = target.payload.index; + } + } else if (typeof target === 'string') { + payload = { + videoId: target + }; + } else { + throw new InnertubeError('Invalid target, expected either a video id or a valid NavigationEndpoint', target); + } const cpn = generateRandomString(16); - const initial_info = this.actions.getVideoInfo(video_id, cpn, client); - const continuation = this.actions.execute('/next', { videoId: video_id }); + const initial_info = this.actions.getVideoInfo(payload.videoId, cpn, client); + const continuation = this.actions.execute('/next', payload); const response = await Promise.all([ initial_info, continuation ]); return new VideoInfo(response, this.actions, this.session.player, cpn); diff --git a/src/parser/classes/TwoColumnWatchNextResults.ts b/src/parser/classes/TwoColumnWatchNextResults.ts index 875d83e05..bc4167100 100644 --- a/src/parser/classes/TwoColumnWatchNextResults.ts +++ b/src/parser/classes/TwoColumnWatchNextResults.ts @@ -1,5 +1,15 @@ import Parser from '../index.js'; import { YTNode } from '../helpers.js'; +import Text from './misc/Text.js'; +import PlaylistAuthor from './misc/PlaylistAuthor.js'; +import NavigationEndpoint from './NavigationEndpoint.js'; + +import type Menu from './menus/Menu.js'; + +type AutoplaySet = { + autoplay_video: NavigationEndpoint, + next_button_video?: NavigationEndpoint +}; class TwoColumnWatchNextResults extends YTNode { static type = 'TwoColumnWatchNextResults'; @@ -7,12 +17,66 @@ class TwoColumnWatchNextResults extends YTNode { results; secondary_results; conversation_bar; + playlist?: { + id: string, + title: string, + author: Text | PlaylistAuthor, + contents: YTNode[], + current_index: number, + is_infinite: boolean, + menu: Menu | null + }; + autoplay?: { + sets: AutoplaySet[], + modified_sets?: AutoplaySet[], + count_down_secs?: number + }; constructor(data: any) { super(); this.results = Parser.parseArray(data.results?.results.contents); this.secondary_results = Parser.parseArray(data.secondaryResults?.secondaryResults.results); this.conversation_bar = Parser.parseItem(data?.conversationBar); + + const playlistData = data.playlist?.playlist; + if (playlistData) { + this.playlist = { + id: playlistData.playlistId, + title: playlistData.title, + author: playlistData.shortBylineText?.simpleText ? + new Text(playlistData.shortBylineText) : + new PlaylistAuthor(playlistData.longBylineText), + contents: Parser.parseArray(playlistData.contents), + current_index: playlistData.currentIndex, + is_infinite: !!playlistData.isInfinite, + menu: Parser.parseItem(playlistData.menu) + }; + } + + const autoplayData = data.autoplay?.autoplay; + if (autoplayData) { + this.autoplay = { + sets: autoplayData.sets.map((set: any) => this.#parseAutoplaySet(set)) + }; + if (autoplayData.modifiedSets) { + this.autoplay.modified_sets = autoplayData.modifiedSets.map((set: any) => this.#parseAutoplaySet(set)); + } + if (autoplayData.countDownSecs) { + this.autoplay.count_down_secs = autoplayData.countDownSecs; + } + } + } + + #parseAutoplaySet(data: any): AutoplaySet { + const result = { + autoplay_video: new NavigationEndpoint(data.autoplayVideo) + } as AutoplaySet; + + if (data.nextButtonVideo) { + result.next_button_video = new NavigationEndpoint(data.nextButtonVideo); + } + + return result; } } diff --git a/src/parser/youtube/VideoInfo.ts b/src/parser/youtube/VideoInfo.ts index bfd7b8376..eb95914a1 100644 --- a/src/parser/youtube/VideoInfo.ts +++ b/src/parser/youtube/VideoInfo.ts @@ -20,6 +20,7 @@ import TwoColumnWatchNextResults from '../classes/TwoColumnWatchNextResults.js'; import VideoPrimaryInfo from '../classes/VideoPrimaryInfo.js'; import VideoSecondaryInfo from '../classes/VideoSecondaryInfo.js'; import LiveChatWrap from './LiveChat.js'; +import NavigationEndpoint from '../classes/NavigationEndpoint.js'; import type CardCollection from '../classes/CardCollection.js'; import type Endscreen from '../classes/Endscreen.js'; @@ -60,6 +61,7 @@ class VideoInfo { primary_info?: VideoPrimaryInfo | null; secondary_info?: VideoSecondaryInfo | null; + playlist?; game_info?; merchandise?: MerchandiseShelf | null; related_chip_cloud?: ChipCloud | null; @@ -67,6 +69,7 @@ class VideoInfo { player_overlays?: PlayerOverlay | null; comments_entry_point_header?: CommentsEntryPointHeader | null; livechat?: LiveChat | null; + autoplay?; /** * @param data - API response. @@ -141,6 +144,10 @@ class VideoInfo { this.merchandise = results.firstOfType(MerchandiseShelf); this.related_chip_cloud = secondary_results.firstOfType(RelatedChipCloud)?.content.item().as(ChipCloud); + if (two_col?.playlist) { + this.playlist = two_col.playlist; + } + this.watch_next_feed = secondary_results.firstOfType(ItemSection)?.contents || secondary_results; if (this.watch_next_feed && Array.isArray(this.watch_next_feed) && this.watch_next_feed.at(-1)?.is(ContinuationItem)) @@ -148,6 +155,10 @@ class VideoInfo { this.player_overlays = next?.player_overlays?.item().as(PlayerOverlay); + if (two_col?.autoplay) { + this.autoplay = two_col.autoplay; + } + const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton); if (segmented_like_dislike_button?.like_button?.is(ToggleButton) && segmented_like_dislike_button?.dislike_button?.is(ToggleButton)) { @@ -377,6 +388,13 @@ class VideoInfo { return !!this.#watch_next_continuation; } + /** + * Gets the endpoint of the autoplay video + */ + get autoplay_video_endpoint(): NavigationEndpoint | null { + return this.autoplay?.sets?.[0]?.autoplay_video || null; + } + /** * Get songs used in the video. */