mirror of
https://github.com/LuanRT/YouTube.js.git
synced 2026-06-15 10:32:14 +00:00
chore: v8.1.0 release
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "youtubei.js",
|
||||
"version": "8.0.0",
|
||||
"version": "8.1.0",
|
||||
"description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).",
|
||||
"type": "module",
|
||||
"types": "./dist/src/platform/lib.d.ts",
|
||||
|
||||
@@ -280,6 +280,16 @@ export default class Innertube {
|
||||
return new Feed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves channels feed.
|
||||
*/
|
||||
async getChannelsFeed(): Promise<Feed<IBrowseResponse>> {
|
||||
const response = await this.actions.execute(
|
||||
BrowseEndpoint.PATH, { ...BrowseEndpoint.build({ browse_id: 'FEchannels' }), parse: true }
|
||||
);
|
||||
return new Feed(this.actions, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves contents for a given channel.
|
||||
* @param id - Channel id
|
||||
|
||||
@@ -167,6 +167,7 @@ export default class Actions {
|
||||
'FElibrary',
|
||||
'FEhistory',
|
||||
'FEsubscriptions',
|
||||
'FEchannels',
|
||||
'FEmusic_listening_review',
|
||||
'FEmusic_library_landing',
|
||||
'SPaccount_overview',
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { DownloadOptions, FormatFilter, FormatOptions, URLTransformer } fro
|
||||
import * as FormatUtils from '../../utils/FormatUtils.ts';
|
||||
import { InnertubeError } from '../../utils/Utils.ts';
|
||||
import type Format from '../../parser/classes/misc/Format.ts';
|
||||
import type { INextResponse, IPlayerResponse } from '../../parser/index.ts';
|
||||
import type { INextResponse, IPlayerConfig, IPlayerResponse } from '../../parser/index.ts';
|
||||
import { Parser } from '../../parser/index.ts';
|
||||
import type { DashOptions } from '../../types/DashOptions.ts';
|
||||
import PlayerStoryboardSpec from '../../parser/classes/PlayerStoryboardSpec.ts';
|
||||
@@ -20,6 +20,7 @@ export default class MediaInfo {
|
||||
#playback_tracking;
|
||||
streaming_data;
|
||||
playability_status;
|
||||
player_config: IPlayerConfig;
|
||||
|
||||
constructor(data: [ApiResponse, ApiResponse?], actions: Actions, cpn: string) {
|
||||
this.#actions = actions;
|
||||
@@ -35,6 +36,7 @@ export default class MediaInfo {
|
||||
|
||||
this.streaming_data = info.streaming_data;
|
||||
this.playability_status = info.playability_status;
|
||||
this.player_config = info.player_config;
|
||||
this.#playback_tracking = info.playback_tracking;
|
||||
}
|
||||
|
||||
|
||||
30
deno/src/parser/classes/AvatarView.ts
Normal file
30
deno/src/parser/classes/AvatarView.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { type RawNode } from '../index.ts';
|
||||
import { Thumbnail } from '../misc.ts';
|
||||
|
||||
export default class AvatarView extends YTNode {
|
||||
static type = 'AvatarView';
|
||||
|
||||
image: {
|
||||
sources: Thumbnail[],
|
||||
processor: {
|
||||
border_image_processor: {
|
||||
circular: boolean
|
||||
}
|
||||
}
|
||||
};
|
||||
avatar_image_size: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.image = {
|
||||
sources: data.image.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width),
|
||||
processor: {
|
||||
border_image_processor: {
|
||||
circular: data.image.processor.borderImageProcessor.circular
|
||||
}
|
||||
}
|
||||
};
|
||||
this.avatar_image_size = data.avatarImageSize;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,6 @@ export default class ChannelExternalLinkView extends YTNode {
|
||||
|
||||
this.title = Text.fromAttributed(data.title);
|
||||
this.link = Text.fromAttributed(data.link);
|
||||
this.favicon = data.favicon.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width);
|
||||
this.favicon = Thumbnail.fromResponse(data.favicon);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export default class ContentPreviewImageView extends YTNode {
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.image = data.image.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width);
|
||||
this.image = Thumbnail.fromResponse(data.image);
|
||||
this.style = data.style;
|
||||
}
|
||||
}
|
||||
19
deno/src/parser/classes/DecoratedAvatarView.ts
Normal file
19
deno/src/parser/classes/DecoratedAvatarView.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
import AvatarView from './AvatarView.ts';
|
||||
|
||||
export default class DecoratedAvatarView extends YTNode {
|
||||
static type = 'DecoratedAvatarView';
|
||||
|
||||
avatar: AvatarView;
|
||||
a11y_label: string;
|
||||
on_tap_endpoint: NavigationEndpoint;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.avatar = new AvatarView(data.avatar.avatarViewModel);
|
||||
this.a11y_label = data.a11yLabel;
|
||||
this.on_tap_endpoint = new NavigationEndpoint(data.rendererContext.commandContext.onTap.innertubeCommand);
|
||||
}
|
||||
}
|
||||
16
deno/src/parser/classes/DislikeButtonView.ts
Normal file
16
deno/src/parser/classes/DislikeButtonView.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import ToggleButtonView from './ToggleButtonView.ts';
|
||||
|
||||
export default class DislikeButtonView extends YTNode {
|
||||
static type = 'DislikeButtonView';
|
||||
|
||||
toggle_button: ToggleButtonView | null;
|
||||
dislike_entity_key: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.toggle_button = Parser.parseItem(data.toggleButtonViewModel, ToggleButtonView);
|
||||
this.dislike_entity_key = data.dislikeEntityKey;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { NavigationEndpoint } from '../nodes.ts';
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
import Text from './misc/Text.ts';
|
||||
|
||||
import type { RawNode } from '../types/index.ts';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
import Thumbnail from './misc/Thumbnail.ts';
|
||||
import { YTNode, observe } from '../helpers.ts';
|
||||
import { type RawNode } from '../index.ts';
|
||||
|
||||
@@ -6,11 +7,7 @@ export class Panel extends YTNode {
|
||||
static type = 'Panel';
|
||||
|
||||
thumbnail?: {
|
||||
image: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}[];
|
||||
image: Thumbnail[];
|
||||
endpoint: NavigationEndpoint;
|
||||
on_long_press_endpoint: NavigationEndpoint;
|
||||
content_mode: string;
|
||||
@@ -18,16 +15,8 @@ export class Panel extends YTNode {
|
||||
};
|
||||
|
||||
background_image: {
|
||||
image: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}[];
|
||||
gradient_image: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}[];
|
||||
image: Thumbnail[];
|
||||
gradient_image: Thumbnail[];
|
||||
};
|
||||
|
||||
strapline: string;
|
||||
@@ -48,7 +37,7 @@ export class Panel extends YTNode {
|
||||
|
||||
if (data.thumbnail) {
|
||||
this.thumbnail = {
|
||||
image: data.thumbnail.image.sources,
|
||||
image: Thumbnail.fromResponse(data.thumbnail.image),
|
||||
endpoint: new NavigationEndpoint(data.thumbnail.onTap),
|
||||
on_long_press_endpoint: new NavigationEndpoint(data.thumbnail.onLongPress),
|
||||
content_mode: data.thumbnail.contentMode,
|
||||
@@ -57,8 +46,8 @@ export class Panel extends YTNode {
|
||||
}
|
||||
|
||||
this.background_image = {
|
||||
image: data.backgroundImage.image.sources,
|
||||
gradient_image: data.backgroundImage.gradientImage.sources
|
||||
image: Thumbnail.fromResponse(data.backgroundImage.image),
|
||||
gradient_image: Thumbnail.fromResponse(data.backgroundImage.gradientImage)
|
||||
};
|
||||
|
||||
this.strapline = data.strapline;
|
||||
|
||||
24
deno/src/parser/classes/LikeButtonView.ts
Normal file
24
deno/src/parser/classes/LikeButtonView.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import ToggleButtonView from './ToggleButtonView.ts';
|
||||
|
||||
export default class LikeButtonView extends YTNode {
|
||||
static type = 'LikeButtonView';
|
||||
|
||||
toggle_button: ToggleButtonView | null;
|
||||
like_status_entity_key: string;
|
||||
like_status_entity: {
|
||||
key: string,
|
||||
like_status: string
|
||||
};
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.toggle_button = Parser.parseItem(data.toggleButtonViewModel, ToggleButtonView);
|
||||
this.like_status_entity_key = data.likeStatusEntityKey;
|
||||
this.like_status_entity = {
|
||||
key: data.likeStatusEntity.key,
|
||||
like_status: data.likeStatusEntity.likeStatus
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import NavigationEndpoint from './NavigationEndpoint.ts';
|
||||
import Thumbnail from './misc/Thumbnail.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
|
||||
@@ -21,11 +22,7 @@ class ActionButton {
|
||||
class Panel {
|
||||
static type = 'Panel';
|
||||
|
||||
image: {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}[];
|
||||
image: Thumbnail[];
|
||||
|
||||
content_mode: string;
|
||||
crop_options: string;
|
||||
@@ -34,7 +31,7 @@ class Panel {
|
||||
action_buttons: ActionButton[];
|
||||
|
||||
constructor (data: RawNode) {
|
||||
this.image = data.image.image.sources;
|
||||
this.image = Thumbnail.fromResponse(data.image.image);
|
||||
this.content_mode = data.image.contentMode;
|
||||
this.crop_options = data.image.cropOptions;
|
||||
this.image_aspect_ratio = data.imageAspectRatio;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import ContentMetadataView from './ContentMetadataView.ts';
|
||||
import ContentPreviewImageView from './ContentPreviewImageView.ts';
|
||||
import DecoratedAvatarView from './DecoratedAvatarView.ts';
|
||||
import DynamicTextView from './DynamicTextView.ts';
|
||||
import FlexibleActionsView from './FlexibleActionsView.ts';
|
||||
|
||||
@@ -9,14 +10,14 @@ export default class PageHeaderView extends YTNode {
|
||||
static type = 'PageHeaderView';
|
||||
|
||||
title: DynamicTextView | null;
|
||||
image: ContentPreviewImageView | null;
|
||||
image: ContentPreviewImageView | DecoratedAvatarView | null;
|
||||
metadata: ContentMetadataView | null;
|
||||
actions: FlexibleActionsView | null;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.title = Parser.parseItem(data.title, DynamicTextView);
|
||||
this.image = Parser.parseItem(data.image, ContentPreviewImageView);
|
||||
this.image = Parser.parseItem(data.image, [ ContentPreviewImageView, DecoratedAvatarView ]);
|
||||
this.metadata = Parser.parseItem(data.metadata, ContentMetadataView);
|
||||
this.actions = Parser.parseItem(data.actions, FlexibleActionsView);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ObservedArray} from '../helpers.ts';
|
||||
import type { ObservedArray } from '../helpers.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import { Parser } from '../index.ts';
|
||||
|
||||
56
deno/src/parser/classes/SegmentedLikeDislikeButtonView.ts
Normal file
56
deno/src/parser/classes/SegmentedLikeDislikeButtonView.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import LikeButtonView from './LikeButtonView.ts';
|
||||
import DislikeButtonView from './DislikeButtonView.ts';
|
||||
|
||||
export default class SegmentedLikeDislikeButtonView extends YTNode {
|
||||
static type = 'SegmentedLikeDislikeButtonView';
|
||||
|
||||
like_button: LikeButtonView | null;
|
||||
dislike_button: DislikeButtonView | null;
|
||||
icon_type: string;
|
||||
like_count_entity: {
|
||||
key: string
|
||||
};
|
||||
dynamic_like_count_update_data: {
|
||||
update_status_key: string,
|
||||
placeholder_like_count_values_key: string,
|
||||
update_delay_loop_id: string,
|
||||
update_delay_sec: number
|
||||
};
|
||||
|
||||
like_count?: number;
|
||||
short_like_count?: string;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.like_button = Parser.parseItem(data.likeButtonViewModel, LikeButtonView);
|
||||
this.dislike_button = Parser.parseItem(data.dislikeButtonViewModel, DislikeButtonView);
|
||||
this.icon_type = data.iconType;
|
||||
|
||||
if (this.like_button && this.like_button.toggle_button) {
|
||||
const toggle_button = this.like_button.toggle_button;
|
||||
|
||||
if (toggle_button.default_button) {
|
||||
this.short_like_count = toggle_button.default_button.title;
|
||||
|
||||
this.like_count = parseInt(toggle_button.default_button.accessibility_text.replace(/\D/g, ''));
|
||||
} else if (toggle_button.toggled_button) {
|
||||
this.short_like_count = toggle_button.toggled_button.title;
|
||||
|
||||
this.like_count = parseInt(toggle_button.toggled_button.accessibility_text.replace(/\D/g, ''));
|
||||
}
|
||||
}
|
||||
|
||||
this.like_count_entity = {
|
||||
key: data.likeCountEntity.key
|
||||
};
|
||||
|
||||
this.dynamic_like_count_update_data = {
|
||||
update_status_key: data.dynamicLikeCountUpdateData.updateStatusKey,
|
||||
placeholder_like_count_values_key: data.dynamicLikeCountUpdateData.placeholderLikeCountValuesKey,
|
||||
update_delay_loop_id: data.dynamicLikeCountUpdateData.updateDelayLoopId,
|
||||
update_delay_sec: data.dynamicLikeCountUpdateData.updateDelaySec
|
||||
};
|
||||
}
|
||||
}
|
||||
20
deno/src/parser/classes/ToggleButtonView.ts
Normal file
20
deno/src/parser/classes/ToggleButtonView.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
import ButtonView from './ButtonView.ts';
|
||||
|
||||
export default class ToggleButtonView extends YTNode {
|
||||
static type = 'ToggleButtonView';
|
||||
|
||||
default_button: ButtonView | null;
|
||||
toggled_button: ButtonView | null;
|
||||
identifier?: string;
|
||||
is_toggling_disabled: boolean;
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
this.default_button = Parser.parseItem(data.defaultButtonViewModel, ButtonView);
|
||||
this.toggled_button = Parser.parseItem(data.toggledButtonViewModel, ButtonView);
|
||||
this.identifier = data.identifier;
|
||||
this.is_toggling_disabled = data.isTogglingDisabled;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,7 @@ import Thumbnail from './misc/Thumbnail.ts';
|
||||
export default class VideoAttributeView extends YTNode {
|
||||
static type = 'VideoAttributeView';
|
||||
|
||||
image: ContentPreviewImageView | {
|
||||
sources: Thumbnail[];
|
||||
} | null;
|
||||
image: ContentPreviewImageView | Thumbnail[] | null;
|
||||
image_style: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -26,11 +24,9 @@ export default class VideoAttributeView extends YTNode {
|
||||
|
||||
constructor(data: RawNode) {
|
||||
super();
|
||||
// @NOTE: "image" is not a renderer so not sure why we're parsing it as one. Leaving this hack here for now to avoid breaking things.
|
||||
|
||||
if (data.image?.sources) {
|
||||
this.image = {
|
||||
sources: data.image.sources.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width)
|
||||
};
|
||||
this.image = Thumbnail.fromResponse(data.image);
|
||||
} else {
|
||||
this.image = Parser.parseItem(data.image, ContentPreviewImageView);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ObservedArray} from '../helpers.ts';
|
||||
import type { ObservedArray } from '../helpers.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
import { Parser } from '../index.ts';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ObservedArray} from '../helpers.ts';
|
||||
import type { ObservedArray } from '../helpers.ts';
|
||||
import { YTNode } from '../helpers.ts';
|
||||
import { Parser, type RawNode } from '../index.ts';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Parser } from '../../index.ts';
|
||||
import type { ObservedArray} from '../../helpers.ts';
|
||||
import type { ObservedArray } from '../../helpers.ts';
|
||||
import { YTNode } from '../../helpers.ts';
|
||||
import type { RawNode } from '../../index.ts';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ObservedArray} from '../../helpers.ts';
|
||||
import type { ObservedArray } from '../../helpers.ts';
|
||||
import { YTNode } from '../../helpers.ts';
|
||||
import type { RawNode } from '../../index.ts';
|
||||
import { Parser } from '../../index.ts';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ObservedArray} from '../../helpers.ts';
|
||||
import type { ObservedArray } from '../../helpers.ts';
|
||||
import { YTNode } from '../../helpers.ts';
|
||||
import type { RawNode } from '../../index.ts';
|
||||
import { Parser } from '../../index.ts';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SuperParsedResult} from '../../helpers.ts';
|
||||
import type { SuperParsedResult } from '../../helpers.ts';
|
||||
import { YTNode } from '../../helpers.ts';
|
||||
import type { RawNode } from '../../index.ts';
|
||||
import { Parser } from '../../index.ts';
|
||||
|
||||
@@ -15,7 +15,20 @@ export default class Thumbnail {
|
||||
* Get thumbnails from response object.
|
||||
*/
|
||||
static fromResponse(data: any): Thumbnail[] {
|
||||
if (!data || !data.thumbnails) return [];
|
||||
return data.thumbnails.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width);
|
||||
if (!data) return [];
|
||||
|
||||
let thumbnail_data;
|
||||
|
||||
if (data.thumbnails) {
|
||||
thumbnail_data = data.thumbnails;
|
||||
} else if (data.sources) {
|
||||
thumbnail_data = data.sources;
|
||||
}
|
||||
|
||||
if (thumbnail_data) {
|
||||
return thumbnail_data.map((x: any) => new Thumbnail(x)).sort((a: Thumbnail, b: Thumbnail) => b.width - a.width);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -30,27 +30,43 @@ export type MiscInferenceType = {
|
||||
params: [string, string?],
|
||||
}
|
||||
|
||||
export type InferenceType = {
|
||||
type: 'renderer',
|
||||
renderers: string[],
|
||||
optional: boolean,
|
||||
} | {
|
||||
type: 'renderer_list',
|
||||
renderers: string[],
|
||||
optional: boolean,
|
||||
} | MiscInferenceType | {
|
||||
export interface ObjectInferenceType {
|
||||
type: 'object',
|
||||
keys: KeyInfo,
|
||||
optional: boolean,
|
||||
} | {
|
||||
}
|
||||
|
||||
export interface RendererInferenceType {
|
||||
type: 'renderer',
|
||||
renderers: string[],
|
||||
optional: boolean
|
||||
}
|
||||
|
||||
export interface PrimativeInferenceType {
|
||||
type: 'primative',
|
||||
typeof: ('string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'undefined' | 'function')[],
|
||||
optional: boolean,
|
||||
} | {
|
||||
type: 'unknown',
|
||||
typeof: ('string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'undefined' | 'function' | 'never' | 'unknown')[],
|
||||
optional: boolean,
|
||||
}
|
||||
|
||||
export type ArrayInferenceType = {
|
||||
type: 'array',
|
||||
array_type: 'primitive',
|
||||
items: PrimativeInferenceType,
|
||||
optional: boolean,
|
||||
} | {
|
||||
type: 'array',
|
||||
array_type: 'object',
|
||||
items: ObjectInferenceType,
|
||||
optional: boolean,
|
||||
} | {
|
||||
type: 'array',
|
||||
array_type: 'renderer',
|
||||
renderers: string[],
|
||||
optional: boolean,
|
||||
};
|
||||
|
||||
export type InferenceType = RendererInferenceType | MiscInferenceType | ObjectInferenceType | PrimativeInferenceType | ArrayInferenceType;
|
||||
|
||||
export type KeyInfo = (readonly [string, InferenceType])[];
|
||||
|
||||
const IGNORED_KEYS = new Set([
|
||||
@@ -70,7 +86,7 @@ export function camelToSnake(str: string) {
|
||||
* @returns The inferred type
|
||||
*/
|
||||
export function inferType(key: string, value: unknown): InferenceType {
|
||||
let return_value: string | Record<string, any> | boolean | MiscInferenceType = false;
|
||||
let return_value: string | Record<string, any> | false | MiscInferenceType | ArrayInferenceType = false;
|
||||
if (typeof value === 'object' && value != null) {
|
||||
if (return_value = isRenderer(value)) {
|
||||
RENDERER_EXAMPLES[return_value] = Reflect.get(value, Reflect.ownKeys(value)[0]);
|
||||
@@ -85,7 +101,8 @@ export function inferType(key: string, value: unknown): InferenceType {
|
||||
RENDERER_EXAMPLES[key] = value;
|
||||
}
|
||||
return {
|
||||
type: 'renderer_list',
|
||||
type: 'array',
|
||||
array_type: 'renderer',
|
||||
renderers: Object.keys(return_value),
|
||||
optional: false
|
||||
};
|
||||
@@ -93,6 +110,9 @@ export function inferType(key: string, value: unknown): InferenceType {
|
||||
if (return_value = isMiscType(key, value)) {
|
||||
return return_value as MiscInferenceType;
|
||||
}
|
||||
if (return_value = isArrayType(value)) {
|
||||
return return_value as ArrayInferenceType;
|
||||
}
|
||||
}
|
||||
const primative_type = typeof value;
|
||||
if (primative_type === 'object')
|
||||
@@ -116,6 +136,9 @@ export function inferType(key: string, value: unknown): InferenceType {
|
||||
*/
|
||||
export function isRendererList(value: unknown) {
|
||||
const arr = Array.isArray(value);
|
||||
if (arr && value.length === 0)
|
||||
return false;
|
||||
|
||||
const is_list = arr && value.every((item) => isRenderer(item));
|
||||
return (
|
||||
is_list ?
|
||||
@@ -176,12 +199,89 @@ export function isRenderer(value: unknown) {
|
||||
const is_object = typeof value === 'object';
|
||||
if (!is_object) return false;
|
||||
const keys = Reflect.ownKeys(value as object);
|
||||
if (keys.length === 1 && keys[0].toString().includes('Renderer')) {
|
||||
return Parser.sanitizeClassName(keys[0].toString());
|
||||
|
||||
if (keys.length === 1) {
|
||||
const first_key = keys[0].toString();
|
||||
|
||||
if (first_key.endsWith('Renderer') || first_key.endsWith('Model')) {
|
||||
return Parser.sanitizeClassName(first_key);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given value is an array
|
||||
* @param value - The value to check
|
||||
* @returns If it is an array, return the InferenceType. Otherwise, return false.
|
||||
*/
|
||||
export function isArrayType(value: unknown): false | ArrayInferenceType {
|
||||
if (!Array.isArray(value))
|
||||
return false;
|
||||
|
||||
// If the array is empty, we can't infer anything
|
||||
if (value.length === 0)
|
||||
return {
|
||||
type: 'array',
|
||||
array_type: 'primitive',
|
||||
items: {
|
||||
type: 'primative',
|
||||
typeof: [ 'never' ],
|
||||
optional: false
|
||||
},
|
||||
optional: false
|
||||
};
|
||||
// We'll infer the primative type of the array entries
|
||||
const array_entry_types = value.map((item) => typeof item);
|
||||
// We only support arrays that have the same primative type throughout
|
||||
const all_same_type = array_entry_types.every((type) => type === array_entry_types[0]);
|
||||
if (!all_same_type)
|
||||
return {
|
||||
type: 'array',
|
||||
array_type: 'primitive',
|
||||
items: {
|
||||
type: 'primative',
|
||||
typeof: [ 'unknown' ],
|
||||
optional: false
|
||||
},
|
||||
optional: false
|
||||
};
|
||||
|
||||
const type = array_entry_types[0];
|
||||
if (type !== 'object')
|
||||
return {
|
||||
type: 'array',
|
||||
array_type: 'primitive',
|
||||
items: {
|
||||
type: 'primative',
|
||||
typeof: [ type ],
|
||||
optional: false
|
||||
},
|
||||
optional: false
|
||||
};
|
||||
|
||||
let key_type: KeyInfo = [];
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const current_keys = Object.entries(value[i] as object).map(([ key, value ]) => [ key, inferType(key, value) ] as const);
|
||||
if (i === 0) {
|
||||
key_type = current_keys;
|
||||
continue;
|
||||
}
|
||||
key_type = mergeKeyInfo(key_type, current_keys).resolved_key_info;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'array',
|
||||
array_type: 'object',
|
||||
items: {
|
||||
type: 'object',
|
||||
keys: key_type,
|
||||
optional: false
|
||||
},
|
||||
optional: false
|
||||
};
|
||||
}
|
||||
|
||||
function introspectKeysFirstPass(classdata: unknown): KeyInfo {
|
||||
if (typeof classdata !== 'object' || classdata === null) {
|
||||
throw new InnertubeError('Generator: Cannot introspect non-object', {
|
||||
@@ -236,7 +336,7 @@ function introspectKeysSecondPass(key_info: KeyInfo) {
|
||||
// Verify that its actually badges
|
||||
const badge_key_info = key_info.find(([ key ]) => key === cannonical_badges);
|
||||
const is_badges = badge_key_info ?
|
||||
badge_key_info[1].type === 'renderer_list' && Reflect.has(badge_key_info[1].renderers, 'MetadataBadge') :
|
||||
badge_key_info[1].type === 'array' && badge_key_info[1].array_type === 'renderer' && Reflect.has(badge_key_info[1].renderers, 'MetadataBadge') :
|
||||
false;
|
||||
|
||||
if (is_badges && cannonical_badges) excluded_keys.add(cannonical_badges);
|
||||
@@ -273,7 +373,7 @@ export function introspect(classdata: unknown) {
|
||||
const key_info = introspect2(classdata);
|
||||
const dependencies = new Map<string, any>();
|
||||
for (const [ , value ] of key_info) {
|
||||
if (value.type === 'renderer' || value.type === 'renderer_list')
|
||||
if (value.type === 'renderer' || (value.type === 'array' && value.array_type === 'renderer'))
|
||||
for (const renderer of value.renderers) {
|
||||
const example = RENDERER_EXAMPLES[renderer];
|
||||
if (example)
|
||||
@@ -401,6 +501,10 @@ export function generateTypescriptClass(classname: string, key_info: KeyInfo) {
|
||||
return `class ${classname} extends YTNode {\n static type = '${classname}';\n\n ${props.join('\n ')}\n\n constructor(data: RawNode) {\n ${constructor_lines.join('\n ')}\n }\n}\n`;
|
||||
}
|
||||
|
||||
function toTypeDeclarationObject(indentation: number, keys: KeyInfo) {
|
||||
return `{\n${keys.map(([ key, value ]) => `${' '.repeat((indentation + 2) * 2)}${camelToSnake(key)}${value.optional ? '?' : ''}: ${toTypeDeclaration(value, indentation + 1)}`).join(',\n')}\n${' '.repeat((indentation + 1) * 2)}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* For a given inference type, get the typescript type declaration
|
||||
* @param inference_type - The inference type to get the declaration for
|
||||
@@ -413,13 +517,33 @@ export function toTypeDeclaration(inference_type: InferenceType, indentation = 0
|
||||
{
|
||||
return `${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')} | null`;
|
||||
}
|
||||
case 'renderer_list':
|
||||
case 'array':
|
||||
{
|
||||
return `ObservedArray<${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')}> | null`;
|
||||
switch (inference_type.array_type) {
|
||||
case 'renderer':
|
||||
return `ObservedArray<${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')}> | null`;
|
||||
|
||||
case 'primitive':
|
||||
{
|
||||
const items_list = inference_type.items.typeof;
|
||||
if (inference_type.items.optional && !items_list.includes('undefined'))
|
||||
items_list.push('undefined');
|
||||
const items =
|
||||
items_list.length === 1 ?
|
||||
`${items_list[0]}` : `(${items_list.join(' | ')})`;
|
||||
return `${items}[]`;
|
||||
}
|
||||
|
||||
case 'object':
|
||||
return `${toTypeDeclarationObject(indentation, inference_type.items.keys)}[]`;
|
||||
|
||||
default:
|
||||
throw new Error('Unreachable code reached! Switch missing case!');
|
||||
}
|
||||
}
|
||||
case 'object':
|
||||
{
|
||||
return `{\n${inference_type.keys.map(([ key, value ]) => `${' '.repeat((indentation + 2) * 2)}${camelToSnake(key)}${value.optional ? '?' : ''}: ${toTypeDeclaration(value, indentation + 1)}`).join(',\n')}\n${' '.repeat((indentation + 1) * 2)}}`;
|
||||
return toTypeDeclarationObject(indentation, inference_type.keys);
|
||||
}
|
||||
case 'misc':
|
||||
switch (inference_type.misc_type) {
|
||||
@@ -430,11 +554,14 @@ export function toTypeDeclaration(inference_type: InferenceType, indentation = 0
|
||||
}
|
||||
case 'primative':
|
||||
return inference_type.typeof.join(' | ');
|
||||
case 'unknown':
|
||||
return '/* TODO: determine correct type */ unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function toParserObject(indentation: number, keys: KeyInfo, key_path: string[], key: string) {
|
||||
const new_keypath = [ ...key_path, key ];
|
||||
return `{\n${keys.map(([ key, value ]) => `${' '.repeat((indentation + 2) * 2)}${camelToSnake(key)}: ${toParser(key, value, new_keypath, indentation + 1)}`).join(',\n')}\n${' '.repeat((indentation + 1) * 2)}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate statements to parse a given inference type
|
||||
* @param key - The key to parse
|
||||
@@ -448,18 +575,32 @@ export function toParser(key: string, inference_type: InferenceType, key_path: s
|
||||
switch (inference_type.type) {
|
||||
case 'renderer':
|
||||
{
|
||||
parser = `Parser.parseItem(${key_path.join('.')}.${key}, [ ${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')} ])`;
|
||||
parser = `Parser.parseItem(${key_path.join('.')}.${key}, ${toParserValidTypes(inference_type.renderers)})`;
|
||||
}
|
||||
break;
|
||||
case 'renderer_list':
|
||||
case 'array':
|
||||
{
|
||||
parser = `Parser.parse(${key_path.join('.')}.${key}, true, [ ${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')} ])`;
|
||||
switch (inference_type.array_type) {
|
||||
case 'renderer':
|
||||
parser = `Parser.parse(${key_path.join('.')}.${key}, true, ${toParserValidTypes(inference_type.renderers)})`;
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
parser = `${key_path.join('.')}.${key}.map((item: any) => (${toParserObject(indentation, inference_type.items.keys, [], 'item')}))`;
|
||||
break;
|
||||
|
||||
case 'primitive':
|
||||
parser = `${key_path.join('.')}.${key}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Unreachable code reached! Switch missing case!');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'object':
|
||||
{
|
||||
const new_keypath = [ ...key_path, key ];
|
||||
parser = `{\n${inference_type.keys.map(([ key, value ]) => `${' '.repeat((indentation + 2) * 2)}${camelToSnake(key)}: ${toParser(key, value, new_keypath, indentation + 1)}`).join(',\n')}\n${' '.repeat((indentation + 1) * 2)}}`;
|
||||
parser = toParserObject(indentation, inference_type.keys, key_path, key);
|
||||
}
|
||||
break;
|
||||
case 'misc':
|
||||
@@ -482,7 +623,6 @@ export function toParser(key: string, inference_type: InferenceType, key_path: s
|
||||
throw new Error('Unreachable code reached! Switch missing case!');
|
||||
break;
|
||||
case 'primative':
|
||||
case 'unknown':
|
||||
parser = `${key_path.join('.')}.${key}`;
|
||||
break;
|
||||
}
|
||||
@@ -491,6 +631,14 @@ export function toParser(key: string, inference_type: InferenceType, key_path: s
|
||||
return parser;
|
||||
}
|
||||
|
||||
function toParserValidTypes(types: string[]) {
|
||||
if (types.length === 1) {
|
||||
return `YTNodes.${types[0]}`;
|
||||
}
|
||||
|
||||
return `[ ${types.map((type) => `YTNodes.${type}`).join(', ')} ]`;
|
||||
}
|
||||
|
||||
function accessDataFromKeyPath(root: any, key_path: string[]) {
|
||||
let data = root;
|
||||
for (const key of key_path)
|
||||
@@ -508,6 +656,15 @@ function hasDataFromKeyPath(root: any, key_path: string[]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseObject(key: string, data: unknown, key_path: string[], keys: KeyInfo, should_optional: boolean) {
|
||||
const obj: any = {};
|
||||
const new_key_path = [ ...key_path, key ];
|
||||
for (const [ key, value ] of keys) {
|
||||
obj[key] = should_optional ? parse(key, value, data, new_key_path) : undefined;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a value from a given key path using the given inference type
|
||||
* @param key - The key to parse
|
||||
@@ -523,18 +680,26 @@ export function parse(key: string, inference_type: InferenceType, data: unknown,
|
||||
{
|
||||
return should_optional ? Parser.parseItem(accessDataFromKeyPath({ data }, [ ...key_path, key ]), inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
|
||||
}
|
||||
case 'renderer_list':
|
||||
case 'array':
|
||||
{
|
||||
return should_optional ? Parser.parse(accessDataFromKeyPath({ data }, [ ...key_path, key ]), true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
|
||||
switch (inference_type.array_type) {
|
||||
case 'renderer':
|
||||
return should_optional ? Parser.parse(accessDataFromKeyPath({ data }, [ ...key_path, key ]), true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined;
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
return should_optional ? accessDataFromKeyPath({ data }, [ ...key_path, key ]).map((_: any, idx: number) => {
|
||||
return parseObject(`${idx}`, data, [ ...key_path, key ], inference_type.items.keys, should_optional);
|
||||
}) : undefined;
|
||||
|
||||
case 'primitive':
|
||||
return should_optional ? accessDataFromKeyPath({ data }, [ ...key_path, key ]) : undefined;
|
||||
}
|
||||
throw new Error('Unreachable code reached! Switch missing case!');
|
||||
}
|
||||
case 'object':
|
||||
{
|
||||
const obj: any = {};
|
||||
const new_key_path = [ ...key_path, key ];
|
||||
for (const [ key, value ] of inference_type.keys) {
|
||||
obj[key] = should_optional ? parse(key, value, data, new_key_path) : undefined;
|
||||
}
|
||||
return obj;
|
||||
return parseObject(key, data, key_path, inference_type.keys, should_optional);
|
||||
}
|
||||
case 'misc':
|
||||
switch (inference_type.misc_type) {
|
||||
@@ -556,7 +721,6 @@ export function parse(key: string, inference_type: InferenceType, data: unknown,
|
||||
}
|
||||
throw new Error('Unreachable code reached! Switch missing case!');
|
||||
case 'primative':
|
||||
case 'unknown':
|
||||
return accessDataFromKeyPath({ data }, [ ...key_path, key ]);
|
||||
}
|
||||
}
|
||||
@@ -585,7 +749,8 @@ export function mergeKeyInfo(key_info: KeyInfo, new_key_info: KeyInfo) {
|
||||
if (type.type !== new_type.type) {
|
||||
// We've got a type mismatch, this is unknown, we do not resolve unions
|
||||
changed_keys.set(key, {
|
||||
type: 'unknown',
|
||||
type: 'primative',
|
||||
typeof: [ 'unknown' ],
|
||||
optional: true
|
||||
});
|
||||
continue;
|
||||
@@ -628,27 +793,128 @@ export function mergeKeyInfo(key_info: KeyInfo, new_key_info: KeyInfo) {
|
||||
if (did_change) changed_keys.set(key, resolved_key);
|
||||
}
|
||||
break;
|
||||
case 'renderer_list':
|
||||
case 'array':
|
||||
{
|
||||
if (new_type.type !== 'renderer_list') continue;
|
||||
const union_map = {
|
||||
...type.renderers,
|
||||
...new_type.renderers
|
||||
};
|
||||
const either_optional = type.optional || new_type.optional;
|
||||
const resolved_key: InferenceType = {
|
||||
type: 'renderer_list',
|
||||
renderers: union_map,
|
||||
optional: either_optional
|
||||
};
|
||||
const did_change = JSON.stringify({
|
||||
...resolved_key,
|
||||
renderers: Object.keys(resolved_key.renderers)
|
||||
}) !== JSON.stringify({
|
||||
...type,
|
||||
renderers: Object.keys(type.renderers)
|
||||
});
|
||||
if (did_change) changed_keys.set(key, resolved_key);
|
||||
if (new_type.type !== 'array') continue;
|
||||
switch (type.array_type) {
|
||||
case 'renderer':
|
||||
{
|
||||
if (new_type.array_type !== 'renderer') {
|
||||
// Type mismatch
|
||||
changed_keys.set(key, {
|
||||
type: 'array',
|
||||
array_type: 'primitive',
|
||||
items: {
|
||||
type: 'primative',
|
||||
typeof: [ 'unknown' ],
|
||||
optional: true
|
||||
},
|
||||
optional: true
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const union_map = {
|
||||
...type.renderers,
|
||||
...new_type.renderers
|
||||
};
|
||||
const either_optional = type.optional || new_type.optional;
|
||||
const resolved_key: InferenceType = {
|
||||
type: 'array',
|
||||
array_type: 'renderer',
|
||||
renderers: union_map,
|
||||
optional: either_optional
|
||||
};
|
||||
const did_change = JSON.stringify({
|
||||
...resolved_key,
|
||||
renderers: Object.keys(resolved_key.renderers)
|
||||
}) !== JSON.stringify({
|
||||
...type,
|
||||
renderers: Object.keys(type.renderers)
|
||||
});
|
||||
if (did_change) changed_keys.set(key, resolved_key);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
{
|
||||
if (new_type.array_type === 'primitive' && new_type.items.typeof.length == 1 && new_type.items.typeof[0] === 'never') {
|
||||
// It's an empty array. We assume the type is unchanged
|
||||
continue;
|
||||
}
|
||||
if (new_type.array_type !== 'object') {
|
||||
// Type mismatch
|
||||
changed_keys.set(key, {
|
||||
type: 'array',
|
||||
array_type: 'primitive',
|
||||
items: {
|
||||
type: 'primative',
|
||||
typeof: [ 'unknown' ],
|
||||
optional: true
|
||||
},
|
||||
optional: true
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const { resolved_key_info } = mergeKeyInfo(type.items.keys, new_type.items.keys);
|
||||
const resolved_key: InferenceType = {
|
||||
type: 'array',
|
||||
array_type: 'object',
|
||||
items: {
|
||||
type: 'object',
|
||||
keys: resolved_key_info,
|
||||
optional: type.items.optional || new_type.items.optional
|
||||
},
|
||||
optional: type.optional || new_type.optional
|
||||
};
|
||||
const did_change = JSON.stringify(resolved_key) !== JSON.stringify(type);
|
||||
if (did_change) changed_keys.set(key, resolved_key);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'primitive':
|
||||
{
|
||||
if (type.items.typeof.includes('never') && new_type.array_type === 'object') {
|
||||
// Type is now known from previosly unknown
|
||||
changed_keys.set(key, new_type);
|
||||
continue;
|
||||
}
|
||||
if (new_type.array_type !== 'primitive') {
|
||||
// Type mismatch
|
||||
changed_keys.set(key, {
|
||||
type: 'array',
|
||||
array_type: 'primitive',
|
||||
items: {
|
||||
type: 'primative',
|
||||
typeof: [ 'unknown' ],
|
||||
optional: true
|
||||
},
|
||||
optional: true
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const key_types = new Set([ ...new_type.items.typeof, ...type.items.typeof ]);
|
||||
if (key_types.size > 1 && key_types.has('never'))
|
||||
key_types.delete('never');
|
||||
|
||||
const resolved_key: InferenceType = {
|
||||
type: 'array',
|
||||
array_type: 'primitive',
|
||||
items: {
|
||||
type: 'primative',
|
||||
typeof: Array.from(key_types),
|
||||
optional: type.items.optional || new_type.items.optional
|
||||
},
|
||||
optional: type.optional || new_type.optional
|
||||
};
|
||||
const did_change = JSON.stringify(resolved_key) !== JSON.stringify(type);
|
||||
if (did_change) changed_keys.set(key, resolved_key);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Unreachable code reached! Switch missing case!');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'misc':
|
||||
@@ -657,7 +923,8 @@ export function mergeKeyInfo(key_info: KeyInfo, new_key_info: KeyInfo) {
|
||||
if (type.misc_type !== new_type.misc_type) {
|
||||
// We've got a type mismatch, this is unknown, we do not resolve unions
|
||||
changed_keys.set(key, {
|
||||
type: 'unknown',
|
||||
type: 'primative',
|
||||
typeof: [ 'unknown' ],
|
||||
optional: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export { default as DataModelSection } from './classes/analytics/DataModelSectio
|
||||
export { default as StatRow } from './classes/analytics/StatRow.ts';
|
||||
export { default as AudioOnlyPlayability } from './classes/AudioOnlyPlayability.ts';
|
||||
export { default as AutomixPreviewVideo } from './classes/AutomixPreviewVideo.ts';
|
||||
export { default as AvatarView } from './classes/AvatarView.ts';
|
||||
export { default as BackstageImage } from './classes/BackstageImage.ts';
|
||||
export { default as BackstagePost } from './classes/BackstagePost.ts';
|
||||
export { default as BackstagePostThread } from './classes/BackstagePostThread.ts';
|
||||
@@ -93,9 +94,11 @@ export { default as ContinuationItem } from './classes/ContinuationItem.ts';
|
||||
export { default as ConversationBar } from './classes/ConversationBar.ts';
|
||||
export { default as CopyLink } from './classes/CopyLink.ts';
|
||||
export { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog.ts';
|
||||
export { default as DecoratedAvatarView } from './classes/DecoratedAvatarView.ts';
|
||||
export { default as DecoratedPlayerBar } from './classes/DecoratedPlayerBar.ts';
|
||||
export { default as DefaultPromoPanel } from './classes/DefaultPromoPanel.ts';
|
||||
export { default as DidYouMean } from './classes/DidYouMean.ts';
|
||||
export { default as DislikeButtonView } from './classes/DislikeButtonView.ts';
|
||||
export { default as DownloadButton } from './classes/DownloadButton.ts';
|
||||
export { default as Dropdown } from './classes/Dropdown.ts';
|
||||
export { default as DropdownItem } from './classes/DropdownItem.ts';
|
||||
@@ -158,6 +161,7 @@ export { default as ItemSectionHeader } from './classes/ItemSectionHeader.ts';
|
||||
export { default as ItemSectionTab } from './classes/ItemSectionTab.ts';
|
||||
export { default as ItemSectionTabbedHeader } from './classes/ItemSectionTabbedHeader.ts';
|
||||
export { default as LikeButton } from './classes/LikeButton.ts';
|
||||
export { default as LikeButtonView } from './classes/LikeButtonView.ts';
|
||||
export { default as LiveChat } from './classes/LiveChat.ts';
|
||||
export { default as AddBannerToLiveChatCommand } from './classes/livechat/AddBannerToLiveChatCommand.ts';
|
||||
export { default as AddChatItemAction } from './classes/livechat/AddChatItemAction.ts';
|
||||
@@ -328,6 +332,7 @@ export { default as SearchSuggestionsSection } from './classes/SearchSuggestions
|
||||
export { default as SecondarySearchContainer } from './classes/SecondarySearchContainer.ts';
|
||||
export { default as SectionList } from './classes/SectionList.ts';
|
||||
export { default as SegmentedLikeDislikeButton } from './classes/SegmentedLikeDislikeButton.ts';
|
||||
export { default as SegmentedLikeDislikeButtonView } from './classes/SegmentedLikeDislikeButtonView.ts';
|
||||
export { default as SettingBoolean } from './classes/SettingBoolean.ts';
|
||||
export { default as SettingsCheckbox } from './classes/SettingsCheckbox.ts';
|
||||
export { default as SettingsOptions } from './classes/SettingsOptions.ts';
|
||||
@@ -373,6 +378,7 @@ export { default as ThumbnailOverlayToggleButton } from './classes/ThumbnailOver
|
||||
export { default as TimedMarkerDecoration } from './classes/TimedMarkerDecoration.ts';
|
||||
export { default as TitleAndButtonListHeader } from './classes/TitleAndButtonListHeader.ts';
|
||||
export { default as ToggleButton } from './classes/ToggleButton.ts';
|
||||
export { default as ToggleButtonView } from './classes/ToggleButtonView.ts';
|
||||
export { default as ToggleMenuServiceItem } from './classes/ToggleMenuServiceItem.ts';
|
||||
export { default as Tooltip } from './classes/Tooltip.ts';
|
||||
export { default as TopicChannelDetails } from './classes/TopicChannelDetails.ts';
|
||||
|
||||
@@ -398,6 +398,28 @@ export function parseResponse<T extends IParsedResponse = IParsedResponse>(data:
|
||||
parsed_data.streaming_data = streaming_data;
|
||||
}
|
||||
|
||||
if (data.playerConfig) {
|
||||
const player_config = {
|
||||
audio_config: {
|
||||
loudness_db: data.playerConfig.audioConfig?.loudnessDb,
|
||||
perceptual_loudness_db: data.playerConfig.audioConfig?.perceptualLoudnessDb,
|
||||
enable_per_format_loudness: data.playerConfig.audioConfig?.enablePerFormatLoudness
|
||||
},
|
||||
stream_selection_config: {
|
||||
max_bitrate: data.playerConfig.streamSelectionConfig?.maxBitrate || '0'
|
||||
},
|
||||
media_common_config: {
|
||||
dynamic_readahead_config: {
|
||||
max_read_ahead_media_time_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.maxReadAheadMediaTimeMs || 0,
|
||||
min_read_ahead_media_time_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.minReadAheadMediaTimeMs || 0,
|
||||
read_ahead_growth_rate_ms: data.playerConfig.mediaCommonConfig?.dynamicReadaheadConfig?.readAheadGrowthRateMs || 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
parsed_data.player_config = player_config;
|
||||
}
|
||||
|
||||
const current_video_endpoint = data.currentVideoEndpoint ? new NavigationEndpoint(data.currentVideoEndpoint) : null;
|
||||
if (current_video_endpoint) {
|
||||
parsed_data.current_video_endpoint = current_video_endpoint;
|
||||
@@ -545,6 +567,7 @@ export function parseArray(data?: RawNode[], validTypes?: YTNodeConstructor | YT
|
||||
* @param validTypes - YTNode types that are allowed to be parsed.
|
||||
*/
|
||||
export function parse<T extends YTNode, K extends YTNodeConstructor<T>[]>(data: RawData, requireArray: true, validTypes?: K): ObservedArray<InstanceType<K[number]>> | null;
|
||||
export function parse<T extends YTNode, K extends YTNodeConstructor<T>>(data: RawData, requireArray: true, validTypes?: K): ObservedArray<InstanceType<K>> | null;
|
||||
export function parse<T extends YTNode = YTNode>(data?: RawData, requireArray?: false | undefined, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]): SuperParsedResult<T>;
|
||||
export function parse<T extends YTNode = YTNode>(data?: RawData, requireArray?: boolean, validTypes?: YTNodeConstructor<T> | YTNodeConstructor<T>[]) {
|
||||
if (!data) return null;
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface IParsedResponse {
|
||||
};
|
||||
playability_status?: IPlayabilityStatus;
|
||||
streaming_data?: IStreamingData;
|
||||
player_config?: IPlayerConfig;
|
||||
current_video_endpoint?: NavigationEndpoint;
|
||||
endpoint?: NavigationEndpoint;
|
||||
captions?: PlayerCaptionsTracklist;
|
||||
@@ -71,6 +72,24 @@ export interface IParsedResponse {
|
||||
continuationEndpoint?: YTNode;
|
||||
}
|
||||
|
||||
export interface IPlayerConfig {
|
||||
audio_config: {
|
||||
loudness_db?: number;
|
||||
perceptual_loudness_db?: number;
|
||||
enable_per_format_loudness: boolean;
|
||||
};
|
||||
stream_selection_config: {
|
||||
max_bitrate: string;
|
||||
};
|
||||
media_common_config: {
|
||||
dynamic_readahead_config: {
|
||||
max_read_ahead_media_time_ms: number;
|
||||
min_read_ahead_media_time_ms: number;
|
||||
read_ahead_growth_rate_ms: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IStreamingData {
|
||||
expires: Date;
|
||||
formats: Format[];
|
||||
@@ -87,6 +106,7 @@ export interface IPlayerResponse {
|
||||
annotations?: ObservedArray<PlayerAnnotationsExpanded>;
|
||||
playability_status: IPlayabilityStatus;
|
||||
streaming_data?: IStreamingData;
|
||||
player_config: IPlayerConfig;
|
||||
playback_tracking?: {
|
||||
videostats_watchtime_url: string;
|
||||
videostats_playback_url: string;
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
export type RawNode = Record<string, any>;
|
||||
export type RawData = RawNode | RawNode[];
|
||||
|
||||
export interface IRawPlayerConfig {
|
||||
audioConfig: {
|
||||
loudnessDb?: number;
|
||||
perceptualLoudnessDb?: number;
|
||||
enablePerFormatLoudness: boolean;
|
||||
};
|
||||
streamSelectionConfig: {
|
||||
maxBitrate: string;
|
||||
};
|
||||
mediaCommonConfig: {
|
||||
dynamicReadaheadConfig: {
|
||||
maxReadAheadMediaTimeMs: number;
|
||||
minReadAheadMediaTimeMs: number;
|
||||
readAheadGrowthRateMs: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRawResponse {
|
||||
contents?: RawData;
|
||||
onResponseReceivedActions?: RawNode[];
|
||||
@@ -41,6 +59,7 @@ export interface IRawResponse {
|
||||
dashManifestUrl?: string;
|
||||
hlsManifestUrl?: string;
|
||||
};
|
||||
playerConfig?: IRawPlayerConfig;
|
||||
currentVideoEndpoint?: RawNode;
|
||||
unseenCount?: number;
|
||||
playlistId?: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { IGuideResponse } from '../types/ParsedResponse.ts';
|
||||
import type { IRawResponse} from '../index.ts';
|
||||
import type { IRawResponse } from '../index.ts';
|
||||
import { Parser } from '../index.ts';
|
||||
import type { ObservedArray } from '../helpers.ts';
|
||||
import GuideSection from '../classes/GuideSection.ts';
|
||||
|
||||
@@ -12,13 +12,14 @@ import RelatedChipCloud from '../classes/RelatedChipCloud.ts';
|
||||
import RichMetadata from '../classes/RichMetadata.ts';
|
||||
import RichMetadataRow from '../classes/RichMetadataRow.ts';
|
||||
import SegmentedLikeDislikeButton from '../classes/SegmentedLikeDislikeButton.ts';
|
||||
import SegmentedLikeDislikeButtonView from '../classes/SegmentedLikeDislikeButtonView.ts';
|
||||
import ToggleButton from '../classes/ToggleButton.ts';
|
||||
import TwoColumnWatchNextResults from '../classes/TwoColumnWatchNextResults.ts';
|
||||
import VideoPrimaryInfo from '../classes/VideoPrimaryInfo.ts';
|
||||
import VideoSecondaryInfo from '../classes/VideoSecondaryInfo.ts';
|
||||
import LiveChatWrap from './LiveChat.ts';
|
||||
import type NavigationEndpoint from '../classes/NavigationEndpoint.ts';
|
||||
import NavigationEndpoint from '../classes/NavigationEndpoint.ts';
|
||||
import PlayerLegacyDesktopYpcTrailer from '../classes/PlayerLegacyDesktopYpcTrailer.ts';
|
||||
import LiveChatWrap from './LiveChat.ts';
|
||||
|
||||
import type CardCollection from '../classes/CardCollection.ts';
|
||||
import type Endscreen from '../classes/Endscreen.ts';
|
||||
@@ -35,6 +36,7 @@ import { InnertubeError } from '../../utils/Utils.ts';
|
||||
import { MediaInfo } from '../../core/mixins/index.ts';
|
||||
import StructuredDescriptionContent from '../classes/StructuredDescriptionContent.ts';
|
||||
import { VideoDescriptionMusicSection } from '../nodes.ts';
|
||||
import type { RawNode } from '../index.ts';
|
||||
|
||||
class VideoInfo extends MediaInfo {
|
||||
#watch_next_continuation?: ContinuationItem;
|
||||
@@ -105,11 +107,10 @@ class VideoInfo extends MediaInfo {
|
||||
// The combined formats only exist for the default language, even for videos with multiple audio tracks
|
||||
// So we can copy the language from the default audio track to the combined formats
|
||||
this.streaming_data.formats.forEach((format) => format.language = default_audio_track.language);
|
||||
} else if (typeof this.captions?.default_audio_track_index !== 'undefined' && this.captions?.audio_tracks && this.captions.caption_tracks) {
|
||||
} else if (this.captions?.caption_tracks && this.captions?.caption_tracks.length > 0) {
|
||||
// For videos with a single audio track and captions, we can use the captions to figure out the language of the audio and combined formats
|
||||
const audioTrack = this.captions.audio_tracks[this.captions.default_audio_track_index];
|
||||
const index = audioTrack.default_caption_track_index || 0;
|
||||
const language_code = this.captions.caption_tracks[index].language_code;
|
||||
const auto_generated_caption_track = this.captions.caption_tracks.find((caption) => caption.kind === 'asr');
|
||||
const language_code = auto_generated_caption_track?.language_code;
|
||||
|
||||
this.streaming_data.adaptive_formats.forEach((format) => {
|
||||
if (format.has_audio) {
|
||||
@@ -164,6 +165,17 @@ class VideoInfo extends MediaInfo {
|
||||
this.basic_info.is_disliked = segmented_like_dislike_button?.dislike_button?.is_toggled;
|
||||
}
|
||||
|
||||
const segmented_like_dislike_button_view = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButtonView);
|
||||
if (segmented_like_dislike_button_view) {
|
||||
this.basic_info.like_count = segmented_like_dislike_button_view.like_count;
|
||||
|
||||
if (segmented_like_dislike_button_view.like_button) {
|
||||
const like_status = segmented_like_dislike_button_view.like_button.like_status_entity.like_status;
|
||||
this.basic_info.is_liked = like_status === 'LIKE';
|
||||
this.basic_info.is_disliked = like_status === 'DISLIKE';
|
||||
}
|
||||
}
|
||||
|
||||
const comments_entry_point = results.get({ target_id: 'comments-entry-point' })?.as(ItemSection);
|
||||
|
||||
this.comments_entry_point_header = comments_entry_point?.contents?.firstOfType(CommentsEntryPointHeader);
|
||||
@@ -239,6 +251,26 @@ class VideoInfo extends MediaInfo {
|
||||
* Likes the video.
|
||||
*/
|
||||
async like(): Promise<ApiResponse> {
|
||||
const segmented_like_dislike_button_view = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButtonView);
|
||||
|
||||
if (segmented_like_dislike_button_view) {
|
||||
const button = segmented_like_dislike_button_view?.like_button?.toggle_button;
|
||||
|
||||
if (!button || !button.default_button || !segmented_like_dislike_button_view.like_button)
|
||||
throw new InnertubeError('Like button not found', { video_id: this.basic_info.id });
|
||||
|
||||
const like_status = segmented_like_dislike_button_view.like_button.like_status_entity.like_status;
|
||||
|
||||
if (like_status === 'LIKE')
|
||||
throw new InnertubeError('This video is already liked', { video_id: this.basic_info.id });
|
||||
|
||||
const endpoint = new NavigationEndpoint(button.default_button.on_tap.payload.commands.find((cmd: RawNode) => cmd.innertubeCommand));
|
||||
|
||||
const response = await endpoint.call(this.actions);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const button = segmented_like_dislike_button?.like_button;
|
||||
|
||||
@@ -260,6 +292,26 @@ class VideoInfo extends MediaInfo {
|
||||
* Dislikes the video.
|
||||
*/
|
||||
async dislike(): Promise<ApiResponse> {
|
||||
const segmented_like_dislike_button_view = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButtonView);
|
||||
|
||||
if (segmented_like_dislike_button_view) {
|
||||
const button = segmented_like_dislike_button_view?.dislike_button?.toggle_button;
|
||||
|
||||
if (!button || !button.default_button || !segmented_like_dislike_button_view.dislike_button || !segmented_like_dislike_button_view.like_button)
|
||||
throw new InnertubeError('Dislike button not found', { video_id: this.basic_info.id });
|
||||
|
||||
const like_status = segmented_like_dislike_button_view.like_button.like_status_entity.like_status;
|
||||
|
||||
if (like_status === 'DISLIKE')
|
||||
throw new InnertubeError('This video is already disliked', { video_id: this.basic_info.id });
|
||||
|
||||
const endpoint = new NavigationEndpoint(button.default_button.on_tap.payload.commands.find((cmd: RawNode) => cmd.innertubeCommand));
|
||||
|
||||
const response = await endpoint.call(this.actions);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
const button = segmented_like_dislike_button?.dislike_button;
|
||||
|
||||
@@ -283,6 +335,34 @@ class VideoInfo extends MediaInfo {
|
||||
async removeRating(): Promise<ApiResponse> {
|
||||
let button;
|
||||
|
||||
const segmented_like_dislike_button_view = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButtonView);
|
||||
|
||||
if (segmented_like_dislike_button_view) {
|
||||
const toggle_button = segmented_like_dislike_button_view?.like_button?.toggle_button;
|
||||
|
||||
if (!toggle_button || !toggle_button.default_button || !segmented_like_dislike_button_view.like_button)
|
||||
throw new InnertubeError('Like button not found', { video_id: this.basic_info.id });
|
||||
|
||||
const like_status = segmented_like_dislike_button_view.like_button.like_status_entity.like_status;
|
||||
|
||||
if (like_status === 'LIKE') {
|
||||
button = segmented_like_dislike_button_view?.like_button?.toggle_button;
|
||||
} else if (like_status === 'DISLIKE') {
|
||||
button = segmented_like_dislike_button_view?.dislike_button?.toggle_button;
|
||||
} else {
|
||||
throw new InnertubeError('This video is not liked/disliked', { video_id: this.basic_info.id });
|
||||
}
|
||||
|
||||
if (!button || !button.toggled_button)
|
||||
throw new InnertubeError('Like/Dislike button not found', { video_id: this.basic_info.id });
|
||||
|
||||
const endpoint = new NavigationEndpoint(button.toggled_button.on_tap.payload.commands.find((cmd: RawNode) => cmd.innertubeCommand));
|
||||
|
||||
const response = await endpoint.call(this.actions);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
const segmented_like_dislike_button = this.primary_info?.menu?.top_level_buttons.firstOfType(SegmentedLikeDislikeButton);
|
||||
|
||||
const like_button = segmented_like_dislike_button?.like_button;
|
||||
|
||||
Reference in New Issue
Block a user