mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-16 11:02:10 +00:00
refactor(parser): improve typings and do some refactoring (#305)
* dev: add response types * dev: refactor `Parser#parseResponse()` * dev: update YouTube parsers * dev: update YouTube Music classes * dev: update YouTube Kids classes * dev: update core classes * dev(Parser): fix some inconsistencies * chore: update docs * chore: update docs x2 * fix: export response types * chore(docs): update parser example
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -70,5 +70,8 @@ bundle/*.cjs
|
||||
bundle/*.cjs.*
|
||||
deno/
|
||||
|
||||
# VSCode files
|
||||
.vscode/
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
12
README.md
12
README.md
@@ -451,13 +451,13 @@ Retrieves watch history.
|
||||
### getTrending()
|
||||
Retrieves trending content.
|
||||
|
||||
**Returns**: `Promise.<TabbedFeed>`
|
||||
**Returns**: `Promise.<TabbedFeed<IBrowseResponse>>`
|
||||
|
||||
<a name="getsubscriptionsfeed"></a>
|
||||
### getSubscriptionsFeed()
|
||||
Retrieves subscriptions feed.
|
||||
|
||||
**Returns**: `Promise.<Feed>`
|
||||
**Returns**: `Promise.<Feed<IBrowseResponse>>`
|
||||
|
||||
<a name="getchannel"></a>
|
||||
### getChannel(id)
|
||||
@@ -584,7 +584,7 @@ Resolves a given url.
|
||||
### call(endpoint, args?)
|
||||
Utility to call navigation endpoints.
|
||||
|
||||
**Returns**: `Promise.<ActionsResponse | ParsedResponse>`
|
||||
**Returns**: `Promise.<T extends IParsedResponse | IParsedResponse | ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -635,7 +635,7 @@ import { Innertube, YTNodes } from 'youtubei.js';
|
||||
|
||||
if (button) {
|
||||
// After making sure it exists, we can call its navigation endpoint:
|
||||
const page = await button.endpoint.call(yt.actions);
|
||||
const page = await button.endpoint.call(yt.actions, { parse: true });
|
||||
console.info(page);
|
||||
}
|
||||
})();
|
||||
@@ -667,7 +667,7 @@ console.info('Header:', header);
|
||||
* the parser to add type safety and many utility methods
|
||||
* that make working with InnerTube much easier.
|
||||
*/
|
||||
const tab = page.contents.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
const tab = page.contents?.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
|
||||
|
||||
if (!tab)
|
||||
@@ -676,7 +676,7 @@ if (!tab)
|
||||
if (!tab.content)
|
||||
throw new Error('Target tab appears to be empty');
|
||||
|
||||
const sections = tab.content?.as(YTNodes.SectionList).contents.array().as(YTNodes.MusicCarouselShelf, YTNodes.MusicDescriptionShelf, YTNodes.MusicShelf);
|
||||
const sections = tab.content?.as(YTNodes.SectionList).contents.as(YTNodes.MusicCarouselShelf, YTNodes.MusicDescriptionShelf, YTNodes.MusicShelf);
|
||||
|
||||
console.info('Sections:', sections);
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ Handles direct interactions.
|
||||
|
||||
Likes given video.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -30,7 +30,7 @@ Likes given video.
|
||||
|
||||
Dislikes given video.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -41,7 +41,7 @@ Dislikes given video.
|
||||
|
||||
Remover like/dislike.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -52,7 +52,7 @@ Remover like/dislike.
|
||||
|
||||
Subscribes to given channel.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -63,7 +63,7 @@ Subscribes to given channel.
|
||||
|
||||
Unsubscribes from given channel.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -74,7 +74,7 @@ Unsubscribes from given channel.
|
||||
|
||||
Posts a comment on given video.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -86,7 +86,7 @@ Posts a comment on given video.
|
||||
|
||||
Translates given text using YouTube's comment translation feature.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -100,7 +100,7 @@ Translates given text using YouTube's comment translation feature.
|
||||
Changes notification preferences for a given channel.
|
||||
Only works with channels you are subscribed to.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -16,7 +16,7 @@ Playlist management class.
|
||||
|
||||
Creates a playlist.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -28,7 +28,7 @@ Creates a playlist.
|
||||
|
||||
Deletes given playlist.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -14,7 +14,7 @@ YouTube Studio class (WIP).
|
||||
|
||||
Uploads a custom thumbnail and sets it for a video.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -26,7 +26,7 @@ Uploads a custom thumbnail and sets it for a video.
|
||||
|
||||
Updates given video's metadata.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -38,7 +38,7 @@ Updates given video's metadata.
|
||||
|
||||
Uploads a video to YouTube.
|
||||
|
||||
**Returns:** `Promise.<ActionsResponse>`
|
||||
**Returns:** `Promise.<ApiResponse>`
|
||||
|
||||
| Param | Type | Description |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Innertube, UniversalCache, YTNodes } from 'youtubei.js';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ cache: new UniversalCache() });
|
||||
const yt = await Innertube.create({ cache: new UniversalCache(), generate_session_locally: true });
|
||||
|
||||
const channel = await yt.getChannel('UCX6OQ3DkcsbYNE6H8uQQuVA');
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Innertube, UniversalCache } from 'youtubei.js';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ cache: new UniversalCache() });
|
||||
const yt = await Innertube.create({ cache: new UniversalCache(), generate_session_locally: true });
|
||||
|
||||
const comment_section = await yt.getComments('a-rqu-hjobc');
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFileSync, existsSync, mkdirSync, createWriteStream } from 'fs';
|
||||
import { streamToIterable } from 'youtubei.js/dist/src/utils/Utils';
|
||||
|
||||
(async () => {
|
||||
const yt = await Innertube.create({ cache: new UniversalCache() });
|
||||
const yt = await Innertube.create({ cache: new UniversalCache(), generate_session_locally: true });
|
||||
|
||||
const search = await yt.music.search('No Copyright Background Music', { type: 'album' });
|
||||
|
||||
@@ -19,7 +19,7 @@ import { streamToIterable } from 'youtubei.js/dist/src/utils/Utils';
|
||||
|
||||
for (const song of album.contents) {
|
||||
const stream = await yt.download(song.id as string, {
|
||||
type: 'audio', // audio, video or audio+video
|
||||
type: 'audio', // audio, video or video+audio
|
||||
quality: 'best', // best, bestefficiency, 144p, 240p, 480p, 720p and so on.
|
||||
format: 'mp4' // media container format
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ console.info('Header:', header);
|
||||
// A proxy intercepts access to the actual data, allowing
|
||||
// the parser to add type safety and many utility methods
|
||||
// that make working with InnerTube much easier.
|
||||
const tab = page.contents.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
const tab = page.contents?.item().as(YTNodes.SingleColumnBrowseResults).tabs.firstOfType(YTNodes.Tab);
|
||||
|
||||
if (!tab)
|
||||
throw new Error('Target tab not found');
|
||||
@@ -21,6 +21,6 @@ if (!tab)
|
||||
if (!tab.content)
|
||||
throw new Error('Target tab appears to be empty');
|
||||
|
||||
const sections = tab.content?.as(YTNodes.SectionList).contents.array().as(YTNodes.MusicCarouselShelf, YTNodes.MusicDescriptionShelf, YTNodes.MusicShelf);
|
||||
const sections = tab.content?.as(YTNodes.SectionList).contents.as(YTNodes.MusicCarouselShelf, YTNodes.MusicDescriptionShelf, YTNodes.MusicShelf);
|
||||
|
||||
console.info('Sections:', sections);
|
||||
@@ -31,5 +31,5 @@ const creds = existsSync(creds_path) ? JSON.parse(readFileSync(creds_path).toStr
|
||||
privacy: 'UNLISTED'
|
||||
});
|
||||
|
||||
console.info('Done!');
|
||||
console.info('Done!', upload);
|
||||
})();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
|
||||
import Session, { SessionOptions } from './core/Session.js';
|
||||
import type { ParsedResponse } from './parser/index.js';
|
||||
import type { ActionsResponse } from './core/Actions.js';
|
||||
|
||||
import NavigationEndpoint from './parser/classes/NavigationEndpoint.js';
|
||||
import Channel from './parser/youtube/Channel.js';
|
||||
@@ -30,6 +28,8 @@ import type Format from './parser/classes/misc/Format.js';
|
||||
|
||||
import { generateRandomString, throwIfMissing } from './utils/Utils.js';
|
||||
import type { FormatOptions, DownloadOptions } from './utils/FormatUtils.js';
|
||||
import type { ApiResponse } from './core/Actions.js';
|
||||
import type { IBrowseResponse, IParsedResponse } from './parser/types/ParsedResponse.js';
|
||||
|
||||
export type InnertubeConfig = SessionOptions;
|
||||
|
||||
@@ -116,7 +116,7 @@ class Innertube {
|
||||
|
||||
const response = await this.actions.execute('/search', args);
|
||||
|
||||
return new Search(this.actions, response.data);
|
||||
return new Search(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +166,7 @@ class Innertube {
|
||||
*/
|
||||
async getHomeFeed(): Promise<HomeFeed> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEwhat_to_watch' });
|
||||
return new HomeFeed(this.actions, response.data);
|
||||
return new HomeFeed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +174,7 @@ class Innertube {
|
||||
*/
|
||||
async getLibrary(): Promise<Library> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FElibrary' });
|
||||
return new Library(response.data, this.actions);
|
||||
return new Library(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,23 +183,23 @@ class Innertube {
|
||||
*/
|
||||
async getHistory(): Promise<History> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEhistory' });
|
||||
return new History(this.actions, response.data);
|
||||
return new History(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves trending content.
|
||||
*/
|
||||
async getTrending(): Promise<TabbedFeed> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEtrending' });
|
||||
return new TabbedFeed(this.actions, response.data);
|
||||
async getTrending(): Promise<TabbedFeed<IBrowseResponse>> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEtrending', parse: true });
|
||||
return new TabbedFeed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves subscriptions feed.
|
||||
*/
|
||||
async getSubscriptionsFeed(): Promise<Feed> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEsubscriptions' });
|
||||
return new Feed(this.actions, response.data);
|
||||
async getSubscriptionsFeed(): Promise<Feed<IBrowseResponse>> {
|
||||
const response = await this.actions.execute('/browse', { browseId: 'FEsubscriptions', parse: true });
|
||||
return new Feed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,7 +209,7 @@ class Innertube {
|
||||
async getChannel(id: string): Promise<Channel> {
|
||||
throwIfMissing({ id });
|
||||
const response = await this.actions.execute('/browse', { browseId: id });
|
||||
return new Channel(this.actions, response.data);
|
||||
return new Channel(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,7 +241,8 @@ class Innertube {
|
||||
}
|
||||
|
||||
const response = await this.actions.execute('/browse', { browseId: id });
|
||||
return new Playlist(this.actions, response.data);
|
||||
|
||||
return new Playlist(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,7 +275,7 @@ class Innertube {
|
||||
*/
|
||||
async resolveURL(url: string): Promise<NavigationEndpoint> {
|
||||
const response = await this.actions.execute('/navigation/resolve_url', { url, parse: true });
|
||||
return response.endpoint as NavigationEndpoint;
|
||||
return response.endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -282,9 +283,9 @@ class Innertube {
|
||||
* @param endpoint -The endpoint to call.
|
||||
* @param args - Call arguments.
|
||||
*/
|
||||
call(endpoint: NavigationEndpoint, args: { [key: string]: any; parse: true }): Promise<ParsedResponse>;
|
||||
call(endpoint: NavigationEndpoint, args?: { [key: string]: any; parse?: false }): Promise<ActionsResponse>;
|
||||
call(endpoint: NavigationEndpoint, args?: object): Promise<ActionsResponse | ParsedResponse> {
|
||||
call<T extends IParsedResponse>(endpoint: NavigationEndpoint, args: { [key: string]: any; parse: true }): Promise<T>;
|
||||
call(endpoint: NavigationEndpoint, args?: { [key: string]: any; parse?: false }): Promise<ApiResponse>;
|
||||
call(endpoint: NavigationEndpoint, args?: object): Promise<IParsedResponse | ApiResponse> {
|
||||
return endpoint.call(this.actions, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Proto from '../proto/index.js';
|
||||
import type Actions from './Actions.js';
|
||||
import type { ActionsResponse } from './Actions.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
|
||||
import Analytics from '../parser/youtube/Analytics.js';
|
||||
import TimeWatched from '../parser/youtube/TimeWatched.js';
|
||||
@@ -13,8 +13,8 @@ class AccountManager {
|
||||
#actions: Actions;
|
||||
|
||||
channel: {
|
||||
editName: (new_name: string) => Promise<ActionsResponse>;
|
||||
editDescription: (new_description: string) => Promise<ActionsResponse>;
|
||||
editName: (new_name: string) => Promise<ApiResponse>;
|
||||
editDescription: (new_description: string) => Promise<ApiResponse>;
|
||||
getBasicAnalytics: () => Promise<Analytics>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import Parser, { ParsedResponse } from '../parser/index.js';
|
||||
import Parser, { NavigateAction } from '../parser/index.js';
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
|
||||
import type Session from './Session.js';
|
||||
|
||||
import type {
|
||||
IBrowseResponse, IGetNotificationsMenuResponse,
|
||||
INextResponse, IParsedResponse, IPlayerResponse,
|
||||
IResolveURLResponse, ISearchResponse,
|
||||
IUpdatedMetadataResponse
|
||||
} from '../parser/types/ParsedResponse.js';
|
||||
|
||||
import type { IRawResponse } from '../parser/types/RawResponse.js';
|
||||
|
||||
|
||||
export interface ApiResponse {
|
||||
success: boolean;
|
||||
status_code: number;
|
||||
data: any;
|
||||
data: IRawResponse;
|
||||
}
|
||||
|
||||
export type ActionsResponse = Promise<ApiResponse>;
|
||||
export type InnertubeEndpoint = '/player' | '/search' | '/browse' | '/next' | '/updated_metadata' | '/notification/get_notification_menu' | string;
|
||||
|
||||
export type ParsedResponse<T> =
|
||||
T extends '/player' ? IPlayerResponse :
|
||||
T extends '/search' ? ISearchResponse :
|
||||
T extends '/browse' ? IBrowseResponse :
|
||||
T extends '/next' ? INextResponse :
|
||||
T extends '/updated_metadata' ? IUpdatedMetadataResponse :
|
||||
T extends '/navigation/resolve_url' ? IResolveURLResponse :
|
||||
T extends '/notification/get_notification_menu' ? IGetNotificationsMenuResponse :
|
||||
IParsedResponse;
|
||||
|
||||
class Actions {
|
||||
#session: Session;
|
||||
@@ -40,7 +61,7 @@ class Actions {
|
||||
* @param client - The client to use.
|
||||
* @param playlist_id - The playlist ID.
|
||||
*/
|
||||
async getVideoInfo(id: string, cpn?: string, client?: string, playlist_id?: string): Promise<ActionsResponse> {
|
||||
async getVideoInfo(id: string, cpn?: string, client?: string, playlist_id?: string): Promise<ApiResponse> {
|
||||
const data: Record<string, any> = {
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
@@ -109,12 +130,12 @@ class Actions {
|
||||
|
||||
/**
|
||||
* Executes an API call.
|
||||
* @param action - The endpoint to call.
|
||||
* @param endpoint - The endpoint to call.
|
||||
* @param args - Call arguments
|
||||
*/
|
||||
async execute(action: string, args: { [key: string]: any; parse: true; protobuf?: false; serialized_data?: any }) : Promise<ParsedResponse>;
|
||||
async execute(action: string, args?: { [key: string]: any; parse?: false; protobuf?: true; serialized_data?: any }) : Promise<ActionsResponse>;
|
||||
async execute(action: string, args?: { [key: string]: any; parse?: boolean; protobuf?: boolean; serialized_data?: any }): Promise<ParsedResponse | ActionsResponse> {
|
||||
async execute<T extends InnertubeEndpoint>(endpoint: T, args: { [key: string]: any; parse: true; protobuf?: false; serialized_data?: any }): Promise<ParsedResponse<T>>;
|
||||
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: { [key: string]: any; parse?: false; protobuf?: true; serialized_data?: any }): Promise<ApiResponse>;
|
||||
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: { [key: string]: any; parse?: boolean; protobuf?: boolean; serialized_data?: any }): Promise<ParsedResponse<T> | ApiResponse> {
|
||||
let data;
|
||||
|
||||
if (args && !args.protobuf) {
|
||||
@@ -162,9 +183,9 @@ class Actions {
|
||||
data = args.serialized_data;
|
||||
}
|
||||
|
||||
const endpoint = Reflect.has(args || {}, 'override_endpoint') ? args?.override_endpoint : action;
|
||||
const target_endpoint = Reflect.has(args || {}, 'override_endpoint') ? args?.override_endpoint : endpoint;
|
||||
|
||||
const response = await this.#session.http.fetch(endpoint, {
|
||||
const response = await this.#session.http.fetch(target_endpoint, {
|
||||
method: 'POST',
|
||||
body: args?.protobuf ? data : JSON.stringify((data || {})),
|
||||
headers: {
|
||||
@@ -175,12 +196,26 @@ class Actions {
|
||||
});
|
||||
|
||||
if (args?.parse) {
|
||||
return Parser.parseResponse(await response.json());
|
||||
let parsed_response = Parser.parseResponse<ParsedResponse<T>>(await response.json());
|
||||
|
||||
// Handle redirects
|
||||
if (this.#isBrowse(parsed_response) && parsed_response.on_response_received_actions?.first()?.type === 'navigateAction') {
|
||||
const navigate_action = parsed_response.on_response_received_actions.firstOfType(NavigateAction);
|
||||
if (navigate_action) {
|
||||
parsed_response = await navigate_action.endpoint.call(this, { parse: true });
|
||||
}
|
||||
}
|
||||
|
||||
return parsed_response;
|
||||
}
|
||||
|
||||
return this.#wrap(response);
|
||||
}
|
||||
|
||||
#isBrowse(response: IParsedResponse): response is IBrowseResponse {
|
||||
return 'on_response_received_actions' in response;
|
||||
}
|
||||
|
||||
#needsLogin(id: string) {
|
||||
return [
|
||||
'FElibrary',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Memo, ObservedArray, SuperParsedResult, YTNode } from '../parser/helpers.js';
|
||||
import Parser, { ParsedResponse, ReloadContinuationItemsCommand } from '../parser/index.js';
|
||||
import Parser, { ReloadContinuationItemsCommand } from '../parser/index.js';
|
||||
import { concatMemos, InnertubeError } from '../utils/Utils.js';
|
||||
import type Actions from './Actions.js';
|
||||
|
||||
@@ -30,20 +30,23 @@ import type MusicQueue from '../parser/classes/MusicQueue.js';
|
||||
import type RichGrid from '../parser/classes/RichGrid.js';
|
||||
import type SectionList from '../parser/classes/SectionList.js';
|
||||
|
||||
class Feed {
|
||||
#page: ParsedResponse;
|
||||
import type { IParsedResponse } from '../parser/types/ParsedResponse.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
|
||||
class Feed<T extends IParsedResponse = IParsedResponse> {
|
||||
#page: T;
|
||||
#continuation?: ObservedArray<ContinuationItem>;
|
||||
#actions: Actions;
|
||||
#memo: Memo;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
if (data.on_response_received_actions || data.on_response_received_endpoints || already_parsed) {
|
||||
this.#page = data;
|
||||
constructor(actions: Actions, response: ApiResponse | IParsedResponse, already_parsed = false) {
|
||||
if (this.#isParsed(response) || already_parsed) {
|
||||
this.#page = response as T;
|
||||
} else {
|
||||
this.#page = Parser.parseResponse(data);
|
||||
this.#page = Parser.parseResponse<T>(response.data);
|
||||
}
|
||||
|
||||
const memo = concatMemos(
|
||||
const memo = concatMemos(...[
|
||||
this.#page.contents_memo,
|
||||
this.#page.continuation_contents_memo,
|
||||
this.#page.on_response_received_commands_memo,
|
||||
@@ -51,7 +54,7 @@ class Feed {
|
||||
this.#page.on_response_received_actions_memo,
|
||||
this.#page.sidebar_memo,
|
||||
this.#page.header_memo
|
||||
);
|
||||
]);
|
||||
|
||||
if (!memo)
|
||||
throw new InnertubeError('No memo found in feed');
|
||||
@@ -60,6 +63,10 @@ class Feed {
|
||||
this.#actions = actions;
|
||||
}
|
||||
|
||||
#isParsed(response: IParsedResponse | ApiResponse): response is IParsedResponse {
|
||||
return !('data' in response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all videos on a given page via memo
|
||||
*/
|
||||
@@ -143,10 +150,10 @@ class Feed {
|
||||
* Returns secondary contents from the page.
|
||||
*/
|
||||
get secondary_contents(): SuperParsedResult<YTNode> | undefined {
|
||||
if (!this.#page.contents.is_node)
|
||||
if (!this.#page.contents?.is_node)
|
||||
return undefined;
|
||||
|
||||
const node = this.#page.contents.item();
|
||||
const node = this.#page.contents?.item();
|
||||
|
||||
if (!node.is(TwoColumnBrowseResults, TwoColumnSearchResults))
|
||||
return undefined;
|
||||
@@ -161,7 +168,7 @@ class Feed {
|
||||
/**
|
||||
* Get the original page data
|
||||
*/
|
||||
get page(): ParsedResponse {
|
||||
get page(): T {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
@@ -175,14 +182,14 @@ class Feed {
|
||||
/**
|
||||
* Retrieves continuation data as it is.
|
||||
*/
|
||||
async getContinuationData(): Promise<ParsedResponse | undefined> {
|
||||
async getContinuationData(): Promise<T | undefined> {
|
||||
if (this.#continuation) {
|
||||
if (this.#continuation.length > 1)
|
||||
throw new InnertubeError('There are too many continuations, you\'ll need to find the correct one yourself in this.page');
|
||||
if (this.#continuation.length === 0)
|
||||
throw new InnertubeError('There are no continuations');
|
||||
|
||||
const response = await this.#continuation[0].endpoint.call(this.#actions, { parse: true });
|
||||
const response = await this.#continuation[0].endpoint.call<T>(this.#actions, { parse: true });
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -196,9 +203,11 @@ class Feed {
|
||||
/**
|
||||
* Retrieves next batch of contents and returns a new {@link Feed} object.
|
||||
*/
|
||||
async getContinuation(): Promise<Feed> {
|
||||
async getContinuation(): Promise<Feed<T>> {
|
||||
const continuation_data = await this.getContinuationData();
|
||||
return new Feed(this.actions, continuation_data, true);
|
||||
if (!continuation_data)
|
||||
throw new InnertubeError('Could not get continuation data');
|
||||
return new Feed<T>(this.actions, continuation_data, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import ChipCloudChip from '../parser/classes/ChipCloudChip.js';
|
||||
import FeedFilterChipBar from '../parser/classes/FeedFilterChipBar.js';
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
import Feed from './Feed.js';
|
||||
|
||||
import type { ObservedArray } from '../parser/helpers.js';
|
||||
import { InnertubeError } from '../utils/Utils.js';
|
||||
import type { IParsedResponse } from '../parser/types/ParsedResponse.js';
|
||||
import type Actions from './Actions.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
|
||||
class FilterableFeed extends Feed {
|
||||
class FilterableFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#chips?: ObservedArray<ChipCloudChip>;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | T, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@ class FilterableFeed extends Feed {
|
||||
/**
|
||||
* Applies given filter and returns a new {@link Feed} object.
|
||||
*/
|
||||
async getFilteredFeed(filter: string | ChipCloudChip): Promise<Feed> {
|
||||
async getFilteredFeed(filter: string | ChipCloudChip): Promise<Feed<T>> {
|
||||
let target_filter: ChipCloudChip | undefined;
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
@@ -62,6 +64,9 @@ class FilterableFeed extends Feed {
|
||||
|
||||
const response = await target_filter.endpoint?.call(this.actions, { parse: true });
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Failed to get filtered feed');
|
||||
|
||||
return new Feed(this.actions, response, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class Kids {
|
||||
*/
|
||||
async search(query: string): Promise<Search> {
|
||||
const response = await this.#session.actions.execute('/search', { query, client: 'YTKIDS' });
|
||||
return new Search(this.#session.actions, response.data);
|
||||
return new Search(this.#session.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,7 +53,7 @@ class Kids {
|
||||
*/
|
||||
async getChannel(channel_id: string): Promise<Channel> {
|
||||
const response = await this.#session.actions.execute('/browse', { browseId: channel_id, client: 'YTKIDS' });
|
||||
return new Channel(this.#session.actions, response.data);
|
||||
return new Channel(this.#session.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +61,7 @@ class Kids {
|
||||
*/
|
||||
async getHomeFeed(): Promise<HomeFeed> {
|
||||
const response = await this.#session.actions.execute('/browse', { browseId: 'FEkids_home', client: 'YTKIDS' });
|
||||
return new HomeFeed(this.#session.actions, response.data);
|
||||
return new HomeFeed(this.#session.actions, response);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ class Music {
|
||||
|
||||
const response = await this.#actions.execute('/search', payload);
|
||||
|
||||
return new Search(response, this.#actions, { is_filtered: Reflect.has(filters, 'type') && filters.type !== 'all' });
|
||||
return new Search(response, this.#actions, Reflect.has(filters, 'type') && filters.type !== 'all');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +198,7 @@ class Music {
|
||||
browseId: album_id
|
||||
});
|
||||
|
||||
return new Album(response, this.#actions);
|
||||
return new Album(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,9 +234,9 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs?.[0];
|
||||
const tab = tabs?.first();
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
@@ -260,10 +260,10 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
if (!page)
|
||||
if (!page || !page.contents_memo)
|
||||
throw new InnertubeError('Could not fetch automix');
|
||||
|
||||
return page.contents_memo.getType(PlaylistPanel)?.[0];
|
||||
return page.contents_memo.getType(PlaylistPanel).first();
|
||||
}
|
||||
|
||||
return playlist_panel;
|
||||
@@ -282,7 +282,7 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_RELATED');
|
||||
|
||||
@@ -291,6 +291,9 @@ class Music {
|
||||
|
||||
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
if (!page.contents)
|
||||
throw new InnertubeError('Unexpected response', page);
|
||||
|
||||
const shelves = page.contents.item().as(SectionList).contents.as(MusicCarouselShelf, MusicDescriptionShelf);
|
||||
|
||||
return shelves;
|
||||
@@ -309,7 +312,7 @@ class Music {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const tabs = data.contents_memo.getType(Tab);
|
||||
const tabs = data.contents_memo?.getType(Tab);
|
||||
|
||||
const tab = tabs?.matchCondition((tab) => tab.endpoint.payload.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType === 'MUSIC_PAGE_TYPE_TRACK_LYRICS');
|
||||
|
||||
@@ -318,10 +321,14 @@ class Music {
|
||||
|
||||
const page = await tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
if (!page.contents)
|
||||
throw new InnertubeError('Unexpected response', page);
|
||||
|
||||
if (page.contents.item().key('type').string() === 'Message')
|
||||
throw new InnertubeError(page.contents.item().as(Message).text, video_id);
|
||||
|
||||
const section_list = page.contents.item().as(SectionList).contents;
|
||||
|
||||
return section_list.firstOfType(MusicDescriptionShelf);
|
||||
}
|
||||
|
||||
@@ -348,7 +355,7 @@ class Music {
|
||||
client: 'YTMUSIC'
|
||||
});
|
||||
|
||||
const search_suggestions_section = response.contents_memo.getType(SearchSuggestionsSection)?.[0];
|
||||
const search_suggestions_section = response.contents_memo?.getType(SearchSuggestionsSection)?.[0];
|
||||
|
||||
if (!search_suggestions_section?.contents.is_array)
|
||||
return observe([] as YTNode[]);
|
||||
|
||||
@@ -72,8 +72,6 @@ export default class Player {
|
||||
const args = new URLSearchParams(url);
|
||||
const url_components = new URL(args.get('url') || url);
|
||||
|
||||
url_components.searchParams.set('alr', 'yes');
|
||||
|
||||
if (signature_cipher || cipher) {
|
||||
const signature = Platform.shim.eval(this.#sig_sc, {
|
||||
sig: args.get('s')
|
||||
|
||||
@@ -16,7 +16,7 @@ class PlaylistManager {
|
||||
* @param title - The title of the playlist.
|
||||
* @param video_ids - An array of video IDs to add to the playlist.
|
||||
*/
|
||||
async create(title: string, video_ids: string[]): Promise<{ success: boolean; status_code: number; playlist_id: string; data: any }> {
|
||||
async create(title: string, video_ids: string[]): Promise<{ success: boolean; status_code: number; playlist_id?: string; data: any }> {
|
||||
throwIfMissing({ title, video_ids });
|
||||
|
||||
if (!this.#actions.session.logged_in)
|
||||
|
||||
@@ -4,23 +4,25 @@ import { InnertubeError } from '../utils/Utils.js';
|
||||
|
||||
import type Actions from './Actions.js';
|
||||
import type { ObservedArray } from '../parser/helpers.js';
|
||||
import type { IParsedResponse } from '../parser/types/ParsedResponse.js';
|
||||
import type { ApiResponse } from './Actions.js';
|
||||
|
||||
class TabbedFeed extends Feed {
|
||||
#tabs: ObservedArray<Tab>;
|
||||
class TabbedFeed<T extends IParsedResponse> extends Feed<T> {
|
||||
#tabs?: ObservedArray<Tab>;
|
||||
#actions: Actions;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IParsedResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.#actions = actions;
|
||||
this.#tabs = this.page.contents_memo.getType(Tab);
|
||||
this.#tabs = this.page.contents_memo?.getType(Tab);
|
||||
}
|
||||
|
||||
get tabs(): string[] {
|
||||
return this.#tabs.map((tab) => tab.title.toString());
|
||||
return this.#tabs?.map((tab) => tab.title.toString()) ?? [];
|
||||
}
|
||||
|
||||
async getTabByName(title: string): Promise<TabbedFeed> {
|
||||
const tab = this.#tabs.find((tab) => tab.title.toLowerCase() === title.toLowerCase());
|
||||
async getTabByName(title: string): Promise<TabbedFeed<T>> {
|
||||
const tab = this.#tabs?.find((tab) => tab.title.toLowerCase() === title.toLowerCase());
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError(`Tab "${title}" not found`);
|
||||
@@ -30,11 +32,11 @@ class TabbedFeed extends Feed {
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
return new TabbedFeed(this.#actions, response.data, false);
|
||||
return new TabbedFeed<T>(this.#actions, response, false);
|
||||
}
|
||||
|
||||
async getTabByURL(url: string): Promise<TabbedFeed> {
|
||||
const tab = this.#tabs.find((tab) => tab.endpoint.metadata.url?.split('/').pop() === url);
|
||||
async getTabByURL(url: string): Promise<TabbedFeed<T>> {
|
||||
const tab = this.#tabs?.find((tab) => tab.endpoint.metadata.url?.split('/').pop() === url);
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError(`Tab "${url}" not found`);
|
||||
@@ -44,15 +46,15 @@ class TabbedFeed extends Feed {
|
||||
|
||||
const response = await tab.endpoint.call(this.#actions);
|
||||
|
||||
return new TabbedFeed(this.#actions, response.data, false);
|
||||
return new TabbedFeed<T>(this.#actions, response, false);
|
||||
}
|
||||
|
||||
hasTabWithURL(url: string): boolean {
|
||||
return this.#tabs.some((tab) => tab.endpoint.metadata.url?.split('/').pop() === url);
|
||||
return this.#tabs?.some((tab) => tab.endpoint.metadata.url?.split('/').pop() === url) ?? false;
|
||||
}
|
||||
|
||||
get title(): string | undefined {
|
||||
return this.page.contents_memo.getType(Tab)?.find((tab) => tab.selected)?.title.toString();
|
||||
return this.page.contents_memo?.getType(Tab)?.find((tab) => tab.selected)?.title.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
# Parser
|
||||
|
||||
Sanitizes and standardizes InnerTube responses while maintaining the integrity of the data. Also [drastically improves](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube/Library.ts#L69) how API calls are made and handled.
|
||||
Sanitizes and standardizes InnerTube responses while maintaining the integrity of the data.
|
||||
|
||||
Structure:
|
||||
|
||||
* [`/classes`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/classes) - InnerTube nodes.
|
||||
* [`/types`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/types) - General response types.
|
||||
* [`/youtube`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/youtube) - Contains the logic for parsing YouTube responses.
|
||||
* [`/ytmusic`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytmusic) - Contains the logic for parsing YouTube Music responses.
|
||||
* [`/ytkids`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/ytkids) - Contains the logic for parsing YouTube Kids responses.
|
||||
* [`helpers.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/helpers.ts) - Helper functions/classes for the parser.
|
||||
* [`parser.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/index.ts) - The core of the parser.
|
||||
* [`map.ts`](https://github.com/LuanRT/YouTube.js/blob/main/src/parser/map.ts) - A list of all InnerTube nodes, it is used to determine which node to use for a given renderer. Note that this file is auto-generated and should not be edited manually.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
|
||||
@@ -13,16 +13,16 @@ class C4TabbedHeader extends YTNode {
|
||||
static type = 'C4TabbedHeader';
|
||||
|
||||
author: Author;
|
||||
banner: Thumbnail[];
|
||||
tv_banner: Thumbnail[];
|
||||
mobile_banner: Thumbnail[];
|
||||
subscribers: Text;
|
||||
videos_count: Text;
|
||||
sponsor_button: Button | null;
|
||||
subscribe_button: SubscribeButton | null;
|
||||
header_links: ChannelHeaderLinks | null;
|
||||
channel_handle: Text;
|
||||
channel_id: string;
|
||||
banner?: Thumbnail[];
|
||||
tv_banner?: Thumbnail[];
|
||||
mobile_banner?: Thumbnail[];
|
||||
subscribers?: Text;
|
||||
videos_count?: Text;
|
||||
sponsor_button?: Button | null;
|
||||
subscribe_button?: SubscribeButton | null;
|
||||
header_links?: ChannelHeaderLinks | null;
|
||||
channel_handle?: Text;
|
||||
channel_id?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
@@ -31,16 +31,45 @@ class C4TabbedHeader extends YTNode {
|
||||
navigationEndpoint: data.navigationEndpoint
|
||||
}, data.badges, data.avatar);
|
||||
|
||||
this.banner = Thumbnail.fromResponse(data.banner);
|
||||
this.tv_banner = Thumbnail.fromResponse(data.tvBanner);
|
||||
this.mobile_banner = Thumbnail.fromResponse(data.mobileBanner);
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
this.videos_count = new Text(data.videosCountText);
|
||||
this.sponsor_button = Parser.parseItem<Button>(data.sponsorButton);
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
|
||||
this.header_links = Parser.parseItem<ChannelHeaderLinks>(data.headerLinks);
|
||||
this.channel_handle = new Text(data.channelHandleText);
|
||||
this.channel_id = data.channelId;
|
||||
if (data.banner) {
|
||||
this.banner = Thumbnail.fromResponse(data.banner);
|
||||
}
|
||||
|
||||
if (data.tv_banner) {
|
||||
this.tv_banner = Thumbnail.fromResponse(data.tvBanner);
|
||||
}
|
||||
|
||||
if (data.mobile_banner) {
|
||||
this.mobile_banner = Thumbnail.fromResponse(data.mobileBanner);
|
||||
}
|
||||
|
||||
if (data.subscriberCountText) {
|
||||
this.subscribers = new Text(data.subscriberCountText);
|
||||
}
|
||||
|
||||
if (data.videosCountText) {
|
||||
this.videos_count = new Text(data.videosCountText);
|
||||
}
|
||||
|
||||
if (data.sponsorButton) {
|
||||
this.sponsor_button = Parser.parseItem<Button>(data.sponsorButton);
|
||||
}
|
||||
|
||||
if (data.subscribeButton) {
|
||||
this.subscribe_button = Parser.parseItem<SubscribeButton>(data.subscribeButton);
|
||||
}
|
||||
|
||||
if (data.headerLinks) {
|
||||
this.header_links = Parser.parseItem<ChannelHeaderLinks>(data.headerLinks);
|
||||
}
|
||||
|
||||
if (data.channelHandleText) {
|
||||
this.channel_handle = new Text(data.channelHandleText);
|
||||
}
|
||||
|
||||
if (data.channelId) {
|
||||
this.channel_id = data.channelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,23 +11,35 @@ class MusicShelf extends YTNode {
|
||||
|
||||
title: Text;
|
||||
contents;
|
||||
endpoint: NavigationEndpoint | null;
|
||||
continuation: string | null;
|
||||
bottom_text: Text | null;
|
||||
endpoint?: NavigationEndpoint;
|
||||
continuation?: string;
|
||||
bottom_text?: Text;
|
||||
bottom_button?: Button | null;
|
||||
subheaders?: Array<any>;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
this.title = new Text(data.title);
|
||||
this.contents = Parser.parseArray<MusicResponsiveListItem>(data.contents, MusicResponsiveListItem);
|
||||
this.endpoint = Reflect.has(data, 'bottomEndpoint') ? new NavigationEndpoint(data.bottomEndpoint) : null;
|
||||
this.continuation =
|
||||
data.continuations?.[0].nextContinuationData?.continuation ||
|
||||
data.continuations?.[0].reloadContinuationData?.continuation || null;
|
||||
this.bottom_text = Reflect.has(data, 'bottomText') ? new Text(data.bottomText) : null;
|
||||
this.bottom_button = Parser.parseItem(data.bottomButton, Button);
|
||||
|
||||
if (data.bottomEndpoint) {
|
||||
this.endpoint = new NavigationEndpoint(data.bottomEndpoint);
|
||||
}
|
||||
|
||||
if (data.continuations) {
|
||||
this.continuation =
|
||||
data.continuations?.[0].nextContinuationData?.continuation ||
|
||||
data.continuations?.[0].reloadContinuationData?.continuation;
|
||||
}
|
||||
|
||||
if (data.bottomText) {
|
||||
this.bottom_text = new Text(data.bottomText);
|
||||
}
|
||||
|
||||
if (data.bottomButton) {
|
||||
this.bottom_button = Parser.parseItem<Button>(data.bottomButton);
|
||||
}
|
||||
|
||||
if (data.subheaders) {
|
||||
this.subheaders = Parser.parseArray(data.subheaders);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Actions, { ActionsResponse } from '../../core/Actions.js';
|
||||
import Parser from '../index.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IParsedResponse } from '../types/ParsedResponse.js';
|
||||
import { YTNode } from '../helpers.js';
|
||||
|
||||
import CreatePlaylistDialog from './CreatePlaylistDialog.js';
|
||||
|
||||
class NavigationEndpoint extends YTNode {
|
||||
@@ -85,9 +86,9 @@ class NavigationEndpoint extends YTNode {
|
||||
}
|
||||
}
|
||||
|
||||
call(actions: Actions, args: { [ key: string ]: any; parse: true }): Promise<ParsedResponse>;
|
||||
call(actions: Actions, args?: { [ key: string ]: any; parse?: false }): Promise<ActionsResponse>;
|
||||
call(actions: Actions, args?: { [ key: string ]: any; parse?: boolean }): Promise<ParsedResponse | ActionsResponse> {
|
||||
call<T extends IParsedResponse>(actions: Actions, args: { [ key: string ]: any; parse: true }): Promise<T>;
|
||||
call(actions: Actions, args?: { [ key: string ]: any; parse?: false }): Promise<ApiResponse>;
|
||||
call(actions: Actions, args?: { [ key: string ]: any; parse?: boolean }): Promise<IParsedResponse | ApiResponse> {
|
||||
if (!actions)
|
||||
throw new Error('An active caller must be provided');
|
||||
if (!this.metadata.api_url)
|
||||
|
||||
@@ -7,8 +7,8 @@ class SectionList extends YTNode {
|
||||
target_id?: string;
|
||||
contents;
|
||||
continuation?: string;
|
||||
header;
|
||||
sub_menu;
|
||||
header?;
|
||||
sub_menu?;
|
||||
|
||||
constructor(data: any) {
|
||||
super();
|
||||
|
||||
@@ -45,7 +45,10 @@ class CommentThread extends YTNode {
|
||||
|
||||
const response = await continuation.endpoint.call(this.#actions, { parse: true });
|
||||
|
||||
this.replies = observe(response.on_response_received_endpoints_memo?.getType(Comment).map((comment) => {
|
||||
if (!response.on_response_received_endpoints_memo)
|
||||
throw new InnertubeError('Unexpected response.', response);
|
||||
|
||||
this.replies = observe(response.on_response_received_endpoints_memo.getType(Comment).map((comment) => {
|
||||
comment.setActions(this.#actions);
|
||||
return comment;
|
||||
}));
|
||||
@@ -75,12 +78,15 @@ class CommentThread extends YTNode {
|
||||
|
||||
const response = await load_more_button.endpoint.call(this.#actions, { parse: true });
|
||||
|
||||
this.replies = observe(response?.on_response_received_endpoints_memo.getType(Comment).map((comment) => {
|
||||
if (!response.on_response_received_endpoints_memo)
|
||||
throw new InnertubeError('Unexpected response.', response);
|
||||
|
||||
this.replies = observe(response.on_response_received_endpoints_memo.getType(Comment).map((comment) => {
|
||||
comment.setActions(this.#actions);
|
||||
return comment;
|
||||
}));
|
||||
|
||||
this.#continuation = response?.on_response_received_endpoints_memo.getType(ContinuationItem)?.[0];
|
||||
this.#continuation = response.on_response_received_endpoints_memo.getType(ContinuationItem)?.[0];
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ export type ObservedArray<T extends YTNode = YTNode> = Array<T> & {
|
||||
/**
|
||||
* Get the first item
|
||||
*/
|
||||
first: () => T | undefined;
|
||||
first: () => T;
|
||||
/**
|
||||
* This is similar to filter but throws if there's a type mismatch.
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
export { default as Parser } from './parser.js';
|
||||
export * from './parser.js';
|
||||
export * from './types/index.js';
|
||||
export { YTNodes, Misc } from '../parser/map.js';
|
||||
export * as YT from './youtube/index.js';
|
||||
export * as YTMusic from './ytmusic/index.js';
|
||||
export * as YTKids from './ytkids/index.js';
|
||||
|
||||
import Parser from './parser.js';
|
||||
export default Parser;
|
||||
export default Parser;
|
||||
@@ -11,6 +11,9 @@ import type LiveChatHeader from './classes/LiveChatHeader.js';
|
||||
import type LiveChatItemList from './classes/LiveChatItemList.js';
|
||||
import type Alert from './classes/Alert.js';
|
||||
|
||||
import type { IParsedResponse } from './types/ParsedResponse.js';
|
||||
import type { IRawResponse, RawData, RawNode } from './types/RawResponse.js';
|
||||
|
||||
import MusicMultiSelectMenuItem from './classes/menus/MusicMultiSelectMenuItem.js';
|
||||
import Format from './classes/misc/Format.js';
|
||||
import VideoDetails from './classes/misc/VideoDetails.js';
|
||||
@@ -58,115 +61,209 @@ export default class Parser {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses InnerTube response.
|
||||
* @param data - The response data.
|
||||
* Parses given InnerTube response.
|
||||
* @param data - Raw data.
|
||||
*/
|
||||
static parseResponse(data: any) {
|
||||
// Memoize the response objects by classname
|
||||
static parseResponse<T extends IParsedResponse = IParsedResponse>(data: IRawResponse): T {
|
||||
const parsed_data = {} as T;
|
||||
|
||||
this.#createMemo();
|
||||
// TODO: should this parseItem?
|
||||
const contents = Parser.parse(data.contents);
|
||||
const contents = this.parse(data.contents);
|
||||
const contents_memo = this.#getMemo();
|
||||
if (contents) {
|
||||
parsed_data.contents = contents;
|
||||
parsed_data.contents_memo = contents_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const on_response_received_actions = data.onResponseReceivedActions ? Parser.parseRR(data.onResponseReceivedActions) : null;
|
||||
const on_response_received_actions = data.onResponseReceivedActions ? this.parseRR(data.onResponseReceivedActions) : null;
|
||||
const on_response_received_actions_memo = this.#getMemo();
|
||||
if (on_response_received_actions) {
|
||||
parsed_data.on_response_received_actions = on_response_received_actions;
|
||||
parsed_data.on_response_received_actions_memo = on_response_received_actions_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const on_response_received_endpoints = data.onResponseReceivedEndpoints ? Parser.parseRR(data.onResponseReceivedEndpoints) : null;
|
||||
const on_response_received_endpoints = data.onResponseReceivedEndpoints ? this.parseRR(data.onResponseReceivedEndpoints) : null;
|
||||
const on_response_received_endpoints_memo = this.#getMemo();
|
||||
if (on_response_received_endpoints) {
|
||||
parsed_data.on_response_received_endpoints = on_response_received_endpoints;
|
||||
parsed_data.on_response_received_endpoints_memo = on_response_received_endpoints_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const on_response_received_commands = data.onResponseReceivedCommands ? Parser.parseRR(data.onResponseReceivedCommands) : null;
|
||||
const on_response_received_commands = data.onResponseReceivedCommands ? this.parseRR(data.onResponseReceivedCommands) : null;
|
||||
const on_response_received_commands_memo = this.#getMemo();
|
||||
if (on_response_received_commands) {
|
||||
parsed_data.on_response_received_commands = on_response_received_commands;
|
||||
parsed_data.on_response_received_commands_memo = on_response_received_commands_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const continuation_contents = data.continuationContents ? Parser.parseLC(data.continuationContents) : null;
|
||||
const continuation_contents = data.continuationContents ? this.parseLC(data.continuationContents) : null;
|
||||
const continuation_contents_memo = this.#getMemo();
|
||||
if (continuation_contents) {
|
||||
parsed_data.continuation_contents = continuation_contents;
|
||||
parsed_data.continuation_contents_memo = continuation_contents_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const actions = data.actions ? Parser.parseActions(data.actions) : null;
|
||||
const actions = data.actions ? this.parseActions(data.actions) : null;
|
||||
const actions_memo = this.#getMemo();
|
||||
if (actions) {
|
||||
parsed_data.actions = actions;
|
||||
parsed_data.actions_memo = actions_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const live_chat_item_context_menu_supported_renderers = data.liveChatItemContextMenuSupportedRenderers
|
||||
? Parser.parseItem(data.liveChatItemContextMenuSupportedRenderers)
|
||||
: null;
|
||||
const live_chat_item_context_menu_supported_renderers = data.liveChatItemContextMenuSupportedRenderers ? this.parseItem(data.liveChatItemContextMenuSupportedRenderers) : null;
|
||||
const live_chat_item_context_menu_supported_renderers_memo = this.#getMemo();
|
||||
if (live_chat_item_context_menu_supported_renderers) {
|
||||
parsed_data.live_chat_item_context_menu_supported_renderers = live_chat_item_context_menu_supported_renderers;
|
||||
parsed_data.live_chat_item_context_menu_supported_renderers_memo = live_chat_item_context_menu_supported_renderers_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const header = data.header ? Parser.parse(data.header) : null;
|
||||
const header = data.header ? this.parse(data.header) : null;
|
||||
const header_memo = this.#getMemo();
|
||||
if (header) {
|
||||
parsed_data.header = header;
|
||||
parsed_data.header_memo = header_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.#createMemo();
|
||||
const sidebar = data.sidebar ? Parser.parseItem(data.sidebar) : null;
|
||||
const sidebar = data.sidebar ? this.parseItem(data.sidebar) : null;
|
||||
const sidebar_memo = this.#getMemo();
|
||||
if (sidebar) {
|
||||
parsed_data.sidebar = sidebar;
|
||||
parsed_data.sidebar_memo = sidebar_memo;
|
||||
}
|
||||
this.#clearMemo();
|
||||
|
||||
this.applyMutations(contents_memo, data.frameworkUpdates?.entityBatchUpdate?.mutations);
|
||||
|
||||
return {
|
||||
actions,
|
||||
actions_memo,
|
||||
contents,
|
||||
contents_memo,
|
||||
header,
|
||||
header_memo,
|
||||
sidebar,
|
||||
sidebar_memo,
|
||||
live_chat_item_context_menu_supported_renderers,
|
||||
live_chat_item_context_menu_supported_renderers_memo,
|
||||
on_response_received_actions,
|
||||
on_response_received_actions_memo,
|
||||
on_response_received_endpoints,
|
||||
on_response_received_endpoints_memo,
|
||||
on_response_received_commands,
|
||||
on_response_received_commands_memo,
|
||||
continuation: data.continuation ? Parser.parseC(data.continuation) : null,
|
||||
continuation_contents,
|
||||
continuation_contents_memo,
|
||||
metadata: Parser.parse(data.metadata),
|
||||
microformat: data.microformat ? Parser.parseItem(data.microformat) : null,
|
||||
overlay: Parser.parseItem(data.overlay),
|
||||
alerts: Parser.parseArray<Alert>(data.alerts),
|
||||
refinements: data.refinements || null,
|
||||
estimated_results: data.estimatedResults ? parseInt(data.estimatedResults) : null,
|
||||
player_overlays: Parser.parse(data.playerOverlays),
|
||||
playback_tracking: data.playbackTracking ? {
|
||||
videostats_watchtime_url: data.playbackTracking.videostatsWatchtimeUrl.baseUrl as string,
|
||||
videostats_playback_url: data.playbackTracking.videostatsPlaybackUrl.baseUrl as string
|
||||
} : null,
|
||||
playability_status: data.playabilityStatus ? {
|
||||
status: data.playabilityStatus.status as string,
|
||||
error_screen: Parser.parseItem(data.playabilityStatus.errorScreen),
|
||||
audio_only_playablility: Parser.parseItem<AudioOnlyPlayability>(data.playabilityStatus.audioOnlyPlayability),
|
||||
embeddable: !!data.playabilityStatus.playableInEmbed || false,
|
||||
reason: data.playabilityStatus?.reason || ''
|
||||
} : undefined,
|
||||
streaming_data: data.streamingData ? {
|
||||
expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000),
|
||||
formats: Parser.parseFormats(data.streamingData.formats),
|
||||
adaptive_formats: Parser.parseFormats(data.streamingData.adaptiveFormats),
|
||||
dash_manifest_url: data.streamingData?.dashManifestUrl as string || null,
|
||||
hls_manifest_url: data.streamingData?.hlsManifestUrl as string || null
|
||||
} : undefined,
|
||||
current_video_endpoint: data.currentVideoEndpoint ? new NavigationEndpoint(data.currentVideoEndpoint) : null,
|
||||
endpoint: data.endpoint ? new NavigationEndpoint(data.endpoint) : null,
|
||||
captions: Parser.parseItem<PlayerCaptionsTracklist>(data.captions),
|
||||
video_details: data.videoDetails ? new VideoDetails(data.videoDetails) : undefined,
|
||||
annotations: Parser.parseArray<PlayerAnnotationsExpanded>(data.annotations),
|
||||
storyboards: Parser.parseItem<PlayerStoryboardSpec | PlayerLiveStoryboardSpec>(data.storyboards),
|
||||
endscreen: Parser.parseItem<Endscreen>(data.endscreen),
|
||||
cards: Parser.parseItem<CardCollection>(data.cards)
|
||||
};
|
||||
const continuation = data.continuation ? this.parseC(data.continuation) : null;
|
||||
if (continuation) {
|
||||
parsed_data.continuation = continuation;
|
||||
}
|
||||
|
||||
const metadata = this.parse(data.metadata);
|
||||
if (metadata) {
|
||||
parsed_data.metadata = metadata;
|
||||
}
|
||||
|
||||
const microformat = this.parseItem(data.microformat);
|
||||
if (microformat) {
|
||||
parsed_data.microformat = microformat;
|
||||
}
|
||||
|
||||
const overlay = this.parseItem(data.overlay);
|
||||
if (overlay) {
|
||||
parsed_data.overlay = overlay;
|
||||
}
|
||||
|
||||
const alerts = this.parseArray<Alert>(data.alerts);
|
||||
if (alerts.length) {
|
||||
parsed_data.alerts = alerts;
|
||||
}
|
||||
|
||||
const refinements = data.refinements;
|
||||
if (refinements) {
|
||||
parsed_data.refinements = refinements;
|
||||
}
|
||||
|
||||
const estimated_results = data.estimatedResults ? parseInt(data.estimatedResults) : null;
|
||||
if (estimated_results) {
|
||||
parsed_data.estimated_results = estimated_results;
|
||||
}
|
||||
|
||||
const player_overlays = this.parse(data.playerOverlays);
|
||||
if (player_overlays) {
|
||||
parsed_data.player_overlays = player_overlays;
|
||||
}
|
||||
|
||||
const playback_tracking = data.playbackTracking ? {
|
||||
videostats_watchtime_url: data.playbackTracking.videostatsWatchtimeUrl.baseUrl,
|
||||
videostats_playback_url: data.playbackTracking.videostatsPlaybackUrl.baseUrl
|
||||
} : null;
|
||||
|
||||
if (playback_tracking) {
|
||||
parsed_data.playback_tracking = playback_tracking;
|
||||
}
|
||||
|
||||
const playability_status = data.playabilityStatus ? {
|
||||
status: data.playabilityStatus.status,
|
||||
reason: data.playabilityStatus.reason || '',
|
||||
embeddable: !!data.playabilityStatus.playableInEmbed || false,
|
||||
audio_only_playablility: this.parseItem<AudioOnlyPlayability>(data.playabilityStatus.audioOnlyPlayability),
|
||||
error_screen: this.parseItem(data.playabilityStatus.errorScreen)
|
||||
} : null;
|
||||
|
||||
if (playability_status) {
|
||||
parsed_data.playability_status = playability_status;
|
||||
}
|
||||
|
||||
const streaming_data = data.streamingData ? {
|
||||
expires: new Date(Date.now() + parseInt(data.streamingData.expiresInSeconds) * 1000),
|
||||
formats: Parser.parseFormats(data.streamingData.formats),
|
||||
adaptive_formats: Parser.parseFormats(data.streamingData.adaptiveFormats),
|
||||
dash_manifest_url: data.streamingData.dashManifestUrl || null,
|
||||
hls_manifest_url: data.streamingData.hlsManifestUrl || null
|
||||
} : undefined;
|
||||
|
||||
if (streaming_data) {
|
||||
parsed_data.streaming_data = streaming_data;
|
||||
}
|
||||
|
||||
const current_video_endpoint = data.currentVideoEndpoint ? new NavigationEndpoint(data.currentVideoEndpoint) : null;
|
||||
if (current_video_endpoint) {
|
||||
parsed_data.current_video_endpoint = current_video_endpoint;
|
||||
}
|
||||
|
||||
const endpoint = data.endpoint ? new NavigationEndpoint(data.endpoint) : null;
|
||||
if (endpoint) {
|
||||
parsed_data.endpoint = endpoint;
|
||||
}
|
||||
|
||||
const captions = this.parseItem<PlayerCaptionsTracklist>(data.captions);
|
||||
if (captions) {
|
||||
parsed_data.captions = captions;
|
||||
}
|
||||
|
||||
const video_details = data.videoDetails ? new VideoDetails(data.videoDetails) : null;
|
||||
if (video_details) {
|
||||
parsed_data.video_details = video_details;
|
||||
}
|
||||
|
||||
const annotations = this.parseArray<PlayerAnnotationsExpanded>(data.annotations);
|
||||
if (annotations.length) {
|
||||
parsed_data.annotations = annotations;
|
||||
}
|
||||
|
||||
const storyboards = this.parseItem<PlayerStoryboardSpec | PlayerLiveStoryboardSpec>(data.storyboards);
|
||||
if (storyboards) {
|
||||
parsed_data.storyboards = storyboards;
|
||||
}
|
||||
|
||||
const endscreen = this.parseItem<Endscreen>(data.endscreen);
|
||||
if (endscreen) {
|
||||
parsed_data.endscreen = endscreen;
|
||||
}
|
||||
|
||||
const cards = this.parseItem<CardCollection>(data.cards);
|
||||
if (cards) {
|
||||
parsed_data.cards = cards;
|
||||
}
|
||||
|
||||
return parsed_data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +271,7 @@ export default class Parser {
|
||||
* @param data - The data to parse.
|
||||
* @param validTypes - YTNode types that are allowed to be parsed.
|
||||
*/
|
||||
static parseItem<T extends YTNode = YTNode>(data: any, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
||||
static parseItem<T extends YTNode = YTNode>(data?: RawNode, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
||||
if (!data) return null;
|
||||
|
||||
const keys = Object.keys(data);
|
||||
@@ -214,7 +311,7 @@ export default class Parser {
|
||||
* @param data - The data to parse.
|
||||
* @param validTypes - YTNode types that are allowed to be parsed.
|
||||
*/
|
||||
static parseArray<T extends YTNode = YTNode>(data: any[], validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
||||
static parseArray<T extends YTNode = YTNode>(data?: RawNode[], validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
||||
if (Array.isArray(data)) {
|
||||
const results: T[] = [];
|
||||
|
||||
@@ -238,9 +335,9 @@ export default class Parser {
|
||||
* @param requireArray - Whether the data should be parsed as an array.
|
||||
* @param validTypes - YTNode types that are allowed to be parsed.
|
||||
*/
|
||||
static parse<T extends YTNode = YTNode>(data: any, requireArray: true, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]): ObservedArray<T> | null;
|
||||
static parse<T extends YTNode = YTNode>(data: any, requireArray?: false | undefined, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]): SuperParsedResult<T>;
|
||||
static parse<T extends YTNode = YTNode>(data: any, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
||||
static parse<T extends YTNode = YTNode>(data: RawData, requireArray: true, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]): ObservedArray<T> | null;
|
||||
static parse<T extends YTNode = YTNode>(data?: RawData, requireArray?: false | undefined, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]): SuperParsedResult<T>;
|
||||
static parse<T extends YTNode = YTNode>(data?: RawData, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
||||
if (!data) return null;
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
@@ -263,12 +360,13 @@ export default class Parser {
|
||||
return new SuperParsedResult(this.parseItem(data, validTypes));
|
||||
}
|
||||
|
||||
static parseC(data: any) {
|
||||
static parseC(data: RawNode) {
|
||||
if (data.timedContinuationData)
|
||||
return new Continuation({ continuation: data.timedContinuationData, type: 'timed' });
|
||||
return null;
|
||||
}
|
||||
|
||||
static parseLC(data: any) {
|
||||
static parseLC(data: RawNode) {
|
||||
if (data.itemSectionContinuation)
|
||||
return new ItemSectionContinuation(data.itemSectionContinuation);
|
||||
if (data.sectionListContinuation)
|
||||
@@ -283,10 +381,14 @@ export default class Parser {
|
||||
return new GridContinuation(data.gridContinuation);
|
||||
if (data.playlistPanelContinuation)
|
||||
return new PlaylistPanelContinuation(data.playlistPanelContinuation);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static parseRR(actions: any[]) {
|
||||
static parseRR(actions: RawNode[]) {
|
||||
return observe(actions.map((action: any) => {
|
||||
if (action.navigateAction)
|
||||
return new NavigateAction(action.navigateAction);
|
||||
if (action.reloadContinuationItemsCommand)
|
||||
return new ReloadContinuationItemsCommand(action.reloadContinuationItemsCommand);
|
||||
if (action.appendContinuationItemsAction)
|
||||
@@ -294,21 +396,21 @@ export default class Parser {
|
||||
}).filter((item) => item) as (ReloadContinuationItemsCommand | AppendContinuationItemsAction)[]);
|
||||
}
|
||||
|
||||
static parseActions(data: any) {
|
||||
static parseActions(data: RawData) {
|
||||
if (Array.isArray(data)) {
|
||||
return Parser.parse(data.map((action) => {
|
||||
delete action.clickTrackingParams;
|
||||
return action;
|
||||
}));
|
||||
}
|
||||
return new SuperParsedResult(Parser.parseItem(data));
|
||||
return new SuperParsedResult(this.parseItem(data));
|
||||
}
|
||||
|
||||
static parseFormats(formats: any[]) {
|
||||
static parseFormats(formats: RawNode[]): Format[] {
|
||||
return formats?.map((format) => new Format(format)) || [];
|
||||
}
|
||||
|
||||
static applyMutations(memo: Memo, mutations: Array<any>) {
|
||||
static applyMutations(memo: Memo, mutations: RawNode[]) {
|
||||
// Apply mutations to MusicMultiSelectMenuItems
|
||||
const music_multi_select_menu_items = memo.getType(MusicMultiSelectMenuItem);
|
||||
|
||||
@@ -351,7 +453,7 @@ export default class Parser {
|
||||
return console.warn(
|
||||
new InnertubeError(
|
||||
`${classname} not found!\n` +
|
||||
`This is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!`, classdata
|
||||
`This is a bug, want to help us fix it? Follow the instructions at ${Platform.shim.info.repo_url.split('#')[0]}/blob/main/docs/updating-the-parser.md or report it at ${Platform.shim.info.bugs_url}!`, classdata
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -379,7 +481,8 @@ export default class Parser {
|
||||
'PromotedSparklesWeb',
|
||||
'RunAttestationCommand',
|
||||
'CompactPromotedVideo',
|
||||
'StatementBanner'
|
||||
'StatementBanner',
|
||||
'SearchSubMenu'
|
||||
]);
|
||||
|
||||
static shouldIgnore(classname: string) {
|
||||
@@ -387,8 +490,6 @@ export default class Parser {
|
||||
}
|
||||
}
|
||||
|
||||
export type ParsedResponse = ReturnType<typeof Parser.parseResponse>;
|
||||
|
||||
// Continuation
|
||||
|
||||
export class ItemSectionContinuation extends YTNode {
|
||||
@@ -397,21 +498,32 @@ export class ItemSectionContinuation extends YTNode {
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
continuation?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
if (data.continuations) {
|
||||
if (Array.isArray(data.continuations)) {
|
||||
this.continuation = data.continuations?.at(0)?.nextContinuationData?.continuation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NavigateAction extends YTNode {
|
||||
static readonly type = 'navigateAction';
|
||||
|
||||
endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.endpoint = new NavigationEndpoint(data.endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
export class AppendContinuationItemsAction extends YTNode {
|
||||
static readonly type = 'appendContinuationItemsAction';
|
||||
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parse(data.continuationItems, true);
|
||||
}
|
||||
@@ -424,7 +536,7 @@ export class ReloadContinuationItemsCommand extends YTNode {
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
slot?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.target_id = data.targetId;
|
||||
this.contents = Parser.parse(data.continuationItems, true);
|
||||
@@ -438,7 +550,7 @@ export class SectionListContinuation extends YTNode {
|
||||
continuation: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parse(data.contents, true);
|
||||
this.continuation =
|
||||
@@ -453,7 +565,7 @@ export class MusicPlaylistShelfContinuation extends YTNode {
|
||||
continuation: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parse(data.contents, true);
|
||||
this.continuation = data.continuations?.[0].nextContinuationData.continuation || null;
|
||||
@@ -466,9 +578,9 @@ export class MusicShelfContinuation extends YTNode {
|
||||
continuation: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parse(data.contents, true);
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
this.continuation =
|
||||
data.continuations?.[0].nextContinuationData?.continuation ||
|
||||
data.continuations?.[0].reloadContinuationData?.continuation || null;
|
||||
@@ -481,7 +593,7 @@ export class GridContinuation extends YTNode {
|
||||
continuation: string;
|
||||
items: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.items = Parser.parse(data.items, true);
|
||||
this.continuation = data.continuations?.[0].nextContinuationData.continuation || null;
|
||||
@@ -498,9 +610,9 @@ export class PlaylistPanelContinuation extends YTNode {
|
||||
continuation: string;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.contents = Parser.parse(data.contents, true);
|
||||
this.contents = Parser.parseArray(data.contents);
|
||||
this.continuation = data.continuations?.[0]?.nextContinuationData?.continuation ||
|
||||
data.continuations?.[0]?.nextRadioContinuationData?.continuation || null;
|
||||
}
|
||||
@@ -514,7 +626,7 @@ export class Continuation extends YTNode {
|
||||
time_until_last_message_ms?: number;
|
||||
token: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.continuation_type = data.type;
|
||||
this.timeout_ms = data.continuation?.timeoutMs;
|
||||
@@ -541,7 +653,7 @@ export class LiveChatContinuation extends YTNode {
|
||||
continuation: Continuation;
|
||||
viewer_name: string;
|
||||
|
||||
constructor(data: any) {
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.actions = Parser.parse(data.actions?.map((action: any) => {
|
||||
delete action.clickTrackingParams;
|
||||
|
||||
159
src/parser/types/ParsedResponse.ts
Normal file
159
src/parser/types/ParsedResponse.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Memo, ObservedArray, SuperParsedResult, YTNode } from '../helpers.js';
|
||||
|
||||
import type {
|
||||
ReloadContinuationItemsCommand, AppendContinuationItemsAction, Continuation, GridContinuation,
|
||||
ItemSectionContinuation, LiveChatContinuation, MusicPlaylistShelfContinuation, MusicShelfContinuation,
|
||||
PlaylistPanelContinuation, SectionListContinuation
|
||||
} from '../index.js';
|
||||
|
||||
import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js';
|
||||
import type CardCollection from '../classes/CardCollection.js';
|
||||
import type Endscreen from '../classes/Endscreen.js';
|
||||
import type AudioOnlyPlayability from '../classes/AudioOnlyPlayability.js';
|
||||
import type Format from '../classes/misc/Format.js';
|
||||
import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.js';
|
||||
import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.js';
|
||||
import type VideoDetails from '../classes/misc/VideoDetails.js';
|
||||
import type Alert from '../classes/Alert.js';
|
||||
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.js';
|
||||
|
||||
export interface IParsedResponse {
|
||||
actions?: SuperParsedResult<YTNode>;
|
||||
actions_memo?: Memo;
|
||||
contents?: SuperParsedResult<YTNode>;
|
||||
contents_memo?: Memo;
|
||||
header?: SuperParsedResult<YTNode>;
|
||||
header_memo?: Memo;
|
||||
sidebar?: YTNode;
|
||||
sidebar_memo?: Memo;
|
||||
live_chat_item_context_menu_supported_renderers?: YTNode;
|
||||
live_chat_item_context_menu_supported_renderers_memo?: Memo;
|
||||
on_response_received_actions?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
on_response_received_actions_memo?: Memo;
|
||||
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
on_response_received_endpoints_memo?: Memo;
|
||||
on_response_received_commands?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
on_response_received_commands_memo?: Memo;
|
||||
continuation?: Continuation;
|
||||
continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation |
|
||||
MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation;
|
||||
continuation_contents_memo?: Memo;
|
||||
metadata?: SuperParsedResult<YTNode>;
|
||||
microformat?: YTNode;
|
||||
overlay?: YTNode;
|
||||
alerts?: ObservedArray<Alert>;
|
||||
refinements?: string[];
|
||||
estimated_results?: number;
|
||||
player_overlays?: SuperParsedResult<YTNode>;
|
||||
playback_tracking?: {
|
||||
videostats_watchtime_url: string;
|
||||
videostats_playback_url: string;
|
||||
};
|
||||
playability_status?: {
|
||||
status: string;
|
||||
error_screen: YTNode | null;
|
||||
audio_only_playablility: AudioOnlyPlayability | null;
|
||||
embeddable: boolean;
|
||||
reason: string;
|
||||
};
|
||||
streaming_data?: {
|
||||
expires: Date;
|
||||
formats: Format[];
|
||||
adaptive_formats: Format[];
|
||||
dash_manifest_url: string | null;
|
||||
hls_manifest_url: string | null;
|
||||
};
|
||||
current_video_endpoint?: NavigationEndpoint;
|
||||
endpoint?: NavigationEndpoint;
|
||||
captions?: PlayerCaptionsTracklist;
|
||||
video_details?: VideoDetails;
|
||||
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
|
||||
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
|
||||
endscreen?: Endscreen;
|
||||
cards?: CardCollection;
|
||||
}
|
||||
|
||||
export interface IPlayerResponse {
|
||||
captions?: PlayerCaptionsTracklist;
|
||||
cards?: CardCollection;
|
||||
endscreen?: Endscreen;
|
||||
microformat?: YTNode;
|
||||
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
|
||||
playability_status: {
|
||||
status: string;
|
||||
error_screen: YTNode | null;
|
||||
audio_only_playablility: AudioOnlyPlayability | null;
|
||||
embeddable: boolean;
|
||||
reason: string;
|
||||
};
|
||||
streaming_data?: {
|
||||
expires: Date;
|
||||
formats: Format[];
|
||||
adaptive_formats: Format[];
|
||||
dash_manifest_url: string | null;
|
||||
hls_manifest_url: string | null;
|
||||
};
|
||||
playback_tracking?: {
|
||||
videostats_watchtime_url: string;
|
||||
videostats_playback_url: string;
|
||||
};
|
||||
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
|
||||
video_details?: VideoDetails;
|
||||
}
|
||||
|
||||
export interface INextResponse {
|
||||
contents?: SuperParsedResult<YTNode>;
|
||||
contents_memo?: Memo;
|
||||
current_video_endpoint?: NavigationEndpoint;
|
||||
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
on_response_received_endpoints_memo?: Memo;
|
||||
player_overlays?: SuperParsedResult<YTNode>;
|
||||
}
|
||||
|
||||
export interface IBrowseResponse {
|
||||
continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation |
|
||||
MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation;
|
||||
continuation_contents_memo?: Memo;
|
||||
on_response_received_actions: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
on_response_received_actions_memo: Memo;
|
||||
on_response_received_endpoints?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
on_response_received_endpoints_memo?: Memo;
|
||||
contents?: SuperParsedResult<YTNode>;
|
||||
contents_memo?: Memo;
|
||||
header?: SuperParsedResult<YTNode>;
|
||||
header_memo?: Memo;
|
||||
metadata?: SuperParsedResult<YTNode>;
|
||||
microformat?: YTNode;
|
||||
alerts?: ObservedArray<Alert>;
|
||||
sidebar?: YTNode;
|
||||
sidebar_memo?: Memo;
|
||||
}
|
||||
|
||||
export interface ISearchResponse {
|
||||
header?: SuperParsedResult<YTNode>;
|
||||
header_memo?: Memo;
|
||||
contents?: SuperParsedResult<YTNode>;
|
||||
contents_memo?: Memo;
|
||||
on_response_received_commands?: ObservedArray<ReloadContinuationItemsCommand | AppendContinuationItemsAction>;
|
||||
continuation_contents?: ItemSectionContinuation | SectionListContinuation | LiveChatContinuation | MusicPlaylistShelfContinuation |
|
||||
MusicShelfContinuation | GridContinuation | PlaylistPanelContinuation;
|
||||
continuation_contents_memo?: Memo;
|
||||
refinements?: string[];
|
||||
estimated_results: number;
|
||||
}
|
||||
|
||||
export interface IResolveURLResponse {
|
||||
endpoint: NavigationEndpoint;
|
||||
}
|
||||
|
||||
export interface IGetNotificationsMenuResponse {
|
||||
actions: SuperParsedResult<YTNode>;
|
||||
actions_memo: Memo;
|
||||
}
|
||||
|
||||
export interface IUpdatedMetadataResponse {
|
||||
actions: SuperParsedResult<YTNode>;
|
||||
actions_memo: Memo;
|
||||
continuation?: Continuation;
|
||||
}
|
||||
55
src/parser/types/RawResponse.ts
Normal file
55
src/parser/types/RawResponse.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export type RawNode = Record<string, any>;
|
||||
export type RawData = RawNode | RawNode[];
|
||||
|
||||
export interface IRawResponse {
|
||||
contents?: RawData;
|
||||
onResponseReceivedActions?: RawNode[];
|
||||
onResponseReceivedEndpoints?: RawNode[];
|
||||
onResponseReceivedCommands?: RawNode[];
|
||||
continuationContents?: RawNode;
|
||||
actions?: RawNode[];
|
||||
liveChatItemContextMenuSupportedRenderers?: RawNode;
|
||||
header?: RawNode;
|
||||
sidebar?: RawNode;
|
||||
continuation?: RawNode;
|
||||
metadata?: RawNode;
|
||||
microformat?: RawNode;
|
||||
overlay?: RawNode;
|
||||
alerts?: RawNode[];
|
||||
refinements?: string[];
|
||||
estimatedResults?: string;
|
||||
playerOverlays?: RawNode;
|
||||
playbackTracking?: {
|
||||
videostatsWatchtimeUrl: {
|
||||
baseUrl: string;
|
||||
};
|
||||
videostatsPlaybackUrl: {
|
||||
baseUrl: string;
|
||||
};
|
||||
};
|
||||
playabilityStatus?: {
|
||||
status: string;
|
||||
reason?: string;
|
||||
errorScreen?: RawNode;
|
||||
audioOnlyPlayability?: RawNode;
|
||||
playableInEmbed?: boolean;
|
||||
};
|
||||
streamingData?: {
|
||||
expiresInSeconds: string;
|
||||
formats: RawNode[];
|
||||
adaptiveFormats: RawNode[];
|
||||
dashManifestUrl?: string;
|
||||
hlsManifestUrl?: string;
|
||||
};
|
||||
currentVideoEndpoint?: RawNode;
|
||||
unseenCount?: number;
|
||||
playlistId?: string;
|
||||
endpoint?: RawNode;
|
||||
captions?: RawNode;
|
||||
videoDetails?: RawNode;
|
||||
annotations?: RawNode[];
|
||||
storyboards?: RawNode;
|
||||
endscreen?: RawNode;
|
||||
cards?: RawNode;
|
||||
frameworkUpdates?: any;
|
||||
}
|
||||
2
src/parser/types/index.ts
Normal file
2
src/parser/types/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './RawResponse.js';
|
||||
export * from './ParsedResponse.js';
|
||||
@@ -1,5 +1,6 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IParsedResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
import AccountSectionList from '../classes/AccountSectionList.js';
|
||||
import AccountItemSection from '../classes/AccountItemSection.js';
|
||||
@@ -8,7 +9,7 @@ import AccountChannel from '../classes/AccountChannel.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
class AccountInfo {
|
||||
#page: ParsedResponse;
|
||||
#page: IParsedResponse;
|
||||
|
||||
contents: AccountItemSection | null;
|
||||
footers: AccountChannel | null;
|
||||
@@ -16,7 +17,10 @@ class AccountInfo {
|
||||
constructor(response: ApiResponse) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
|
||||
const account_section_list = this.#page.contents.array().as(AccountSectionList)?.[0];
|
||||
if (!this.#page.contents)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
const account_section_list = this.#page.contents.array().as(AccountSectionList).first();
|
||||
|
||||
if (!account_section_list)
|
||||
throw new InnertubeError('Account section list not found');
|
||||
@@ -25,7 +29,7 @@ class AccountInfo {
|
||||
this.footers = account_section_list.footers;
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IParsedResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import Parser from '../index.js';
|
||||
import Element from '../classes/Element.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Analytics {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
sections;
|
||||
|
||||
constructor(response: ApiResponse) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.sections = this.#page.contents_memo?.getType(Element).map((el) => el.model?.item());
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
this.sections = this.#page.contents_memo?.getType(Element).map((el) => el.model?.item()).flatMap((el) => !el ? [] : el);
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import TabbedFeed from '../../core/TabbedFeed.js';
|
||||
import C4TabbedHeader from '../classes/C4TabbedHeader.js';
|
||||
import CarouselHeader from '../classes/CarouselHeader.js';
|
||||
@@ -21,14 +20,17 @@ import SortFilterSubMenu from '../classes/SortFilterSubMenu.js';
|
||||
import { ChannelError, InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
import type { AppendContinuationItemsAction, ReloadContinuationItemsCommand } from '../index.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
export default class Channel extends TabbedFeed {
|
||||
export default class Channel extends TabbedFeed<IBrowseResponse> {
|
||||
header?: C4TabbedHeader | CarouselHeader | InteractiveTabbedHeader;
|
||||
metadata;
|
||||
subscribe_button?: SubscribeButton;
|
||||
current_tab?: Tab | ExpandableTab;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
this.header = this.page.header?.item()?.as(C4TabbedHeader, CarouselHeader, InteractiveTabbedHeader);
|
||||
@@ -48,9 +50,9 @@ export default class Channel extends TabbedFeed {
|
||||
|
||||
this.metadata = { ...metadata, ...(microformat || {}) };
|
||||
|
||||
this.subscribe_button = this.page.header_memo.getType(SubscribeButton)?.[0];
|
||||
this.subscribe_button = this.page.header_memo?.getType(SubscribeButton).first();
|
||||
|
||||
this.current_tab = this.page.contents.item().key('tabs').parsed().array().filterType(Tab, ExpandableTab).get({ selected: true });
|
||||
this.current_tab = this.page.contents?.item().key('tabs').parsed().array().filterType(Tab, ExpandableTab).get({ selected: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +75,10 @@ export default class Channel extends TabbedFeed {
|
||||
if (!target_filter)
|
||||
throw new InnertubeError('Invalid filter', filter);
|
||||
|
||||
const page = await target_filter.endpoint?.call(this.actions, { parse: true });
|
||||
const page = await target_filter.endpoint?.call<IBrowseResponse>(this.actions, { parse: true });
|
||||
|
||||
if (!page)
|
||||
throw new InnertubeError('No page returned', { filter: target_filter });
|
||||
|
||||
return new FilteredChannelList(this.actions, page, true);
|
||||
}
|
||||
@@ -96,7 +101,7 @@ export default class Channel extends TabbedFeed {
|
||||
if (target_sort.selected)
|
||||
return this;
|
||||
|
||||
const page = await target_sort.endpoint?.call(this.actions, { parse: true });
|
||||
const page = await target_sort.endpoint?.call<IBrowseResponse>(this.actions, { parse: true });
|
||||
|
||||
return new Channel(this.actions, page, true);
|
||||
}
|
||||
@@ -119,7 +124,7 @@ export default class Channel extends TabbedFeed {
|
||||
if (item.selected)
|
||||
return this;
|
||||
|
||||
const page = await item.endpoint?.call(this.actions, { parse: true });
|
||||
const page = await item.endpoint?.call<IBrowseResponse>(this.actions, { parse: true });
|
||||
|
||||
return new Channel(this.actions, page, true);
|
||||
}
|
||||
@@ -191,7 +196,7 @@ export default class Channel extends TabbedFeed {
|
||||
if (!tab)
|
||||
throw new InnertubeError('Search tab not found', this);
|
||||
|
||||
const page = await tab.endpoint?.call(this.actions, { query, parse: true });
|
||||
const page = await tab.endpoint?.call<IBrowseResponse>(this.actions, { query, parse: true });
|
||||
|
||||
return new Channel(this.actions, page, true);
|
||||
}
|
||||
@@ -237,14 +242,16 @@ export default class Channel extends TabbedFeed {
|
||||
*/
|
||||
async getContinuation(): Promise<ChannelListContinuation> {
|
||||
const page = await super.getContinuationData();
|
||||
if (!page)
|
||||
throw new InnertubeError('Could not get continuation data');
|
||||
return new ChannelListContinuation(this.actions, page, true);
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelListContinuation extends Feed {
|
||||
export class ChannelListContinuation extends Feed<IBrowseResponse> {
|
||||
contents: ReloadContinuationItemsCommand | AppendContinuationItemsAction | undefined;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.contents =
|
||||
this.page.on_response_received_actions?.[0] ||
|
||||
@@ -256,15 +263,17 @@ export class ChannelListContinuation extends Feed {
|
||||
*/
|
||||
async getContinuation(): Promise<ChannelListContinuation> {
|
||||
const page = await super.getContinuationData();
|
||||
if (!page)
|
||||
throw new InnertubeError('Could not get continuation data');
|
||||
return new ChannelListContinuation(this.actions, page, true);
|
||||
}
|
||||
}
|
||||
|
||||
export class FilteredChannelList extends FilterableFeed {
|
||||
export class FilteredChannelList extends FilterableFeed<IBrowseResponse> {
|
||||
applied_filter?: ChipCloudChip;
|
||||
contents;
|
||||
contents: ReloadContinuationItemsCommand | AppendContinuationItemsAction;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
this.applied_filter = this.memo.getType(ChipCloudChip).get({ is_selected: true });
|
||||
@@ -277,7 +286,7 @@ export class FilteredChannelList extends FilterableFeed {
|
||||
this.page.on_response_received_actions.shift();
|
||||
}
|
||||
|
||||
this.contents = this.page.on_response_received_actions?.[0];
|
||||
this.contents = this.page.on_response_received_actions.first();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,9 +304,12 @@ export class FilteredChannelList extends FilterableFeed {
|
||||
async getContinuation(): Promise<FilteredChannelList> {
|
||||
const page = await super.getContinuationData();
|
||||
|
||||
if (!page?.on_response_received_actions_memo)
|
||||
throw new InnertubeError('Unexpected continuation data', page);
|
||||
|
||||
// Keep the filters
|
||||
page?.on_response_received_actions_memo.set('FeedFilterChipBar', this.memo.getType(FeedFilterChipBar));
|
||||
page?.on_response_received_actions_memo.set('ChipCloudChip', this.memo.getType(ChipCloudChip));
|
||||
page.on_response_received_actions_memo.set('FeedFilterChipBar', this.memo.getType(FeedFilterChipBar));
|
||||
page.on_response_received_actions_memo.set('ChipCloudChip', this.memo.getType(ChipCloudChip));
|
||||
|
||||
return new FilteredChannelList(this.actions, page, true);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import { observe, ObservedArray } from '../helpers.js';
|
||||
import type { INextResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
import CommentsHeader from '../classes/comments/CommentsHeader.js';
|
||||
import CommentSimplebox from '../classes/comments/CommentSimplebox.js';
|
||||
@@ -10,7 +11,7 @@ import CommentThread from '../classes/comments/CommentThread.js';
|
||||
import ContinuationItem from '../classes/ContinuationItem.js';
|
||||
|
||||
class Comments {
|
||||
#page: ParsedResponse;
|
||||
#page: INextResponse;
|
||||
#actions: Actions;
|
||||
#continuation?: ContinuationItem;
|
||||
|
||||
@@ -18,7 +19,7 @@ class Comments {
|
||||
contents: ObservedArray<CommentThread>;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
this.#page = already_parsed ? data : Parser.parseResponse(data);
|
||||
this.#page = already_parsed ? data : Parser.parseResponse<INextResponse>(data);
|
||||
this.#actions = actions;
|
||||
|
||||
const contents = this.#page.on_response_received_endpoints;
|
||||
@@ -116,7 +117,7 @@ class Comments {
|
||||
return !!this.#continuation;
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): INextResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,28 @@ import type Actions from '../../core/Actions.js';
|
||||
import Feed from '../../core/Feed.js';
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import BrowseFeedActions from '../classes/BrowseFeedActions.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
// TODO: make feed actions usable
|
||||
class History extends Feed {
|
||||
class History extends Feed<IBrowseResponse> {
|
||||
sections: ItemSection[];
|
||||
feed_actions: BrowseFeedActions;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.sections = this.memo.getType(ItemSection);
|
||||
this.feed_actions = this.memo.getType(BrowseFeedActions)?.[0];
|
||||
this.feed_actions = this.memo.getType(BrowseFeedActions).first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves next batch of contents.
|
||||
*/
|
||||
async getContinuation(): Promise<History> {
|
||||
const continuation = await this.getContinuationData();
|
||||
return new History(this.actions, continuation, true);
|
||||
const response = await this.getContinuationData();
|
||||
if (!response)
|
||||
throw new Error('No continuation data found');
|
||||
return new History(this.actions, response, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,18 @@ import ChipCloudChip from '../classes/ChipCloudChip.js';
|
||||
import FeedTabbedHeader from '../classes/FeedTabbedHeader.js';
|
||||
import RichGrid from '../classes/RichGrid.js';
|
||||
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
import type { AppendContinuationItemsAction, ReloadContinuationItemsCommand } from '../index.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
export default class HomeFeed extends FilterableFeed {
|
||||
export default class HomeFeed extends FilterableFeed<IBrowseResponse> {
|
||||
contents: RichGrid | AppendContinuationItemsAction | ReloadContinuationItemsCommand;
|
||||
header: FeedTabbedHeader;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.header = this.memo.getType<FeedTabbedHeader>(FeedTabbedHeader)?.[0];
|
||||
this.contents =
|
||||
this.memo.getType<RichGrid>(RichGrid)?.[0] ||
|
||||
this.page.on_response_received_actions?.[0];
|
||||
this.header = this.memo.getType(FeedTabbedHeader).first();
|
||||
this.contents = this.memo.getType(RichGrid).first() || this.page.on_response_received_actions.first();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ export default class HomeFeed extends FilterableFeed {
|
||||
|
||||
// Keep the page header
|
||||
feed.page.header = this.page.header;
|
||||
feed.page.header_memo.set(this.header.type, [ this.header ]);
|
||||
feed.page.header_memo?.set(this.header.type, [ this.header ]);
|
||||
|
||||
return new HomeFeed(this.actions, feed.page, true);
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import MenuServiceItem from '../classes/menus/MenuServiceItem.js';
|
||||
import NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ParsedResponse } from '../index.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import type { IParsedResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class ItemMenu {
|
||||
#page: ParsedResponse;
|
||||
#page: IParsedResponse;
|
||||
#actions: Actions;
|
||||
#items: ObservedArray<YTNode>;
|
||||
|
||||
constructor(data: ParsedResponse, actions: Actions) {
|
||||
constructor(data: IParsedResponse, actions: Actions) {
|
||||
this.#page = data;
|
||||
this.#actions = actions;
|
||||
|
||||
@@ -25,9 +25,9 @@ class ItemMenu {
|
||||
this.#items = menu.as(Menu).items;
|
||||
}
|
||||
|
||||
async selectItem(icon_type: string): Promise<ParsedResponse>
|
||||
async selectItem(button: Button): Promise<ParsedResponse>
|
||||
async selectItem(item: string | Button): Promise<ParsedResponse> {
|
||||
async selectItem(icon_type: string): Promise<IParsedResponse>
|
||||
async selectItem(button: Button): Promise<IParsedResponse>
|
||||
async selectItem(item: string | Button): Promise<IParsedResponse> {
|
||||
let endpoint: NavigationEndpoint | undefined;
|
||||
|
||||
if (item instanceof Button) {
|
||||
@@ -62,7 +62,7 @@ class ItemMenu {
|
||||
return this.#items;
|
||||
}
|
||||
|
||||
page(): ParsedResponse {
|
||||
page(): IParsedResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
import Feed from '../../core/Feed.js';
|
||||
@@ -13,10 +11,10 @@ import Button from '../classes/Button.js';
|
||||
import ProfileColumnStats from '../classes/ProfileColumnStats.js';
|
||||
import ProfileColumnUserInfo from '../classes/ProfileColumnUserInfo.js';
|
||||
|
||||
class Library {
|
||||
#actions: Actions;
|
||||
#page: ParsedResponse;
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
import { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
class Library extends Feed<IBrowseResponse> {
|
||||
profile: {
|
||||
stats?: ProfileColumnStats;
|
||||
user_info?: ProfileColumnUserInfo;
|
||||
@@ -24,16 +22,18 @@ class Library {
|
||||
|
||||
sections;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#actions = actions;
|
||||
this.#page = Parser.parseResponse(response);
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse) {
|
||||
super(actions, data);
|
||||
|
||||
const stats = this.#page.contents_memo.getType(ProfileColumnStats)?.[0];
|
||||
const user_info = this.#page.contents_memo.getType(ProfileColumnUserInfo)?.[0];
|
||||
if (!this.page.contents_memo)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
const stats = this.page.contents_memo.getType(ProfileColumnStats)?.[0];
|
||||
const user_info = this.page.contents_memo.getType(ProfileColumnUserInfo)?.[0];
|
||||
|
||||
this.profile = { stats, user_info };
|
||||
|
||||
const shelves = this.#page.contents_memo.getType(Shelf);
|
||||
const shelves = this.page.contents_memo.getType(Shelf);
|
||||
|
||||
this.sections = shelves.map((shelf: Shelf) => ({
|
||||
type: shelf.icon_type,
|
||||
@@ -52,16 +52,16 @@ class Library {
|
||||
if (!button)
|
||||
throw new InnertubeError('Did not find target button.');
|
||||
|
||||
const page = await button.as(Button).endpoint.call(this.#actions, { parse: true });
|
||||
const page = await button.as(Button).endpoint.call<IBrowseResponse>(this.actions, { parse: true });
|
||||
|
||||
switch (shelf.icon_type) {
|
||||
case 'LIKE':
|
||||
case 'WATCH_LATER':
|
||||
return new Playlist(this.#actions, page, true);
|
||||
return new Playlist(this.actions, page, true);
|
||||
case 'WATCH_HISTORY':
|
||||
return new History(this.#actions, page, true);
|
||||
return new History(this.actions, page, true);
|
||||
case 'CONTENT_CUT':
|
||||
return new Feed(this.#actions, page, true);
|
||||
return new Feed(this.actions, page, true);
|
||||
default:
|
||||
throw new InnertubeError('Target shelf not implemented.');
|
||||
}
|
||||
@@ -79,17 +79,13 @@ class Library {
|
||||
return this.sections.find((section) => section.type === 'LIKE');
|
||||
}
|
||||
|
||||
get playlists() {
|
||||
get playlists_section() {
|
||||
return this.sections.find((section) => section.type === 'PLAYLISTS');
|
||||
}
|
||||
|
||||
get clips() {
|
||||
return this.sections.find((section) => section.type === 'CONTENT_CUT');
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
export default Library;
|
||||
@@ -1,5 +1,5 @@
|
||||
import EventEmitter from '../../utils/EventEmitterLike.js';
|
||||
import Parser, { LiveChatContinuation, ParsedResponse } from '../index.js';
|
||||
import Parser, { LiveChatContinuation } from '../index.js';
|
||||
import VideoInfo from './VideoInfo.js';
|
||||
import SmoothedQueue from './SmoothedQueue.js';
|
||||
|
||||
@@ -35,6 +35,7 @@ import LiveChatViewerEngagementMessage from '../classes/livechat/items/LiveChatV
|
||||
import ItemMenu from './ItemMenu.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { IParsedResponse, IUpdatedMetadataResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
export type ChatAction =
|
||||
AddChatItemAction | AddBannerToLiveChatCommand | AddLiveChatTickerItemAction |
|
||||
@@ -219,7 +220,7 @@ class LiveChat extends EventEmitter {
|
||||
}
|
||||
|
||||
const response = await this.#actions.execute('/updated_metadata', payload);
|
||||
const data = Parser.parseResponse(response.data);
|
||||
const data = Parser.parseResponse<IUpdatedMetadataResponse>(response.data);
|
||||
|
||||
this.#mcontinuation = data.continuation?.token;
|
||||
|
||||
@@ -301,7 +302,7 @@ class LiveChat extends EventEmitter {
|
||||
/**
|
||||
* Equivalent to "clicking" a button.
|
||||
*/
|
||||
async selectButton(button: Button): Promise<ParsedResponse> {
|
||||
async selectButton(button: Button): Promise<IParsedResponse> {
|
||||
const response = await button.endpoint.call(this.#actions, { parse: true });
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import ContinuationItem from '../classes/ContinuationItem.js';
|
||||
import SimpleMenuHeader from '../classes/menus/SimpleMenuHeader.js';
|
||||
@@ -7,9 +6,11 @@ import Notification from '../classes/Notification.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IGetNotificationsMenuResponse } from '../types/ParsedResponse.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
class NotificationsMenu {
|
||||
#page: ParsedResponse;
|
||||
#page: IGetNotificationsMenuResponse;
|
||||
#actions: Actions;
|
||||
|
||||
header: SimpleMenuHeader;
|
||||
@@ -17,14 +18,14 @@ class NotificationsMenu {
|
||||
|
||||
constructor(actions: Actions, response: ApiResponse) {
|
||||
this.#actions = actions;
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IGetNotificationsMenuResponse>(response.data);
|
||||
|
||||
this.header = this.#page.actions_memo.getType(SimpleMenuHeader)?.[0];
|
||||
this.header = this.#page.actions_memo.getType(SimpleMenuHeader).first();
|
||||
this.contents = this.#page.actions_memo.getType(Notification);
|
||||
}
|
||||
|
||||
async getContinuation(): Promise<NotificationsMenu> {
|
||||
const continuation = this.#page.actions_memo.getType(ContinuationItem)?.[0];
|
||||
const continuation = this.#page.actions_memo.getType(ContinuationItem).first();
|
||||
|
||||
if (!continuation)
|
||||
throw new InnertubeError('Continuation not found');
|
||||
@@ -34,7 +35,7 @@ class NotificationsMenu {
|
||||
return new NotificationsMenu(this.#actions, response);
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IGetNotificationsMenuResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import Feed from '../../core/Feed.js';
|
||||
|
||||
import Message from '../classes/Message.js';
|
||||
import Thumbnail from '../classes/misc/Thumbnail.js';
|
||||
import VideoOwner from '../classes/VideoOwner.js';
|
||||
|
||||
import NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
import PlaylistCustomThumbnail from '../classes/PlaylistCustomThumbnail.js';
|
||||
import PlaylistHeader from '../classes/PlaylistHeader.js';
|
||||
import PlaylistMetadata from '../classes/PlaylistMetadata.js';
|
||||
import PlaylistSidebarPrimaryInfo from '../classes/PlaylistSidebarPrimaryInfo.js';
|
||||
import PlaylistSidebarSecondaryInfo from '../classes/PlaylistSidebarSecondaryInfo.js';
|
||||
import PlaylistCustomThumbnail from '../classes/PlaylistCustomThumbnail.js';
|
||||
import PlaylistVideoThumbnail from '../classes/PlaylistVideoThumbnail.js';
|
||||
import PlaylistHeader from '../classes/PlaylistHeader.js';
|
||||
import Message from '../classes/Message.js';
|
||||
import VideoOwner from '../classes/VideoOwner.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import { ObservedArray } from '../helpers.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import { ObservedArray } from '../helpers.js';
|
||||
import NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Playlist extends Feed {
|
||||
class Playlist extends Feed<IBrowseResponse> {
|
||||
info;
|
||||
menu;
|
||||
endpoint?: NavigationEndpoint;
|
||||
messages: ObservedArray<Message>;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const header = this.memo.getType(PlaylistHeader).first();
|
||||
@@ -34,7 +34,7 @@ class Playlist extends Feed {
|
||||
throw new InnertubeError('This playlist does not exist');
|
||||
|
||||
this.info = {
|
||||
...this.page.metadata.item().as(PlaylistMetadata),
|
||||
...this.page.metadata?.item().as(PlaylistMetadata),
|
||||
...{
|
||||
author: secondary_info?.owner.item().as(VideoOwner).author ?? header?.author,
|
||||
thumbnails: primary_info?.thumbnail_renderer.item().as(PlaylistVideoThumbnail, PlaylistCustomThumbnail).thumbnail as Thumbnail[],
|
||||
@@ -63,8 +63,10 @@ class Playlist extends Feed {
|
||||
}
|
||||
|
||||
async getContinuation(): Promise<Playlist> {
|
||||
const response = await this.getContinuationData();
|
||||
return new Playlist(this.actions, response, true);
|
||||
const page = await this.getContinuationData();
|
||||
if (!page)
|
||||
throw new InnertubeError('Could not get continuation data');
|
||||
return new Playlist(this.actions, page, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,31 +4,36 @@ import ItemSection from '../classes/ItemSection.js';
|
||||
import SearchRefinementCard from '../classes/SearchRefinementCard.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import UniversalWatchCard from '../classes/UniversalWatchCard.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import type { ISearchResponse } from '../types/ParsedResponse.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
export default class Search extends Feed {
|
||||
class Search extends Feed<ISearchResponse> {
|
||||
results?: ObservedArray<YTNode> | null;
|
||||
refinements: string[];
|
||||
estimated_results: number | null;
|
||||
watch_card: UniversalWatchCard | null;
|
||||
estimated_results: number;
|
||||
watch_card?: UniversalWatchCard;
|
||||
refinement_cards?: HorizontalCardList | null;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | ISearchResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
|
||||
const contents =
|
||||
this.page.contents_memo.getType(SectionList)?.[0]?.contents ||
|
||||
this.page.on_response_received_commands?.[0].contents;
|
||||
this.page.contents_memo?.getType(SectionList).first().contents ||
|
||||
this.page.on_response_received_commands?.first().contents;
|
||||
|
||||
if (!contents)
|
||||
throw new InnertubeError('No contents found in search response');
|
||||
|
||||
this.results = contents.firstOfType(ItemSection)?.contents;
|
||||
|
||||
this.refinements = this.page.refinements || [];
|
||||
this.estimated_results = this.page.estimated_results;
|
||||
|
||||
this.watch_card = this.page?.contents_memo.getType(UniversalWatchCard)?.[0];
|
||||
this.watch_card = this.page.contents_memo?.getType(UniversalWatchCard).first();
|
||||
this.refinement_cards = this.results?.get({ type: 'HorizontalCardList' }, true)?.as(HorizontalCardList);
|
||||
}
|
||||
|
||||
@@ -49,7 +54,7 @@ export default class Search extends Feed {
|
||||
throw new InnertubeError('Invalid refinement card!');
|
||||
}
|
||||
|
||||
const page = await target_card.endpoint.call(this.actions, { parse: true });
|
||||
const page = await target_card.endpoint.call<ISearchResponse>(this.actions, { parse: true });
|
||||
|
||||
return new Search(this.actions, page, true);
|
||||
}
|
||||
@@ -65,7 +70,11 @@ export default class Search extends Feed {
|
||||
* Retrieves next batch of results.
|
||||
*/
|
||||
async getContinuation(): Promise<Search> {
|
||||
const continuation = await this.getContinuationData();
|
||||
return new Search(this.actions, continuation, true);
|
||||
const response = await this.getContinuationData();
|
||||
if (!response)
|
||||
throw new InnertubeError('Could not get continuation data');
|
||||
return new Search(this.actions, response, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Search;
|
||||
@@ -1,34 +1,37 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import Tab from '../classes/Tab.js';
|
||||
import TwoColumnBrowseResults from '../classes/TwoColumnBrowseResults.js';
|
||||
|
||||
import CompactLink from '../classes/CompactLink.js';
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import PageIntroduction from '../classes/PageIntroduction.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import SettingsOptions from '../classes/SettingsOptions.js';
|
||||
import SettingsSidebar from '../classes/SettingsSidebar.js';
|
||||
import SettingsSwitch from '../classes/SettingsSwitch.js';
|
||||
import Tab from '../classes/Tab.js';
|
||||
import TwoColumnBrowseResults from '../classes/TwoColumnBrowseResults.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Settings {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
#actions: Actions;
|
||||
|
||||
sidebar: SettingsSidebar | null | undefined;
|
||||
introduction: PageIntroduction | null | undefined;
|
||||
sidebar?: SettingsSidebar;
|
||||
introduction?: PageIntroduction;
|
||||
sections;
|
||||
|
||||
constructor(actions: Actions, response: ApiResponse) {
|
||||
this.#actions = actions;
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
|
||||
this.sidebar = this.#page.sidebar?.as(SettingsSidebar);
|
||||
|
||||
if (!this.#page.contents)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
const tab = this.#page.contents.item().as(TwoColumnBrowseResults).tabs.array().as(Tab).get({ selected: true });
|
||||
|
||||
if (!tab)
|
||||
@@ -124,7 +127,7 @@ class Settings {
|
||||
return this.sidebar.items.map((item) => item.title.toString());
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.js';
|
||||
@@ -6,14 +6,18 @@ import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class TimeWatched {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
contents?: ObservedArray<ItemSection>;
|
||||
|
||||
constructor(response: ApiResponse) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
|
||||
if (!this.#page.contents)
|
||||
throw new InnertubeError('Page contents not found');
|
||||
|
||||
const tab = this.#page.contents.item().as(SingleColumnBrowseResults).tabs.get({ selected: true });
|
||||
|
||||
if (!tab)
|
||||
@@ -22,7 +26,7 @@ class TimeWatched {
|
||||
this.contents = tab.content?.as(SectionList).contents.as(ItemSection);
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import Constants from '../../utils/Constants.js';
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
|
||||
import TwoColumnWatchNextResults from '../classes/TwoColumnWatchNextResults.js';
|
||||
import VideoPrimaryInfo from '../classes/VideoPrimaryInfo.js';
|
||||
import VideoSecondaryInfo from '../classes/VideoSecondaryInfo.js';
|
||||
|
||||
import MerchandiseShelf from '../classes/MerchandiseShelf.js';
|
||||
import RelatedChipCloud from '../classes/RelatedChipCloud.js';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import ChipCloud from '../classes/ChipCloud.js';
|
||||
import ChipCloudChip from '../classes/ChipCloudChip.js';
|
||||
@@ -14,11 +7,16 @@ import CommentsEntryPointHeader from '../classes/comments/CommentsEntryPointHead
|
||||
import ContinuationItem from '../classes/ContinuationItem.js';
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import LiveChat from '../classes/LiveChat.js';
|
||||
import MerchandiseShelf from '../classes/MerchandiseShelf.js';
|
||||
import MicroformatData from '../classes/MicroformatData.js';
|
||||
import PlayerMicroformat from '../classes/PlayerMicroformat.js';
|
||||
import PlayerOverlay from '../classes/PlayerOverlay.js';
|
||||
import RelatedChipCloud from '../classes/RelatedChipCloud.js';
|
||||
import SegmentedLikeDislikeButton from '../classes/SegmentedLikeDislikeButton.js';
|
||||
import ToggleButton from '../classes/ToggleButton.js';
|
||||
import TwoColumnWatchNextResults from '../classes/TwoColumnWatchNextResults.js';
|
||||
import VideoPrimaryInfo from '../classes/VideoPrimaryInfo.js';
|
||||
import VideoSecondaryInfo from '../classes/VideoSecondaryInfo.js';
|
||||
import LiveChatWrap from './LiveChat.js';
|
||||
|
||||
import type CardCollection from '../classes/CardCollection.js';
|
||||
@@ -29,17 +27,18 @@ import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js'
|
||||
import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.js';
|
||||
import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.js';
|
||||
|
||||
import type Player from '../../core/Player.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type Player from '../../core/Player.js';
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import type { INextResponse, IPlayerResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
import FormatUtils, { FormatOptions, DownloadOptions, URLTransformer, FormatFilter } from '../../utils/FormatUtils.js';
|
||||
import FormatUtils, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
class VideoInfo {
|
||||
#page: [ParsedResponse, ParsedResponse?];
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
|
||||
#actions: Actions;
|
||||
#player?: Player;
|
||||
@@ -49,11 +48,11 @@ class VideoInfo {
|
||||
basic_info;
|
||||
streaming_data;
|
||||
playability_status;
|
||||
annotations: ObservedArray<PlayerAnnotationsExpanded>;
|
||||
storyboards: PlayerStoryboardSpec | PlayerLiveStoryboardSpec | null;
|
||||
endscreen: Endscreen | null;
|
||||
captions: PlayerCaptionsTracklist | null;
|
||||
cards: CardCollection | null;
|
||||
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
|
||||
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
|
||||
endscreen?: Endscreen;
|
||||
captions?: PlayerCaptionsTracklist;
|
||||
cards?: CardCollection;
|
||||
|
||||
#playback_tracking;
|
||||
|
||||
@@ -77,8 +76,8 @@ class VideoInfo {
|
||||
this.#player = player;
|
||||
this.#cpn = cpn;
|
||||
|
||||
const info = Parser.parseResponse(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse(data[1].data) : undefined;
|
||||
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
|
||||
|
||||
this.#page = [ info, next ];
|
||||
|
||||
@@ -117,7 +116,7 @@ class VideoInfo {
|
||||
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
|
||||
const two_col = next?.contents.item().as(TwoColumnWatchNextResults);
|
||||
const two_col = next?.contents?.item().as(TwoColumnWatchNextResults);
|
||||
|
||||
const results = two_col?.results;
|
||||
const secondary_results = two_col?.secondary_results;
|
||||
@@ -133,7 +132,7 @@ class VideoInfo {
|
||||
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);
|
||||
this.player_overlays = next?.player_overlays?.item().as(PlayerOverlay);
|
||||
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
|
||||
@@ -144,7 +143,7 @@ class VideoInfo {
|
||||
const comments_entry_point = results.get({ target_id: 'comments-entry-point' })?.as(ItemSection);
|
||||
|
||||
this.comments_entry_point_header = comments_entry_point?.contents?.firstOfType(CommentsEntryPointHeader);
|
||||
this.livechat = next?.contents_memo.getType(LiveChat).first();
|
||||
this.livechat = next?.contents_memo?.getType(LiveChat).first();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,7 +393,7 @@ class VideoInfo {
|
||||
/**
|
||||
* Original parsed InnerTube response.
|
||||
*/
|
||||
get page(): [ParsedResponse, ParsedResponse?] {
|
||||
get page(): [IPlayerResponse, INextResponse?] {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import Feed from '../../core/Feed.js';
|
||||
import Actions from '../../core/Actions.js';
|
||||
import C4TabbedHeader from '../classes/C4TabbedHeader.js';
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import { ItemSectionContinuation } from '../index.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
class Channel extends Feed {
|
||||
class Channel extends Feed<IBrowseResponse> {
|
||||
header?: C4TabbedHeader;
|
||||
contents?: ItemSection | ItemSectionContinuation;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.header = this.page.header?.item().as(C4TabbedHeader);
|
||||
this.contents = this.memo.getType(ItemSection).first() || this.page.continuation_contents?.as(ItemSectionContinuation);
|
||||
@@ -23,7 +25,7 @@ class Channel extends Feed {
|
||||
client: 'YTKIDS'
|
||||
});
|
||||
|
||||
return new Channel(this.actions, response.data);
|
||||
return new Channel(this.actions, response);
|
||||
}
|
||||
|
||||
get has_continuation(): boolean {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import Feed from '../../core/Feed.js';
|
||||
import Actions from '../../core/Actions.js';
|
||||
import KidsCategoriesHeader from '../classes/ytkids/KidsCategoriesHeader.js';
|
||||
import KidsCategoryTab from '../classes/ytkids/KidsCategoryTab.js';
|
||||
import KidsHomeScreen from '../classes/ytkids/KidsHomeScreen.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
class HomeFeed extends Feed {
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class HomeFeed extends Feed<IBrowseResponse> {
|
||||
header?: KidsCategoriesHeader;
|
||||
contents?: KidsHomeScreen;
|
||||
|
||||
constructor(actions: Actions, data: any, already_parsed = false) {
|
||||
constructor(actions: Actions, data: ApiResponse | IBrowseResponse, already_parsed = false) {
|
||||
super(actions, data, already_parsed);
|
||||
this.header = this.page.header?.item().as(KidsCategoriesHeader);
|
||||
this.contents = this.page.contents?.item().as(KidsHomeScreen);
|
||||
@@ -31,7 +34,7 @@ class HomeFeed extends Feed {
|
||||
if (!target_tab)
|
||||
throw new InnertubeError(`Tab "${tab}" not found`);
|
||||
|
||||
const page = await target_tab.endpoint.call(this.actions, { client: 'YTKIDS', parse: true });
|
||||
const page = await target_tab.endpoint.call<IBrowseResponse>(this.actions, { client: 'YTKIDS', parse: true });
|
||||
|
||||
// Copy over the header and header memo
|
||||
page.header = this.page.header;
|
||||
|
||||
@@ -3,12 +3,14 @@ import ItemSection from '../classes/ItemSection.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import type { ISearchResponse } from '../types/ParsedResponse.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
class Search extends Feed {
|
||||
estimated_results: number | null;
|
||||
class Search extends Feed<ISearchResponse> {
|
||||
estimated_results: number;
|
||||
contents: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(actions: Actions, data: any) {
|
||||
constructor(actions: Actions, data: ApiResponse | ISearchResponse) {
|
||||
super(actions, data);
|
||||
this.estimated_results = this.page.estimated_results;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
@@ -10,6 +10,7 @@ import type Format from '../classes/misc/Format.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import type { INextResponse, IPlayerResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
import { Constants } from '../../utils/index.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
@@ -17,7 +18,7 @@ import { InnertubeError } from '../../utils/Utils.js';
|
||||
import FormatUtils, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.js';
|
||||
|
||||
class VideoInfo {
|
||||
#page: [ParsedResponse, ParsedResponse?];
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
#actions: Actions;
|
||||
#cpn: string;
|
||||
|
||||
@@ -28,16 +29,16 @@ class VideoInfo {
|
||||
|
||||
#playback_tracking;
|
||||
|
||||
slim_video_metadata?: SlimVideoMetadata | null;
|
||||
watch_next_feed?: ObservedArray<YTNode> | null;
|
||||
current_video_endpoint?: NavigationEndpoint | null;
|
||||
slim_video_metadata?: SlimVideoMetadata;
|
||||
watch_next_feed?: ObservedArray<YTNode>;
|
||||
current_video_endpoint?: NavigationEndpoint;
|
||||
player_overlays?: PlayerOverlay;
|
||||
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
|
||||
this.#actions = actions;
|
||||
|
||||
const info = Parser.parseResponse(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse(data[1].data) : undefined;
|
||||
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
|
||||
|
||||
this.#page = [ info, next ];
|
||||
this.#cpn = cpn;
|
||||
@@ -53,7 +54,7 @@ class VideoInfo {
|
||||
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
|
||||
const two_col = next?.contents.item().as(TwoColumnWatchNextResults);
|
||||
const two_col = next?.contents?.item().as(TwoColumnWatchNextResults);
|
||||
|
||||
const results = two_col?.results;
|
||||
const secondary_results = two_col?.secondary_results;
|
||||
@@ -62,7 +63,7 @@ class VideoInfo {
|
||||
this.slim_video_metadata = results.firstOfType(ItemSection)?.contents?.firstOfType(SlimVideoMetadata);
|
||||
this.watch_next_feed = secondary_results.firstOfType(ItemSection)?.contents || secondary_results;
|
||||
this.current_video_endpoint = next?.current_video_endpoint;
|
||||
this.player_overlays = next?.player_overlays.item().as(PlayerOverlay);
|
||||
this.player_overlays = next?.player_overlays?.item().as(PlayerOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +131,7 @@ class VideoInfo {
|
||||
return this.#cpn;
|
||||
}
|
||||
|
||||
get page(): [ParsedResponse, ParsedResponse?] {
|
||||
get page(): [IPlayerResponse, INextResponse?] {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import MicroformatData from '../classes/MicroformatData.js';
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
|
||||
@@ -9,29 +8,31 @@ import MusicDetailHeader from '../classes/MusicDetailHeader.js';
|
||||
import MusicShelf from '../classes/MusicShelf.js';
|
||||
|
||||
import type MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Album {
|
||||
#page: ParsedResponse;
|
||||
#actions: Actions;
|
||||
#page: IBrowseResponse;
|
||||
|
||||
header?: MusicDetailHeader | null;
|
||||
header?: MusicDetailHeader;
|
||||
contents: ObservedArray<MusicResponsiveListItem>;
|
||||
sections: ObservedArray<MusicCarouselShelf>;
|
||||
|
||||
url: string | null;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#actions = actions;
|
||||
constructor(response: ApiResponse) {
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
|
||||
this.header = this.#page.header?.item().as(MusicDetailHeader);
|
||||
this.url = this.#page.microformat?.as(MicroformatData).url_canonical || null;
|
||||
|
||||
this.contents = this.#page.contents_memo.getType(MusicShelf)?.[0].contents;
|
||||
if (!this.#page.contents_memo)
|
||||
throw new Error('No contents found in the response');
|
||||
|
||||
this.contents = this.#page.contents_memo.getType(MusicShelf)?.first().contents;
|
||||
this.sections = this.#page.contents_memo.getType(MusicCarouselShelf) || [];
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
@@ -9,27 +9,28 @@ import MusicPlaylistShelf from '../classes/MusicPlaylistShelf.js';
|
||||
import MusicImmersiveHeader from '../classes/MusicImmersiveHeader.js';
|
||||
import MusicVisualHeader from '../classes/MusicVisualHeader.js';
|
||||
import MusicHeader from '../classes/MusicHeader.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Artist {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
#actions: Actions;
|
||||
|
||||
header?: MusicImmersiveHeader | MusicVisualHeader | MusicHeader;
|
||||
sections: (MusicCarouselShelf | MusicShelf)[];
|
||||
|
||||
constructor(response: ApiResponse | ParsedResponse, actions: Actions) {
|
||||
this.#page = Parser.parseResponse((response as ApiResponse).data);
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
this.#actions = actions;
|
||||
|
||||
this.header = this.page.header?.item().as(MusicImmersiveHeader, MusicVisualHeader, MusicHeader);
|
||||
|
||||
const music_shelf = this.#page.contents_memo.getType(MusicShelf) || [];
|
||||
const music_carousel_shelf = this.#page.contents_memo.getType(MusicCarouselShelf) || [];
|
||||
const music_shelf = this.#page.contents_memo?.getType(MusicShelf) || [];
|
||||
const music_carousel_shelf = this.#page.contents_memo?.getType(MusicCarouselShelf) || [];
|
||||
|
||||
this.sections = [ ...music_shelf, ...music_carousel_shelf ];
|
||||
}
|
||||
|
||||
async getAllSongs(): Promise<MusicPlaylistShelf | null> {
|
||||
async getAllSongs(): Promise<MusicPlaylistShelf | undefined> {
|
||||
const music_shelves = this.sections.filter((section) => section.type === 'MusicShelf') as MusicShelf[];
|
||||
|
||||
if (!music_shelves.length)
|
||||
@@ -44,12 +45,12 @@ class Artist {
|
||||
throw new InnertubeError('Target shelf (Songs) did not have an endpoint.');
|
||||
|
||||
const page = await shelf.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
const contents = page.contents_memo.getType(MusicPlaylistShelf)?.[0] || null;
|
||||
const contents = page.contents_memo?.getType(MusicPlaylistShelf)?.first();
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import Grid from '../classes/Grid.js';
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
|
||||
@@ -7,19 +7,20 @@ import SectionList from '../classes/SectionList.js';
|
||||
import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.js';
|
||||
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Explore {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
|
||||
top_buttons: MusicNavigationButton[];
|
||||
sections: ObservedArray<MusicCarouselShelf>;
|
||||
|
||||
constructor(response: ApiResponse) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
|
||||
const tab = this.#page.contents.item().as(SingleColumnBrowseResults).tabs.get({ selected: true });
|
||||
const tab = this.#page.contents?.item().as(SingleColumnBrowseResults).tabs.get({ selected: true });
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not find target tab.');
|
||||
@@ -33,7 +34,7 @@ class Explore {
|
||||
this.sections = section_list.contents.filterType(MusicCarouselShelf);
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
import Parser, { ParsedResponse, SectionListContinuation } from '../index.js';
|
||||
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.js';
|
||||
import Parser, { SectionListContinuation } from '../index.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class HomeFeed {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
#actions: Actions;
|
||||
#continuation?: string;
|
||||
|
||||
sections?: ObservedArray<MusicCarouselShelf>;
|
||||
|
||||
constructor(response: ApiResponse | ParsedResponse, actions: Actions) {
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#actions = actions;
|
||||
this.#page = Parser.parseResponse((response as ApiResponse).data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
|
||||
const tab = this.#page.contents.item().as(SingleColumnBrowseResults).tabs.get({ selected: true });
|
||||
const tab = this.#page.contents?.item().as(SingleColumnBrowseResults).tabs.get({ selected: true });
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Could not get Home tab.');
|
||||
throw new InnertubeError('Could not find Home tab.');
|
||||
|
||||
if (tab.key('content').isNull()) {
|
||||
if (!this.#page.continuation_contents)
|
||||
@@ -58,7 +58,7 @@ class HomeFeed {
|
||||
return !!this.#continuation;
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { ParsedResponse, SectionListContinuation } from '../index.js';
|
||||
import Parser, { GridContinuation, MusicShelfContinuation, SectionListContinuation } from '../index.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
@@ -16,9 +16,10 @@ import MusicMenuItemDivider from '../classes/menus/MusicMenuItemDivider.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Library {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
#actions: Actions;
|
||||
#continuation?: string | null;
|
||||
|
||||
@@ -26,10 +27,10 @@ class Library {
|
||||
contents?: ObservedArray<Grid | MusicShelf>;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
this.#actions = actions;
|
||||
|
||||
const section_list = this.#page.contents_memo.getType(SectionList).first();
|
||||
const section_list = this.#page.contents_memo?.getType(SectionList).first();
|
||||
|
||||
this.header = section_list?.header?.item().as(MusicSideAlignedItem);
|
||||
this.contents = section_list?.contents?.as(Grid, MusicShelf);
|
||||
@@ -44,9 +45,9 @@ class Library {
|
||||
let target_item: MusicMultiSelectMenuItem | undefined;
|
||||
|
||||
if (typeof sort_by === 'string') {
|
||||
const button = this.#page.contents_memo.getType(MusicSortFilterButton)?.[0];
|
||||
const button = this.#page.contents_memo?.getType(MusicSortFilterButton).first();
|
||||
|
||||
const options = button.menu?.options
|
||||
const options = button?.menu?.options
|
||||
.filter(
|
||||
(item: MusicMultiSelectMenuItem | MusicMenuItemDivider) => item instanceof MusicMultiSelectMenuItem
|
||||
) as MusicMultiSelectMenuItem[];
|
||||
@@ -76,7 +77,7 @@ class Library {
|
||||
parse: true
|
||||
});
|
||||
|
||||
const previously_selected_item = this.#page.contents_memo.getType(MusicMultiSelectMenuItem)?.find((item) => item.selected);
|
||||
const previously_selected_item = this.#page.contents_memo?.getType(MusicMultiSelectMenuItem)?.find((item) => item.selected);
|
||||
if (previously_selected_item)
|
||||
previously_selected_item.selected = false;
|
||||
|
||||
@@ -93,10 +94,10 @@ class Library {
|
||||
async applyFilter(filter: string | ChipCloudChip): Promise<Library> {
|
||||
let target_chip: ChipCloudChip | undefined;
|
||||
|
||||
const chip_cloud = this.#page.contents_memo.getType(ChipCloud)?.[0];
|
||||
const chip_cloud = this.#page.contents_memo?.getType(ChipCloud).first();
|
||||
|
||||
if (typeof filter === 'string') {
|
||||
target_chip = chip_cloud.chips.get({ text: filter });
|
||||
target_chip = chip_cloud?.chips.get({ text: filter });
|
||||
|
||||
if (!target_chip)
|
||||
throw new InnertubeError(`Filter "${filter}" not found`, { available_filters: this.filters });
|
||||
@@ -133,16 +134,16 @@ class Library {
|
||||
}
|
||||
|
||||
get sort_options(): string[] {
|
||||
const button = this.#page.contents_memo.getType(MusicSortFilterButton)?.[0];
|
||||
const options = button.menu?.options.filter((item: MusicMultiSelectMenuItem | MusicMenuItemDivider) => item instanceof MusicMultiSelectMenuItem) as MusicMultiSelectMenuItem[];
|
||||
const button = this.#page.contents_memo?.getType(MusicSortFilterButton).first();
|
||||
const options = button?.menu?.options.filter((item: MusicMultiSelectMenuItem | MusicMenuItemDivider) => item instanceof MusicMultiSelectMenuItem) as MusicMultiSelectMenuItem[];
|
||||
return options.map((item) => item.title);
|
||||
}
|
||||
|
||||
get filters(): string[] {
|
||||
return this.#page.contents_memo.getType(ChipCloud)?.first()?.chips.map((chip: ChipCloudChip) => chip.text) || [];
|
||||
return this.#page.contents_memo?.getType(ChipCloud)?.first().chips.map((chip: ChipCloudChip) => chip.text) || [];
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
@@ -152,15 +153,16 @@ class LibraryContinuation {
|
||||
#actions;
|
||||
#continuation;
|
||||
|
||||
contents;
|
||||
contents: GridContinuation | MusicShelfContinuation;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
this.#actions = actions;
|
||||
|
||||
this.contents = this.#page.continuation_contents?.hasKey('contents')
|
||||
? this.#page.continuation_contents?.key('contents').array() :
|
||||
this.#page.continuation_contents?.key('items').array();
|
||||
if (!this.#page.continuation_contents)
|
||||
throw new InnertubeError('No continuation contents found');
|
||||
|
||||
this.contents = this.#page.continuation_contents.as(MusicShelfContinuation, GridContinuation);
|
||||
|
||||
this.#continuation = this.#page.continuation_contents?.key('continuation').isNull()
|
||||
? null : this.#page.continuation_contents?.key('continuation').string();
|
||||
@@ -170,19 +172,19 @@ class LibraryContinuation {
|
||||
if (!this.#continuation)
|
||||
throw new InnertubeError('No continuation available');
|
||||
|
||||
const page = await this.#actions.execute('/browse', {
|
||||
const response = await this.#actions.execute('/browse', {
|
||||
client: 'YTMUSIC',
|
||||
continuation: this.#continuation
|
||||
});
|
||||
|
||||
return new LibraryContinuation(page, this.#actions);
|
||||
return new LibraryContinuation(response, this.#actions);
|
||||
}
|
||||
|
||||
get has_continuation(): boolean {
|
||||
return !!this.#continuation;
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { MusicPlaylistShelfContinuation, ParsedResponse, SectionListContinuation } from '../index.js';
|
||||
import Parser, { MusicPlaylistShelfContinuation, SectionListContinuation } from '../index.js';
|
||||
|
||||
import MusicCarouselShelf from '../classes/MusicCarouselShelf.js';
|
||||
import MusicDetailHeader from '../classes/MusicDetailHeader.js';
|
||||
@@ -12,20 +12,21 @@ import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Playlist {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
#actions: Actions;
|
||||
#continuation: string | null;
|
||||
#last_fetched_suggestions: any;
|
||||
#suggestions_continuation: any;
|
||||
|
||||
header?: MusicDetailHeader | null;
|
||||
items: ObservedArray<YTNode> | null;
|
||||
header?: MusicDetailHeader;
|
||||
items?: ObservedArray<YTNode> | null;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#actions = actions;
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
|
||||
this.#last_fetched_suggestions = null;
|
||||
this.#suggestions_continuation = null;
|
||||
@@ -38,10 +39,10 @@ class Playlist {
|
||||
if (this.#page.header?.item().type === 'MusicEditablePlaylistDetailHeader') {
|
||||
this.header = this.#page.header?.item().as(MusicEditablePlaylistDetailHeader).header.item().as(MusicDetailHeader);
|
||||
} else {
|
||||
this.header = this.#page.header?.item().as(MusicDetailHeader) || null;
|
||||
this.header = this.#page.header?.item().as(MusicDetailHeader);
|
||||
}
|
||||
this.items = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0].contents;
|
||||
this.#continuation = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0].continuation || null;
|
||||
this.items = this.#page.contents_memo?.getType(MusicPlaylistShelf).first().contents || null;
|
||||
this.#continuation = this.#page.contents_memo?.getType(MusicPlaylistShelf).first().continuation || null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ class Playlist {
|
||||
* Retrieves related playlists
|
||||
*/
|
||||
async getRelated(): Promise<MusicCarouselShelf> {
|
||||
let section_continuation = this.#page.contents_memo.getType(SectionList)?.[0].continuation;
|
||||
let section_continuation = this.#page.contents_memo?.getType(SectionList)?.[0].continuation;
|
||||
|
||||
while (section_continuation) {
|
||||
const data = await this.#actions.execute('/browse', {
|
||||
@@ -101,7 +102,7 @@ class Playlist {
|
||||
}
|
||||
|
||||
async #fetchSuggestions(): Promise<{ items: never[] | ObservedArray<MusicResponsiveListItem>, continuation: string | null }> {
|
||||
const continuation = this.#suggestions_continuation || this.#page.contents_memo.get('SectionList')?.[0].as(SectionList).continuation;
|
||||
const continuation = this.#suggestions_continuation || this.#page.contents_memo?.get('SectionList')?.[0].as(SectionList).continuation;
|
||||
|
||||
if (continuation) {
|
||||
const page = await this.#actions.execute('/browse', {
|
||||
@@ -127,7 +128,7 @@ class Playlist {
|
||||
};
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
|
||||
import Parser from '../index.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
@@ -17,16 +16,17 @@ import Tab from '../classes/Tab.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { IBrowseResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
class Recap {
|
||||
#page: ParsedResponse;
|
||||
#page: IBrowseResponse;
|
||||
#actions: Actions;
|
||||
|
||||
header?: HighlightsCarousel | MusicHeader;
|
||||
sections?: ObservedArray<ItemSection | MusicCarouselShelf | Message>;
|
||||
|
||||
constructor(response: ApiResponse, actions: Actions) {
|
||||
this.#page = Parser.parseResponse(response.data);
|
||||
this.#page = Parser.parseResponse<IBrowseResponse>(response.data);
|
||||
this.#actions = actions;
|
||||
|
||||
const header = this.#page.header?.item();
|
||||
@@ -35,7 +35,7 @@ class Recap {
|
||||
this.#page.header?.item().as(MusicElementHeader).element?.model?.item().as(HighlightsCarousel) :
|
||||
this.#page.header?.item().as(MusicHeader);
|
||||
|
||||
const tab = this.#page.contents.item().as(SingleColumnBrowseResults).tabs.firstOfType(Tab);
|
||||
const tab = this.#page.contents?.item().as(SingleColumnBrowseResults).tabs.firstOfType(Tab);
|
||||
|
||||
if (!tab)
|
||||
throw new InnertubeError('Target tab not found');
|
||||
@@ -59,7 +59,7 @@ class Recap {
|
||||
return new Playlist(response, this.#actions);
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
get page(): IBrowseResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,37 @@
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import TabbedSearchResults from '../classes/TabbedSearchResults.js';
|
||||
|
||||
import DidYouMean from '../classes/DidYouMean.js';
|
||||
import MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
|
||||
import MusicShelf from '../classes/MusicShelf.js';
|
||||
import ShowingResultsFor from '../classes/ShowingResultsFor.js';
|
||||
import Parser, { MusicShelfContinuation } from '../index.js';
|
||||
|
||||
import ChipCloud from '../classes/ChipCloud.js';
|
||||
import ChipCloudChip from '../classes/ChipCloudChip.js';
|
||||
import DidYouMean from '../classes/DidYouMean.js';
|
||||
import ItemSection from '../classes/ItemSection.js';
|
||||
import Message from '../classes/Message.js';
|
||||
import MusicHeader from '../classes/MusicHeader.js';
|
||||
import MusicResponsiveListItem from '../classes/MusicResponsiveListItem.js';
|
||||
import MusicShelf from '../classes/MusicShelf.js';
|
||||
import SectionList from '../classes/SectionList.js';
|
||||
import ShowingResultsFor from '../classes/ShowingResultsFor.js';
|
||||
import TabbedSearchResults from '../classes/TabbedSearchResults.js';
|
||||
|
||||
import type { ObservedArray } from '../helpers.js';
|
||||
import type { ISearchResponse } from '../types/ParsedResponse.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
class Search {
|
||||
#page: ParsedResponse;
|
||||
#page: ISearchResponse;
|
||||
#actions: Actions;
|
||||
#continuation?: string | null;
|
||||
#continuation?: string;
|
||||
|
||||
header?: ChipCloud | null;
|
||||
header?: ChipCloud;
|
||||
contents?: ObservedArray<MusicShelf | ItemSection>;
|
||||
|
||||
did_you_mean: DidYouMean | null;
|
||||
showing_results_for: ShowingResultsFor | null;
|
||||
message: Message | null;
|
||||
|
||||
results?: ObservedArray<MusicResponsiveListItem>;
|
||||
sections?: ObservedArray<MusicShelf>;
|
||||
|
||||
constructor(response: ApiResponse | ParsedResponse, actions: Actions, args: { is_continuation?: boolean, is_filtered?: boolean } = {}) {
|
||||
constructor(response: ApiResponse, actions: Actions, is_filtered?: boolean) {
|
||||
this.#actions = actions;
|
||||
this.#page = Parser.parseResponse<ISearchResponse>(response.data);
|
||||
|
||||
this.#page = args.is_continuation ?
|
||||
response as ParsedResponse :
|
||||
Parser.parseResponse((response as ApiResponse).data);
|
||||
if (!this.#page.contents || !this.#page.contents_memo)
|
||||
throw new InnertubeError('Response did not contain any contents.');
|
||||
|
||||
const tab = this.#page.contents.item().as(TabbedSearchResults).tabs.get({ selected: true });
|
||||
|
||||
@@ -50,42 +43,33 @@ class Search {
|
||||
if (!tab_content)
|
||||
throw new InnertubeError('Target tab did not have any content.');
|
||||
|
||||
this.header = tab_content.hasKey('header') ? tab_content.header?.item().as(ChipCloud) : null;
|
||||
this.header = tab_content.header?.item().as(ChipCloud);
|
||||
this.contents = tab_content.contents.as(MusicShelf, ItemSection);
|
||||
|
||||
const shelves = tab_content.contents.as(MusicShelf, ItemSection);
|
||||
const item_section = shelves.firstOfType(ItemSection);
|
||||
|
||||
this.did_you_mean = item_section?.contents?.firstOfType(DidYouMean) || null;
|
||||
this.showing_results_for = item_section?.contents?.firstOfType(ShowingResultsFor) || null;
|
||||
this.message = item_section?.contents?.firstOfType(Message) || null;
|
||||
|
||||
if (args.is_continuation || args.is_filtered) {
|
||||
this.results = shelves.firstOfType(MusicShelf)?.contents;
|
||||
this.#continuation = shelves.firstOfType(MusicShelf)?.continuation;
|
||||
} else {
|
||||
this.sections = shelves.filterType(MusicShelf);
|
||||
if (is_filtered) {
|
||||
this.#continuation = this.contents.firstOfType(MusicShelf)?.continuation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Equivalent to clicking on the shelf to load more items.
|
||||
* Loads more items for the given shelf.
|
||||
*/
|
||||
async getMore(shelf: MusicShelf | undefined): Promise<Search> {
|
||||
if (!shelf || !shelf.endpoint)
|
||||
throw new InnertubeError('Cannot retrieve more items for this shelf because it does not have an endpoint.');
|
||||
|
||||
const response = await shelf.endpoint.call(this.#actions, { parse: true, client: 'YTMUSIC' });
|
||||
const response = await shelf.endpoint.call(this.#actions, { client: 'YTMUSIC' });
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Endpoint did not return any data');
|
||||
|
||||
return new Search(response, this.#actions, { is_continuation: true });
|
||||
return new Search(response, this.#actions, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves continuation, only works for individual sections or filtered results.
|
||||
* Retrieves search continuation. Only available for filtered searches and shelf continuations.
|
||||
*/
|
||||
async getContinuation(): Promise<Search> {
|
||||
async getContinuation(): Promise<SearchContinuation> {
|
||||
if (!this.#continuation)
|
||||
throw new InnertubeError('Continuation not found.');
|
||||
|
||||
@@ -94,18 +78,13 @@ class Search {
|
||||
client: 'YTMUSIC'
|
||||
});
|
||||
|
||||
const data = response.data.continuationContents.musicShelfContinuation;
|
||||
|
||||
this.results = Parser.parse(data.contents).array().as(MusicResponsiveListItem);
|
||||
this.#continuation = data?.continuations?.[0]?.nextContinuationData?.continuation;
|
||||
|
||||
return this;
|
||||
return new SearchContinuation(this.#actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies given filter to the search.
|
||||
*/
|
||||
async selectFilter(target_filter: string | ChipCloudChip): Promise<Search> {
|
||||
async applyFilter(target_filter: string | ChipCloudChip): Promise<Search> {
|
||||
let cloud_chip: ChipCloudChip | undefined;
|
||||
|
||||
if (typeof target_filter === 'string') {
|
||||
@@ -114,51 +93,111 @@ class Search {
|
||||
throw new InnertubeError('Could not find filter with given name.', { available_filters: this.filters });
|
||||
} else if (target_filter?.is(ChipCloudChip)) {
|
||||
cloud_chip = target_filter;
|
||||
} else {
|
||||
throw new InnertubeError('Invalid filter', { available_filters: this.filters });
|
||||
}
|
||||
|
||||
if (!cloud_chip)
|
||||
throw new InnertubeError('Invalid filter', { available_filters: this.filters });
|
||||
|
||||
if (cloud_chip?.is_selected) return this;
|
||||
|
||||
const response = await cloud_chip?.endpoint?.call(this.#actions, { parse: true, client: 'YTMUSIC' });
|
||||
if (!cloud_chip.endpoint)
|
||||
throw new InnertubeError('Selected filter does not have an endpoint.');
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Endpoint did not return any data');
|
||||
|
||||
return new Search(response, this.#actions, { is_continuation: true });
|
||||
}
|
||||
|
||||
get has_continuation(): boolean {
|
||||
return !!this.#continuation;
|
||||
const response = await cloud_chip.endpoint.call(this.#actions, { client: 'YTMUSIC' });
|
||||
return new Search(response, this.#actions, true);
|
||||
}
|
||||
|
||||
get filters(): string[] {
|
||||
return this.header?.chips?.as(ChipCloudChip).map((chip) => chip.text) || [];
|
||||
}
|
||||
|
||||
get has_continuation(): boolean {
|
||||
return !!this.#continuation;
|
||||
}
|
||||
|
||||
get did_you_mean(): DidYouMean | undefined {
|
||||
return this.#page.contents_memo?.getType(DidYouMean).first();
|
||||
}
|
||||
|
||||
get showing_results_for(): ShowingResultsFor | undefined {
|
||||
return this.#page.contents_memo?.getType(ShowingResultsFor).first();
|
||||
}
|
||||
|
||||
get message(): Message | undefined {
|
||||
return this.#page.contents_memo?.getType(Message).first();
|
||||
}
|
||||
|
||||
get songs(): MusicShelf | undefined {
|
||||
return this.sections?.find((section) => section.title.toString() === 'Songs');
|
||||
return this.contents?.filterType(MusicShelf).find((section) => section.title.toString() === 'Songs');
|
||||
}
|
||||
|
||||
get videos(): MusicShelf | undefined {
|
||||
return this.sections?.find((section) => section.title.toString() === 'Videos');
|
||||
return this.contents?.filterType(MusicShelf).find((section) => section.title.toString() === 'Videos');
|
||||
}
|
||||
|
||||
get albums(): MusicShelf | undefined {
|
||||
return this.sections?.find((section) => section.title.toString() === 'Albums');
|
||||
return this.contents?.filterType(MusicShelf).find((section) => section.title.toString() === 'Albums');
|
||||
}
|
||||
|
||||
get artists(): MusicShelf | undefined {
|
||||
return this.sections?.find((section) => section.title.toString() === 'Artists');
|
||||
return this.contents?.filterType(MusicShelf).find((section) => section.title.toString() === 'Artists');
|
||||
}
|
||||
|
||||
get playlists(): MusicShelf | undefined {
|
||||
return this.sections?.find((section) => section.title.toString() === 'Community playlists');
|
||||
return this.contents?.filterType(MusicShelf).find((section) => section.title.toString() === 'Community playlists');
|
||||
}
|
||||
|
||||
get page(): ParsedResponse {
|
||||
/**
|
||||
* @deprecated Use {@link Search.contents} instead.
|
||||
*/
|
||||
get results(): ObservedArray<MusicResponsiveListItem> | undefined {
|
||||
return this.contents?.firstOfType(MusicShelf)?.contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link Search.contents} instead.
|
||||
*/
|
||||
get sections(): ObservedArray<MusicShelf> | undefined {
|
||||
return this.contents?.filterType(MusicShelf);
|
||||
}
|
||||
|
||||
get page(): ISearchResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
export default Search;
|
||||
export default Search;
|
||||
|
||||
export class SearchContinuation {
|
||||
#actions: Actions;
|
||||
#page: ISearchResponse;
|
||||
header?: MusicHeader;
|
||||
contents?: MusicShelfContinuation;
|
||||
|
||||
constructor(actions: Actions, response: ApiResponse) {
|
||||
this.#actions = actions;
|
||||
this.#page = Parser.parseResponse<ISearchResponse>(response.data);
|
||||
this.header = this.#page.header?.item().as(MusicHeader);
|
||||
this.contents = this.#page.continuation_contents?.as(MusicShelfContinuation);
|
||||
}
|
||||
|
||||
async getContinuation(): Promise<SearchContinuation> {
|
||||
if (!this.contents?.continuation)
|
||||
throw new InnertubeError('Continuation not found.');
|
||||
|
||||
const response = await this.#actions.execute('/search', {
|
||||
continuation: this.contents.continuation,
|
||||
client: 'YTMUSIC'
|
||||
});
|
||||
|
||||
return new SearchContinuation(this.#actions, response);
|
||||
}
|
||||
|
||||
get has_continuation(): boolean {
|
||||
return !!this.contents?.continuation;
|
||||
}
|
||||
|
||||
get page(): ISearchResponse {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parser, { ParsedResponse } from '../index.js';
|
||||
import Parser from '../index.js';
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { ApiResponse } from '../../core/Actions.js';
|
||||
|
||||
@@ -19,36 +19,37 @@ import SectionList from '../classes/SectionList.js';
|
||||
import Tab from '../classes/Tab.js';
|
||||
import WatchNextTabbedResults from '../classes/WatchNextTabbedResults.js';
|
||||
|
||||
import type Format from '../classes/misc/Format.js';
|
||||
import type NavigationEndpoint from '../classes/NavigationEndpoint.js';
|
||||
import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.js';
|
||||
import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.js';
|
||||
import type Format from '../classes/misc/Format.js';
|
||||
|
||||
import type { ObservedArray, YTNode } from '../helpers.js';
|
||||
import FormatUtils, { URLTransformer, FormatOptions, DownloadOptions, FormatFilter } from '../../utils/FormatUtils.js';
|
||||
import type { INextResponse, IPlayerResponse } from '../types/ParsedResponse.js';
|
||||
|
||||
import FormatUtils, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.js';
|
||||
|
||||
class TrackInfo {
|
||||
#page: [ ParsedResponse, ParsedResponse? ];
|
||||
#page: [ IPlayerResponse, INextResponse? ];
|
||||
#actions: Actions;
|
||||
#cpn: string;
|
||||
|
||||
basic_info;
|
||||
streaming_data;
|
||||
playability_status;
|
||||
storyboards: PlayerStoryboardSpec | PlayerLiveStoryboardSpec | null;
|
||||
endscreen: Endscreen | null;
|
||||
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
|
||||
endscreen?: Endscreen;
|
||||
|
||||
#playback_tracking;
|
||||
|
||||
tabs?: ObservedArray<Tab>;
|
||||
current_video_endpoint?: NavigationEndpoint | null;
|
||||
current_video_endpoint?: NavigationEndpoint;
|
||||
player_overlays?: PlayerOverlay;
|
||||
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
|
||||
this.#actions = actions;
|
||||
|
||||
const info = Parser.parseResponse(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse(data[1].data) : undefined;
|
||||
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
|
||||
|
||||
this.#page = [ info, next ];
|
||||
this.#cpn = cpn;
|
||||
@@ -78,13 +79,13 @@ class TrackInfo {
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
|
||||
if (next) {
|
||||
const tabbed_results = next.contents_memo.getType(WatchNextTabbedResults)?.[0];
|
||||
const tabbed_results = next.contents_memo?.getType(WatchNextTabbedResults)?.[0];
|
||||
|
||||
this.tabs = tabbed_results.tabs.array().as(Tab);
|
||||
this.tabs = tabbed_results?.tabs.array().as(Tab);
|
||||
this.current_video_endpoint = next.current_video_endpoint;
|
||||
|
||||
// TODO: update PlayerOverlay, YTMusic's is a little bit different.
|
||||
this.player_overlays = next.player_overlays.item().as(PlayerOverlay);
|
||||
this.player_overlays = next.player_overlays?.item().as(PlayerOverlay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,9 +135,12 @@ class TrackInfo {
|
||||
|
||||
const page = await target_tab.endpoint.call(this.#actions, { client: 'YTMUSIC', parse: true });
|
||||
|
||||
if (page.contents.item().key('type').string() === 'Message')
|
||||
if (page.contents?.item().key('type').string() === 'Message')
|
||||
return page.contents.item().as(Message);
|
||||
|
||||
if (!page.contents)
|
||||
throw new InnertubeError('Page contents was empty', page);
|
||||
|
||||
return page.contents.item().as(SectionList).contents;
|
||||
}
|
||||
|
||||
@@ -163,7 +167,7 @@ class TrackInfo {
|
||||
parse: true
|
||||
});
|
||||
|
||||
if (!page)
|
||||
if (!page || !page.contents_memo)
|
||||
throw new InnertubeError('Could not fetch automix');
|
||||
|
||||
return page.contents_memo.getType(PlaylistPanel)?.[0];
|
||||
@@ -216,7 +220,7 @@ class TrackInfo {
|
||||
return this.tabs ? this.tabs.map((tab) => tab.title) : [];
|
||||
}
|
||||
|
||||
get page(): [ParsedResponse, ParsedResponse?] {
|
||||
get page(): [IPlayerResponse, INextResponse?] {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,10 +129,11 @@ export function timeToSeconds(time: string): number {
|
||||
}
|
||||
}
|
||||
|
||||
export function concatMemos(...iterables: Memo[]): Memo {
|
||||
export function concatMemos(...iterables: Array<Memo | undefined>): Memo {
|
||||
const memo = new Memo();
|
||||
|
||||
for (const iterable of iterables) {
|
||||
if (!iterable) continue;
|
||||
for (const item of iterable) {
|
||||
memo.set(...item);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user