Skip to content

Commit

Permalink
feat(VideoInfo): support get by endpoint + more info (#342)
Browse files Browse the repository at this point in the history
* feat(VideoInfo): get by endpoint + more info

* chore: fix param description for `getInfo()`
  • Loading branch information
patrickkfkan authored Mar 8, 2023
1 parent 3e3dc35 commit 0d35fe0
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 9 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ const yt = await Innertube.create({
<summary>Methods</summary>
<p>

* [.getInfo(video_id, client?)](#getinfo)
* [.getInfo(target, client?)](#getinfo)
* [.getBasicInfo(video_id, client?)](#getbasicinfo)
* [.search(query, filters?)](#search)
* [.getSearchSuggestions(query)](#getsearchsuggestions)
Expand All @@ -273,15 +273,15 @@ const yt = await Innertube.create({
</details>

<a name="getinfo"></a>
### 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.

**Returns**: `Promise<VideoInfo>`

| 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` |

<details>
Expand Down Expand Up @@ -321,6 +321,9 @@ Retrieves video info, including playback data and even layout elements such as m
- `<info>#addToWatchHistory()`
- Adds the video to the watch history.

- `<info>#autoplay_video_endpoint`
- Returns the endpoint of the video for Autoplay.

- `<info>#page`
- Returns original InnerTube response (sanitized).

Expand Down
44 changes: 38 additions & 6 deletions src/Innertube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<VideoInfo> {
throwIfMissing({ video_id });
async getInfo(target: string | NavigationEndpoint, client?: InnerTubeClient): Promise<VideoInfo> {
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);
Expand Down
64 changes: 64 additions & 0 deletions src/parser/classes/TwoColumnWatchNextResults.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,82 @@
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';

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<Menu>(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;
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/parser/youtube/VideoInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -60,13 +61,15 @@ class VideoInfo {

primary_info?: VideoPrimaryInfo | null;
secondary_info?: VideoSecondaryInfo | null;
playlist?;
game_info?;
merchandise?: MerchandiseShelf | null;
related_chip_cloud?: ChipCloud | null;
watch_next_feed?: ObservedArray<YTNode> | null;
player_overlays?: PlayerOverlay | null;
comments_entry_point_header?: CommentsEntryPointHeader | null;
livechat?: LiveChat | null;
autoplay?;

/**
* @param data - API response.
Expand Down Expand Up @@ -141,13 +144,21 @@ 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))
this.#watch_next_continuation = this.watch_next_feed.pop()?.as(ContinuationItem);

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)) {
Expand Down Expand Up @@ -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.
*/
Expand Down

0 comments on commit 0d35fe0

Please sign in to comment.