mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-28 00:56:23 +00:00
refactor(Parser)!: general refactoring of parsers (#344)
* refactor: move common info into MediaInfo * refactor: better inference on Memo * refactor: improved typesafety in parser methods * refactor: remove PlaylistAuthor in favor of Author * refactor: cleanup live chat parsers - Replace non standard author type with Author class - Remove redundant code * fix: new errors due to changes * fix: pass actions to FormatUtils#toDash * refactor!: merge NavigatableText and Text into single class
This commit is contained in:
@@ -36,6 +36,7 @@ import ItemMenu from './ItemMenu.js';
|
||||
|
||||
import type Actions from '../../core/Actions.js';
|
||||
import type { IParsedResponse, IUpdatedMetadataResponse } from '../types/ParsedResponse.js';
|
||||
import { NavigationEndpoint } from '../nodes.js';
|
||||
|
||||
export type ChatAction =
|
||||
AddChatItemAction | AddBannerToLiveChatCommand | AddLiveChatTickerItemAction |
|
||||
@@ -288,10 +289,10 @@ class LiveChat extends EventEmitter {
|
||||
* Retrieves given chat item's menu.
|
||||
*/
|
||||
async getItemMenu(item: ChatItemWithMenu): Promise<ItemMenu> {
|
||||
if (!item.menu_endpoint)
|
||||
if (!item.hasKey('menu_endpoint') || !item.key('menu_endpoint').isInstanceof(NavigationEndpoint))
|
||||
throw new InnertubeError('This item does not have a menu.', item);
|
||||
|
||||
const response = await item.menu_endpoint.call(this.#actions, { parse: true });
|
||||
const response = await item.key('menu_endpoint').instanceof(NavigationEndpoint).call(this.#actions, { parse: true });
|
||||
|
||||
if (!response)
|
||||
throw new InnertubeError('Could not retrieve item menu.', item);
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import Constants from '../../utils/Constants.js';
|
||||
import Parser from '../index.js';
|
||||
|
||||
import ChipCloud from '../classes/ChipCloud.js';
|
||||
import ChipCloudChip from '../classes/ChipCloudChip.js';
|
||||
import CommentsEntryPointHeader from '../classes/comments/CommentsEntryPointHeader.js';
|
||||
@@ -25,7 +22,6 @@ import PlayerLegacyDesktopYpcTrailer from '../classes/PlayerLegacyDesktopYpcTrai
|
||||
|
||||
import type CardCollection from '../classes/CardCollection.js';
|
||||
import type Endscreen from '../classes/Endscreen.js';
|
||||
import type Format from '../classes/misc/Format.js';
|
||||
import type PlayerAnnotationsExpanded from '../classes/PlayerAnnotationsExpanded.js';
|
||||
import type PlayerCaptionsTracklist from '../classes/PlayerCaptionsTracklist.js';
|
||||
import type PlayerLiveStoryboardSpec from '../classes/PlayerLiveStoryboardSpec.js';
|
||||
@@ -33,33 +29,21 @@ import type PlayerStoryboardSpec from '../classes/PlayerStoryboardSpec.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, { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } from '../../utils/FormatUtils.js';
|
||||
import { ObservedArray, YTNode } from '../helpers.js';
|
||||
|
||||
import { InnertubeError } from '../../utils/Utils.js';
|
||||
import { MediaInfo } from '../../core/MediaInfo.js';
|
||||
|
||||
class VideoInfo {
|
||||
#page: [IPlayerResponse, INextResponse?];
|
||||
|
||||
#actions: Actions;
|
||||
#player?: Player;
|
||||
#cpn?: string;
|
||||
class VideoInfo extends MediaInfo {
|
||||
#watch_next_continuation?: ContinuationItem;
|
||||
|
||||
basic_info;
|
||||
streaming_data;
|
||||
playability_status;
|
||||
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
|
||||
storyboards?: PlayerStoryboardSpec | PlayerLiveStoryboardSpec;
|
||||
endscreen?: Endscreen;
|
||||
captions?: PlayerCaptionsTracklist;
|
||||
cards?: CardCollection;
|
||||
|
||||
#playback_tracking;
|
||||
|
||||
primary_info?: VideoPrimaryInfo | null;
|
||||
secondary_info?: VideoSecondaryInfo | null;
|
||||
playlist?;
|
||||
@@ -78,18 +62,10 @@ class VideoInfo {
|
||||
* @param player - Player instance.
|
||||
* @param cpn - Client Playback Nonce.
|
||||
*/
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, player?: Player, cpn?: string) {
|
||||
this.#actions = actions;
|
||||
this.#player = player;
|
||||
this.#cpn = cpn;
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
|
||||
super(data, actions, cpn);
|
||||
|
||||
const info = Parser.parseResponse<IPlayerResponse>(data[0].data);
|
||||
const next = data?.[1]?.data ? Parser.parseResponse<INextResponse>(data[1].data) : undefined;
|
||||
|
||||
this.#page = [ info, next ];
|
||||
|
||||
if (info.playability_status?.status === 'ERROR')
|
||||
throw new InnertubeError('This video is unavailable', info.playability_status);
|
||||
const [ info, next ] = this.page;
|
||||
|
||||
if (info.microformat && !info.microformat?.is(PlayerMicroformat, MicroformatData))
|
||||
throw new InnertubeError('Invalid microformat', info.microformat);
|
||||
@@ -114,16 +90,12 @@ class VideoInfo {
|
||||
is_disliked: undefined as boolean | undefined
|
||||
};
|
||||
|
||||
this.streaming_data = info.streaming_data;
|
||||
this.playability_status = info.playability_status;
|
||||
this.annotations = info.annotations;
|
||||
this.storyboards = info.storyboards;
|
||||
this.endscreen = info.endscreen;
|
||||
this.captions = info.captions;
|
||||
this.cards = info.cards;
|
||||
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
|
||||
const two_col = next?.contents?.item().as(TwoColumnWatchNextResults);
|
||||
|
||||
const results = two_col?.results;
|
||||
@@ -200,7 +172,7 @@ class VideoInfo {
|
||||
|
||||
if (cloud_chip.is_selected) return this;
|
||||
|
||||
const response = await cloud_chip.endpoint?.call(this.#actions, { parse: true });
|
||||
const response = await cloud_chip.endpoint?.call(this.actions, { parse: true });
|
||||
const data = response?.on_response_received_endpoints?.get({ target_id: 'watch-next-feed' });
|
||||
|
||||
this.watch_next_feed = data?.contents;
|
||||
@@ -212,24 +184,7 @@ class VideoInfo {
|
||||
* Adds video to the watch history.
|
||||
*/
|
||||
async addToWatchHistory(): Promise<Response> {
|
||||
if (!this.#playback_tracking)
|
||||
throw new InnertubeError('Playback tracking not available');
|
||||
|
||||
const url_params = {
|
||||
cpn: this.#cpn,
|
||||
fmt: 251,
|
||||
rtn: 0,
|
||||
rt: 0
|
||||
};
|
||||
|
||||
const url = this.#playback_tracking.videostats_playback_url.replace('https://s.', 'https://www.');
|
||||
|
||||
const response = await this.#actions.stats(url, {
|
||||
client_name: Constants.CLIENTS.WEB.NAME,
|
||||
client_version: Constants.CLIENTS.WEB.VERSION
|
||||
}, url_params);
|
||||
|
||||
return response;
|
||||
return super.addToWatchHistory();
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +195,7 @@ class VideoInfo {
|
||||
if (!this.#watch_next_continuation)
|
||||
throw new InnertubeError('Watch next feed continuation not found');
|
||||
|
||||
const response = await this.#watch_next_continuation?.endpoint.call(this.#actions, { parse: true });
|
||||
const response = await this.#watch_next_continuation?.endpoint.call(this.actions, { parse: true });
|
||||
const data = response?.on_response_received_endpoints?.get({ type: 'appendContinuationItemsAction' });
|
||||
|
||||
if (!data)
|
||||
@@ -272,7 +227,7 @@ class VideoInfo {
|
||||
if (button.is_toggled)
|
||||
throw new InnertubeError('This video is already liked', { video_id: this.basic_info.id });
|
||||
|
||||
const response = await button.endpoint.call(this.#actions);
|
||||
const response = await button.endpoint.call(this.actions);
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -293,7 +248,7 @@ class VideoInfo {
|
||||
if (button.is_toggled)
|
||||
throw new InnertubeError('This video is already disliked', { video_id: this.basic_info.id });
|
||||
|
||||
const response = await button.endpoint.call(this.#actions);
|
||||
const response = await button.endpoint.call(this.actions);
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -321,7 +276,7 @@ class VideoInfo {
|
||||
if (!button)
|
||||
throw new InnertubeError('This video is not liked/disliked', { video_id: this.basic_info.id });
|
||||
|
||||
const response = await button.toggled_endpoint.call(this.#actions);
|
||||
const response = await button.toggled_endpoint.call(this.actions);
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -343,38 +298,12 @@ class VideoInfo {
|
||||
if (this.has_trailer) {
|
||||
const player_response = this.playability_status.error_screen?.as(PlayerLegacyDesktopYpcTrailer).trailer?.player_response;
|
||||
if (player_response) {
|
||||
return new VideoInfo([ { data: player_response } as ApiResponse ], this.#actions, this.#player, this.#cpn);
|
||||
return new VideoInfo([ { data: player_response } as ApiResponse ], this.actions, this.cpn);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the format that best matches the given options.
|
||||
* @param options - Options
|
||||
*/
|
||||
chooseFormat(options: FormatOptions): Format {
|
||||
return FormatUtils.chooseFormat(options, this.streaming_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a DASH manifest from the streaming data.
|
||||
* @param url_transformer - Function to transform the URLs.
|
||||
* @param format_filter - Function to filter the formats.
|
||||
* @returns DASH manifest
|
||||
*/
|
||||
async toDash(url_transformer?: URLTransformer, format_filter?: FormatFilter): Promise<string> {
|
||||
return await FormatUtils.toDash(this.streaming_data, url_transformer, format_filter, this.#cpn, this.#player, this.#actions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the video.
|
||||
* @param options - Download options.
|
||||
*/
|
||||
async download(options: DownloadOptions = {}): Promise<ReadableStream<Uint8Array>> {
|
||||
return FormatUtils.download(options, this.#actions, this.playability_status, this.streaming_data, this.#actions.session.player, this.cpn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch next feed filters.
|
||||
*/
|
||||
@@ -382,20 +311,6 @@ class VideoInfo {
|
||||
return this.related_chip_cloud?.chips?.map((chip) => chip.text?.toString()) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions instance.
|
||||
*/
|
||||
get actions(): Actions {
|
||||
return this.#actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Playback Nonce.
|
||||
*/
|
||||
get cpn(): string | undefined {
|
||||
return this.#cpn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if continuation is available for the watch next feed.
|
||||
*/
|
||||
@@ -455,13 +370,6 @@ class VideoInfo {
|
||||
*/
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Original parsed InnerTube response.
|
||||
*/
|
||||
get page(): [IPlayerResponse, INextResponse?] {
|
||||
return this.#page;
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoInfo;
|
||||
Reference in New Issue
Block a user