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:
Daniel Wykerd
2023-03-15 23:25:12 +02:00
committed by GitHub
parent 3d3436472f
commit b13bf6e992
78 changed files with 400 additions and 737 deletions

View File

@@ -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);

View File

@@ -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;