Skip to content
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

feat(VideoInfo): support get by endpoint + more info #342

Merged
merged 2 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
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
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