From 14e6477b3ff99779929b37168ff2f4a473cb1853 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Wed, 8 Feb 2023 22:13:49 +0200 Subject: [PATCH 01/17] refactor: merge NavigatableText into Text --- src/parser/classes/GridPlaylist.ts | 5 ++-- src/parser/classes/Movie.ts | 4 +-- src/parser/classes/MusicSortFilterButton.ts | 2 +- src/parser/classes/PlaylistVideo.ts | 2 +- src/parser/classes/Video.ts | 4 +-- .../classes/menus/MusicMultiSelectMenu.ts | 2 +- .../classes/menus/MusicMultiSelectMenuItem.ts | 2 +- src/parser/classes/misc/Author.ts | 6 ++--- src/parser/classes/misc/NavigatableText.ts | 26 ------------------- src/parser/classes/misc/Text.ts | 20 +++++++++++--- 10 files changed, 30 insertions(+), 43 deletions(-) delete mode 100644 src/parser/classes/misc/NavigatableText.ts diff --git a/src/parser/classes/GridPlaylist.ts b/src/parser/classes/GridPlaylist.ts index 4996e32c4..2ba18c8da 100644 --- a/src/parser/classes/GridPlaylist.ts +++ b/src/parser/classes/GridPlaylist.ts @@ -3,7 +3,6 @@ import Parser from '../index.js'; import Thumbnail from './misc/Thumbnail.js'; import PlaylistAuthor from './misc/PlaylistAuthor.js'; import NavigationEndpoint from './NavigationEndpoint.js'; -import NavigatableText from './misc/NavigatableText.js'; import { YTNode } from '../helpers.js'; class GridPlaylist extends YTNode { @@ -14,7 +13,7 @@ class GridPlaylist extends YTNode { author?: PlaylistAuthor; badges; endpoint: NavigationEndpoint; - view_playlist: NavigatableText; + view_playlist: Text; thumbnails: Thumbnail[]; thumbnail_renderer; sidebar_thumbnails: Thumbnail[] | null; @@ -32,7 +31,7 @@ class GridPlaylist extends YTNode { this.badges = Parser.parse(data.ownerBadges); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); - this.view_playlist = new NavigatableText(data.viewPlaylistText); + this.view_playlist = new Text(data.viewPlaylistText); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); this.thumbnail_renderer = Parser.parse(data.thumbnailRenderer); this.sidebar_thumbnails = [].concat(...data.sidebarThumbnails?.map((thumbnail: any) => Thumbnail.fromResponse(thumbnail)) || []) || null; diff --git a/src/parser/classes/Movie.ts b/src/parser/classes/Movie.ts index 927547519..bec15e79d 100644 --- a/src/parser/classes/Movie.ts +++ b/src/parser/classes/Movie.ts @@ -43,8 +43,8 @@ class Movie extends YTNode { this.author = new Author(data.longBylineText, data.ownerBadges, data.channelThumbnailSupportedRenderers?.channelThumbnailWithLinkRenderer?.thumbnail); this.duration = { - text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text, - seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text) + text: data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString(), + seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString()) }; this.endpoint = new NavigationEndpoint(data.navigationEndpoint); diff --git a/src/parser/classes/MusicSortFilterButton.ts b/src/parser/classes/MusicSortFilterButton.ts index 0ff6b0134..ec16e01f4 100644 --- a/src/parser/classes/MusicSortFilterButton.ts +++ b/src/parser/classes/MusicSortFilterButton.ts @@ -14,7 +14,7 @@ class MusicSortFilterButton extends YTNode { constructor(data: any) { super(); - this.title = new Text(data.title).text; + this.title = new Text(data.title).toString(); this.icon_type = data.icon?.icon_type || null; this.menu = Parser.parseItem(data.menu, MusicMultiSelectMenu); } diff --git a/src/parser/classes/PlaylistVideo.ts b/src/parser/classes/PlaylistVideo.ts index ab67fee60..9783fbdd4 100644 --- a/src/parser/classes/PlaylistVideo.ts +++ b/src/parser/classes/PlaylistVideo.ts @@ -39,7 +39,7 @@ class PlaylistVideo extends YTNode { this.is_playable = data.isPlayable; this.menu = Parser.parseItem(data.menu); this.duration = { - text: new Text(data.lengthText).text, + text: new Text(data.lengthText).toString(), seconds: parseInt(data.lengthSeconds) }; } diff --git a/src/parser/classes/Video.ts b/src/parser/classes/Video.ts index 3714b9f2d..362d94153 100644 --- a/src/parser/classes/Video.ts +++ b/src/parser/classes/Video.ts @@ -77,8 +77,8 @@ class Video extends YTNode { } this.duration = { - text: data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text, - seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).text : new Text(overlay_time_status).text) + text: data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString(), + seconds: timeToSeconds(data.lengthText ? new Text(data.lengthText).toString() : new Text(overlay_time_status).toString()) }; this.show_action_menu = data.showActionMenu; diff --git a/src/parser/classes/menus/MusicMultiSelectMenu.ts b/src/parser/classes/menus/MusicMultiSelectMenu.ts index aa8045d17..f1b7d2a6d 100644 --- a/src/parser/classes/menus/MusicMultiSelectMenu.ts +++ b/src/parser/classes/menus/MusicMultiSelectMenu.ts @@ -13,7 +13,7 @@ class MusicMultiSelectMenu extends YTNode { constructor(data: any) { super(); - this.title = new Text(data.title.musicMenuTitleRenderer?.primaryText).text; + this.title = new Text(data.title.musicMenuTitleRenderer?.primaryText).toString(); this.options = Parser.parseArray(data.options, [ MusicMultiSelectMenuItem, MusicMenuItemDivider ]); } } diff --git a/src/parser/classes/menus/MusicMultiSelectMenuItem.ts b/src/parser/classes/menus/MusicMultiSelectMenuItem.ts index 09f2153e8..3d54964c3 100644 --- a/src/parser/classes/menus/MusicMultiSelectMenuItem.ts +++ b/src/parser/classes/menus/MusicMultiSelectMenuItem.ts @@ -14,7 +14,7 @@ class MusicMultiSelectMenuItem extends YTNode { constructor(data: any) { super(); - this.title = new Text(data.title).text; + this.title = new Text(data.title).toString(); this.form_item_entity_key = data.formItemEntityKey; this.selected_icon_type = data.selectedIcon?.iconType || null; this.endpoint = data.selectedCommand ? new NavigationEndpoint(data.selectedCommand) : null; diff --git a/src/parser/classes/misc/Author.ts b/src/parser/classes/misc/Author.ts index 37ca931ef..22bd0cfbb 100644 --- a/src/parser/classes/misc/Author.ts +++ b/src/parser/classes/misc/Author.ts @@ -1,9 +1,9 @@ import Parser from '../../index.js'; -import NavigatableText from './NavigatableText.js'; import NavigationEndpoint from '../NavigationEndpoint.js'; import TextRun from './TextRun.js'; import Thumbnail from './Thumbnail.js'; import Constants from '../../../utils/Constants.js'; +import Text from './Text.js'; class Author { #nav_text; @@ -11,14 +11,14 @@ class Author { id: string; name: string; thumbnails: Thumbnail[]; - endpoint: NavigationEndpoint | null; + endpoint?: NavigationEndpoint; badges?: any; is_verified?: boolean | null; is_verified_artist?: boolean | null; url: string | null; constructor(item: any, badges?: any, thumbs?: any) { - this.#nav_text = new NavigatableText(item); + this.#nav_text = new Text(item); this.id = (this.#nav_text.runs?.[0] as TextRun)?.endpoint?.payload?.browseId || diff --git a/src/parser/classes/misc/NavigatableText.ts b/src/parser/classes/misc/NavigatableText.ts deleted file mode 100644 index 961ef6d65..000000000 --- a/src/parser/classes/misc/NavigatableText.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Text from './Text.js'; -import NavigationEndpoint from '../NavigationEndpoint.js'; - -class NavigatableText extends Text { - static type = 'NavigatableText'; - - endpoint: NavigationEndpoint | null; - - constructor(node: any) { - super(node); - // TODO: is this needed? Text now supports this itself - this.endpoint = - node?.runs?.[0]?.navigationEndpoint ? - new NavigationEndpoint(node?.runs[0].navigationEndpoint) : - node?.navigationEndpoint ? - new NavigationEndpoint(node?.navigationEndpoint) : - node?.titleNavigationEndpoint ? - new NavigationEndpoint(node?.titleNavigationEndpoint) : null; - } - - toJSON(): NavigatableText { - return this; - } -} - -export default NavigatableText; \ No newline at end of file diff --git a/src/parser/classes/misc/Text.ts b/src/parser/classes/misc/Text.ts index d13ff191a..48c1d18ac 100644 --- a/src/parser/classes/misc/Text.ts +++ b/src/parser/classes/misc/Text.ts @@ -1,5 +1,6 @@ import TextRun from './TextRun.js'; import EmojiRun from './EmojiRun.js'; +import NavigationEndpoint from '../NavigationEndpoint.js'; export interface Run { text: string; @@ -17,8 +18,9 @@ export function escape(text: string) { } class Text { - text: string; + text?: string; runs; + endpoint?: NavigationEndpoint; constructor(data: any) { if (data?.hasOwnProperty('runs') && Array.isArray(data.runs)) { @@ -28,16 +30,28 @@ class Text { ); this.text = this.runs.map((run) => run.text).join(''); } else { - this.text = data?.simpleText || 'N/A'; + this.text = data?.simpleText; + } + if (data && Reflect.has(data, 'navigationEndpoint')) { + this.endpoint = new NavigationEndpoint(data.navigationEndpoint); + } + if (data && Reflect.has(data, 'titleNavigationEndpoint')) { + this.endpoint = new NavigationEndpoint(data.titleNavigationEndpoint); } + if (!this.endpoint) + this.endpoint = (this.runs?.[0] as TextRun)?.endpoint; } toHTML() { return this.runs ? this.runs.map((run) => run.toHTML()).join('') : this.text; } + isEmpty() { + return this.text === undefined; + } + toString() { - return this.text; + return this.text || 'N/A'; } } From 6e9f1136248001d0ef1af38cd56d0d056c79425c Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Wed, 8 Feb 2023 22:27:05 +0200 Subject: [PATCH 02/17] fix(Text): data might not be object --- src/parser/classes/misc/Text.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/classes/misc/Text.ts b/src/parser/classes/misc/Text.ts index 48c1d18ac..87899ef19 100644 --- a/src/parser/classes/misc/Text.ts +++ b/src/parser/classes/misc/Text.ts @@ -32,10 +32,10 @@ class Text { } else { this.text = data?.simpleText; } - if (data && Reflect.has(data, 'navigationEndpoint')) { + if (typeof data === 'object' && data !== null && Reflect.has(data, 'navigationEndpoint')) { this.endpoint = new NavigationEndpoint(data.navigationEndpoint); } - if (data && Reflect.has(data, 'titleNavigationEndpoint')) { + if (typeof data === 'object' && data !== null && Reflect.has(data, 'titleNavigationEndpoint')) { this.endpoint = new NavigationEndpoint(data.titleNavigationEndpoint); } if (!this.endpoint) From 1a861b0ad1d9792c48912d41796bd94058a5fbc0 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Wed, 8 Feb 2023 22:34:35 +0200 Subject: [PATCH 03/17] refactor: remove GetParserByName from map --- scripts/build-parser-map.cjs | 17 ----------------- src/parser/map.ts | 19 ------------------- 2 files changed, 36 deletions(-) diff --git a/scripts/build-parser-map.cjs b/scripts/build-parser-map.cjs index de10d0345..cf7c184c9 100644 --- a/scripts/build-parser-map.cjs +++ b/scripts/build-parser-map.cjs @@ -39,22 +39,5 @@ export const YTNodes = { export const Misc = { ${misc_exports.join(',\n ')} }; - -const map: Record = YTNodes; - -/** - * @param name - Name of the node to be parsed - */ -export default function GetParserByName(name: string) { - const ParserConstructor = map[name]; - - if (!ParserConstructor) { - const error = new Error(\`Module not found: \${name}\`); - (error as any).code = 'MODULE_NOT_FOUND'; - throw error; - } - - return ParserConstructor; -} ` ); \ No newline at end of file diff --git a/src/parser/map.ts b/src/parser/map.ts index 26242c502..eec14e39d 100644 --- a/src/parser/map.ts +++ b/src/parser/map.ts @@ -180,7 +180,6 @@ import { default as Author } from './classes/misc/Author.js'; import { default as ChildElement } from './classes/misc/ChildElement.js'; import { default as EmojiRun } from './classes/misc/EmojiRun.js'; import { default as Format } from './classes/misc/Format.js'; -import { default as NavigatableText } from './classes/misc/NavigatableText.js'; import { default as PlaylistAuthor } from './classes/misc/PlaylistAuthor.js'; import { default as Text } from './classes/misc/Text.js'; import { default as TextRun } from './classes/misc/TextRun.js'; @@ -657,27 +656,9 @@ export const Misc = { ChildElement, EmojiRun, Format, - NavigatableText, PlaylistAuthor, Text, TextRun, Thumbnail, VideoDetails }; - -const map: Record = YTNodes; - -/** - * @param name - Name of the node to be parsed - */ -export default function GetParserByName(name: string) { - const ParserConstructor = map[name]; - - if (!ParserConstructor) { - const error = new Error(`Module not found: ${name}`); - (error as any).code = 'MODULE_NOT_FOUND'; - throw error; - } - - return ParserConstructor; -} From 66592054ea56323810a50c1e9c0b9fccc94d0001 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Wed, 8 Feb 2023 22:40:00 +0200 Subject: [PATCH 04/17] feat(Parser): just-in-time YTNode generation --- src/parser/generator.ts | 569 ++++++++++++++++++++++++++++++++++++++++ src/parser/parser.ts | 35 ++- 2 files changed, 602 insertions(+), 2 deletions(-) create mode 100644 src/parser/generator.ts diff --git a/src/parser/generator.ts b/src/parser/generator.ts new file mode 100644 index 000000000..e34adf968 --- /dev/null +++ b/src/parser/generator.ts @@ -0,0 +1,569 @@ +/* eslint-disable no-cond-assign */ +import { Platform } from '../utils/Utils.js'; +import Author from './classes/misc/Author.js'; +import Text from './classes/misc/Text.js'; +import Thumbnail from './classes/misc/Thumbnail.js'; +import NavigationEndpoint from './classes/NavigationEndpoint.js'; +import { YTNode, YTNodeConstructor } from './helpers.js'; +import Parser from './parser.js'; + +export type MiscInferenceType = { + type: 'misc', + misc_type: 'NavigationEndpoint', + optional: boolean, + endpoint: NavigationEndpoint +} | { + type: 'misc', + misc_type: 'Text', + optional: boolean, + text: string, + endpoint?: NavigationEndpoint +} | { + type: 'misc', + misc_type: 'Thumbnail', + optional: boolean, +} | { + type: 'misc', + misc_type: 'Author', + optional: boolean, + params: [string, string?], +} + +export type InferenceType = { + type: 'renderer', + renderers: Record, + optional: boolean, +} | { + type: 'renderer_list', + renderers: Record, + optional: boolean, +} | MiscInferenceType | { + type: 'unknown', + optional: boolean, +}; + +export type KeyInfo = (readonly [string, InferenceType])[]; + +export class YTNodeGenerator { + static #ignored_keys = new Set([ + 'trackingParams', 'accessibility', 'accessibilityData' + ]); + static get ignored_keys() { + return this.#ignored_keys; + } + static camelToSnake(str: string) { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + } + static isIgnoredKey(key: string | symbol) { + return typeof key === 'string' && this.ignored_keys.has(key); + } + static resolveKeyInfo(key_info: KeyInfo, new_key_info: KeyInfo) { + const changed_keys = new Map(); + const current_keys = new Set(key_info.map(([ key ]) => key)); + const new_keys = new Set(new_key_info.map(([ key ]) => key)); + + const added_keys = new_key_info.filter(([ key ]) => !current_keys.has(key)); + const removed_keys = key_info.filter(([ key ]) => !new_keys.has(key)); + + const common_keys = key_info.filter(([ key ]) => new_keys.has(key)); + + const new_key_map = new Map(new_key_info); + + for (const [ key, type ] of common_keys) { + const new_type = new_key_map.get(key); + if (!new_type) continue; + 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', + optional: true + }); + continue; + } + // We've got the same type, so we can now resolve the changes + switch (type.type) { + case 'renderer': + { + if (new_type.type !== 'renderer') continue; + const union_map = { + ...type.renderers, + ...new_type.renderers + }; + const either_optional = type.optional || new_type.optional; + const resolved_key: InferenceType = { + 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 'renderer_list': + { + 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); + } + break; + case 'misc': + { + if (new_type.type !== 'misc') continue; + 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', + optional: true + }); + } + switch (type.misc_type) { + case 'Author': + { + if (new_type.misc_type !== 'Author') break; + const had_optional_param = type.params[1] || new_type.params[1]; + const either_optional = type.optional || new_type.optional; + const resolved_key: MiscInferenceType = { + type: 'misc', + misc_type: 'Author', + optional: either_optional, + params: [ new_type.params[0], had_optional_param ] + }; + const did_change = JSON.stringify(resolved_key) !== JSON.stringify(type); + if (did_change) changed_keys.set(key, resolved_key); + } + break; + // Other cases can not change + } + } + break; + } + } + + for (const [ key, type ] of added_keys) { + changed_keys.set(key, { + ...type, + optional: true + }); + } + + for (const [ key, type ] of removed_keys) { + changed_keys.set(key, { + ...type, + optional: true + }); + } + + const unchanged_keys = key_info.filter(([ key ]) => !changed_keys.has(key)); + + const resolved_key_info_map = new Map([ ...unchanged_keys, ...changed_keys ]); + const resolved_key_info = [ ...resolved_key_info_map.entries() ]; + + return { + resolved_key_info, + changed_keys: [ ...changed_keys.entries() ] + }; + } + static createRuntimeClass(classname: string, key_info: KeyInfo): YTNodeConstructor { + this.logNewClass(classname, key_info); + const node = class extends YTNode { + static type = classname; + static #key_info = new Map(); + static set key_info(key_info: KeyInfo) { + this.#key_info = new Map(key_info); + } + static get key_info() { + return [ ...this.#key_info.entries() ]; + } + constructor(data: any) { + super(); + const { + key_info, + unimplemented_dependencies + } = YTNodeGenerator.introspectWithDependencies(data); + + const { + resolved_key_info, + changed_keys + } = YTNodeGenerator.resolveKeyInfo(node.key_info, key_info); + + const did_change = changed_keys.length > 0; + + if (did_change) { + node.key_info = resolved_key_info; + YTNodeGenerator.logChangedKeys(classname, node.key_info, changed_keys); + } + + for (const [ name, data ] of unimplemented_dependencies) + YTNodeGenerator.generateRuntimeClass(name, data); + + for (const [ key, value ] of key_info) { + let snake_key = YTNodeGenerator.camelToSnake(key); + if (value.type === 'misc' && value.misc_type === 'NavigationEndpoint') + snake_key = 'endpoint'; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this[snake_key] = YTNodeGenerator.inferenceTypeParseValue(key, value, data); + } + } + }; + node.key_info = key_info; + Object.defineProperty(node, 'name', { value: classname, writable: false }); + return node; + } + static logNewClass(classname: string, key_info: KeyInfo) { + console.warn(`${classname} not found!\nThis 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}!\nIntrospected and JIT generated this class in the meantime:\n${this.generateTypescriptClass(classname, key_info)}`); + } + static logChangedKeys(classname: string, key_info: KeyInfo, changed_keys: KeyInfo) { + console.warn(`${classname} changed!\nThe following keys where altered: ${changed_keys.map(([ key ]) => this.camelToSnake(key)).join(', ')}\nThe class has changed to:\n${this.generateTypescriptClass(classname, key_info)}`); + } + static stringifyKeyInfo(key_info: KeyInfo) { + return JSON.stringify(key_info, (key, value) => { + if (typeof value === 'object' && value !== null && Reflect.has(value, 'renderers')) { + return { + ...value, + renderers: Object.keys(value.renderers) + }; + } + return value; + }); + } + static parseKeyInfo(key_info: string) { + return JSON.parse(key_info, (key, value) => { + if (typeof value === 'object' && value !== null && Reflect.has(value, 'renderers')) { + return { + ...value, + renderers: Object.fromEntries(value.renderers.map((keys: any) => [ keys, null ])) + }; + } + return value; + }) as KeyInfo; + } + static introspectWithDependencies(classdata: string) { + const key_info = this.introspect(classdata); + const dependencies = new Map(); + for (const [ , value ] of key_info) { + if (value.type === 'renderer' || value.type === 'renderer_list') + Object.entries(value.renderers).forEach(([ key, value ]) => dependencies.set(key, value)); + } + const unimplemented_dependencies = Array.from(dependencies).filter(([ classname ]) => !this.isImplemented(classname)); + + return { + key_info, + unimplemented_dependencies + }; + } + static generateRuntimeClass(classname: string, classdata: any) { + const { + key_info, + unimplemented_dependencies + } = this.introspectWithDependencies(classdata); + + const JITNode = this.createRuntimeClass(classname, key_info); + Parser.addRuntimeParser(classname, JITNode); + + for (const [ name, data ] of unimplemented_dependencies) + this.generateRuntimeClass(name, data); + + return JITNode; + } + static generateTypescriptClass(classname: string, key_info: KeyInfo) { + const props: string[] = []; + const constructor_lines = [ + 'super();' + ]; + for (const [ key, value ] of key_info) { + let snake_key = this.camelToSnake(key); + if (value.type === 'misc' && value.misc_type === 'NavigationEndpoint') + snake_key = 'endpoint'; + props.push(`${snake_key}${value.optional ? '?' : ''}: ${this.inferenceTypeToTS(value)};`); + constructor_lines.push(`this.${snake_key} = ${this.inferenceTypeToParser(key, value)};`); + } + return `class ${classname} extends YTNode {\n static type = '${classname}';\n\n ${props.join('\n ')}\n\n constructor(data: any) {\n ${constructor_lines.join('\n ')}\n }\n}\n`; + } + static isImplemented(classname: string) { + return Parser.hasParser(classname); + } + static inferenceTypeToTS(inference_type: InferenceType) { + switch (inference_type.type) { + case 'renderer': + { + const renderers = Object.entries(inference_type.renderers); + return `${renderers.map(([ type ]) => `YTNodes.${type}`).join(' | ')}`; + } + case 'renderer_list': + { + const renderers = Object.entries(inference_type.renderers); + return `ObservedArray<${renderers.map(([ type ]) => `YTNodes.${type}`).join(' | ')}>`; + } + case 'misc': + switch (inference_type.misc_type) { + case 'NavigationEndpoint': + return 'NavigationEndpoint'; + case 'Text': + return 'Text'; + case 'Thumbnail': + return 'Thumbnail[]'; + case 'Author': + return 'Author'; + } + throw new Error('Unreachable code reached! Switch missing case!'); + case 'unknown': + return '/* TODO: determine correct type */ unknown'; + } + } + static inferenceTypeToParser(key: string, inference_type: InferenceType) { + let parser = 'undefined'; + switch (inference_type.type) { + case 'renderer': + { + const renderers = Object.entries(inference_type.renderers); + parser = `Parser.parseItem(data.${key}, [${renderers.map(([ type ]) => `YTNodes.${type}`).join(', ')}])`; + } + break; + case 'renderer_list': + { + const renderers = Object.entries(inference_type.renderers); + parser = `Parser.parse(data.${key}, true, [${renderers.map(([ type ]) => `YTNodes.${type}`).join(', ')}])`; + } + break; + case 'misc': + switch (inference_type.misc_type) { + case 'NavigationEndpoint': + parser = `new NavigationEndpoint(data.${key})`; + break; + case 'Text': + parser = `new Text(data.${key})`; + break; + case 'Thumbnail': + parser = `Thumbnail.fromResponse(data.${key})`; + break; + case 'Author': + { + const author_parser = `new Author(data.${inference_type.params[0]}, ${inference_type.params[1] ? `data.${inference_type.params[1]}` : 'undefined'})`; + if (inference_type.optional) + return `Reflect.has(data, '${inference_type.params[0]}') ? ${author_parser} : undefined`; + return author_parser; + } + } + if (parser === 'undefined') + throw new Error('Unreachable code reached! Switch missing case!'); + break; + case 'unknown': + parser = `data.${key}`; + break; + } + if (inference_type.optional) + return `Reflect.has(data, '${key}') ? ${parser} : undefined`; + return parser; + } + static inferenceTypeParseValue(key: string, inference_type: InferenceType, data: any) { + const should_optional = !inference_type.optional || Reflect.has(data, key); + switch (inference_type.type) { + case 'renderer': + { + const renderers = Object.entries(inference_type.renderers); + return should_optional ? Parser.parseItem(data[key], renderers.map(([ type ]) => Parser.getParserByName(type))) : undefined; + } + case 'renderer_list': + { + const renderers = Object.entries(inference_type.renderers); + return should_optional ? Parser.parse(data[key], true, renderers.map(([ type ]) => Parser.getParserByName(type))) : undefined; + } + case 'misc': + switch (inference_type.misc_type) { + case 'NavigationEndpoint': + return should_optional ? new NavigationEndpoint(data[key]) : undefined; + case 'Text': + return should_optional ? new Text(data[key]) : undefined; + case 'Thumbnail': + return should_optional ? Thumbnail.fromResponse(data[key]) : undefined; + case 'Author': + { + const author_should_optional = !inference_type.optional || Reflect.has(data, inference_type.params[0]); + return author_should_optional ? new Author(data[inference_type.params[0]], inference_type.params[1] ? data[inference_type.params[1]] : undefined) : undefined; + } + } + throw new Error('Unreachable code reached! Switch missing case!'); + case 'unknown': + return data[key]; + } + } + static #passOne(classdata: any) { + const keys = Reflect.ownKeys(classdata).filter((key) => !this.isIgnoredKey(key)).filter((key) => typeof key === 'string') as string[]; + const key_info = keys.map((key) => { + const value = classdata[key]; + const inferred_type = this.inferType(key as string, value); + return [ + key, + inferred_type + ] as const; + }); + return key_info; + } + static #passTwo(key_info: KeyInfo) { + // The second pass will detect Author + const channel_nav = key_info.filter(([ , value ]) => { + if (value.type !== 'misc') return false; + if (!(value.misc_type === 'NavigationEndpoint' || value.misc_type === 'Text')) return false; + return value.endpoint?.metadata.page_type === 'WEB_PAGE_TYPE_CHANNEL'; + }); + + // Whichever one has the longest text is the most probable match + const most_probable_match = channel_nav.sort(([ , a ], [ , b ]) => { + if (a.type !== 'misc' || b.type !== 'misc') return 0; + if (a.misc_type !== 'Text' || b.misc_type !== 'Text') return 0; + return b.text.length - a.text.length; + }); + + const excluded_keys = new Set(); + + const cannonical_channel_nav = most_probable_match[0]; + + let author: MiscInferenceType | undefined; + // We've found an author + if (cannonical_channel_nav) { + excluded_keys.add(cannonical_channel_nav[0]); + // Now to locate its metadata + // We'll first get all the keys in the classdata + const keys = key_info.map(([ key ]) => key); + // Check for anything ending in 'Badges' equals 'badges' + const badges = keys.filter((key) => key.endsWith('Badges') || key === 'badges'); + // The likely candidate is the one with some prefix (owner, author) + const likely_badges = badges.filter((key) => key.startsWith('owner') || key.startsWith('author')); + // If we have a likely candidate, we'll use that + const cannonical_badges = likely_badges[0] ?? badges[0]; + // Now we have the author and its badges + // 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') : + false; + + if (is_badges && cannonical_badges) excluded_keys.add(cannonical_badges); + // TODO: next we check for the author's thumbnail + author = { + type: 'misc', + misc_type: 'Author', + optional: false, + params: [ + cannonical_channel_nav[0], + is_badges ? cannonical_badges : undefined + ] + }; + } + + if (author) { + key_info.push([ 'author', author ]); + } + + return key_info.filter(([ key ]) => !excluded_keys.has(key)); + } + static introspect(classdata: any) { + const key_info = this.#passOne(classdata); + return this.#passTwo(key_info); + } + static inferType(key: string, value: any): InferenceType { + let return_value: string | Record | boolean | MiscInferenceType = false; + if (return_value = this.isRenderer(value)) { + return { + type: 'renderer', + renderers: { + [return_value]: value[Reflect.ownKeys(value)[0]] + }, + optional: false + }; + } + if (return_value = this.isRendererList(value)) { + return { + type: 'renderer_list', + renderers: return_value, + optional: false + }; + } + if (return_value = this.isMiscType(key, value)) { + return return_value as MiscInferenceType; + } + return { + type: 'unknown', + optional: false + }; + } + static isRendererList(value: any) { + const arr = Array.isArray(value); + const is_list = arr && value.every((item) => this.isRenderer(item)); + return ( + is_list ? + Object.fromEntries(value.map((item) => { + const key = Reflect.ownKeys(item)[0].toString(); + return [ this.getRendererClass(key), item[key] ]; + })) : + false + ); + } + static isMiscType(key: string, value: any): MiscInferenceType | false { + // NavigationEndpoint + if ((key.endsWith('Endpoint') || key.endsWith('Command')) && typeof value === 'object') { + return { + type: 'misc', + endpoint: new NavigationEndpoint(value), + optional: false, + misc_type: 'NavigationEndpoint' + }; + } + // Text + if (typeof value === 'object' && (Reflect.has(value, 'simpleText') || Reflect.has(value, 'runs'))) { + const textNode = new Text(value); + return { + type: 'misc', + misc_type: 'Text', + optional: false, + endpoint: textNode.endpoint, + text: textNode.toString() + }; + } + // Thumbnail + if (typeof value === 'object' && Reflect.has(value, 'thumbnails') && Array.isArray(value.thumbnails)) { + return { + type: 'misc', + misc_type: 'Thumbnail', + optional: false + }; + } + return false; + } + static getRendererClass(renderer: string) { + // Remove 'Renderer' from the end of the string and capitalize the first letter + return renderer.replace(/Renderer$/, '').replace(/^[a-z]/, (letter) => letter.toUpperCase()); + } + static isRenderer(value: any) { + const is_object = typeof value === 'object'; + if (!is_object) return false; + const keys = Reflect.ownKeys(value); + if (keys.length === 1 && keys[0].toString().includes('Renderer')) { + return this.getRendererClass(keys[0].toString()); + } + return false; + } +} diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 50fbaa98b..8c8a653f2 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -22,7 +22,8 @@ import Thumbnail from './classes/misc/Thumbnail.js'; import { InnertubeError, ParsingError, Platform } from '../utils/Utils.js'; import { Memo, observe, ObservedArray, SuperParsedResult, YTNode, YTNodeConstructor } from './helpers.js'; -import GetParserByName from './map.js'; +import { YTNodes } from './map.js'; +import { YTNodeGenerator } from './generator.js'; export type ParserError = { classname: string, classdata: any, err: any }; export type ParserErrorHandler = (error: ParserError) => void; @@ -283,7 +284,9 @@ export default class Parser { if (!this.shouldIgnore(classname)) { try { - const TargetClass = GetParserByName(classname); + const has_target_class = this.hasParser(classname); + + const TargetClass = has_target_class ? this.getParserByName(classname) : YTNodeGenerator.generateRuntimeClass(classname, data[keys[0]]); if (validTypes) { if (Array.isArray(validTypes)) { @@ -488,6 +491,34 @@ export default class Parser { static shouldIgnore(classname: string) { return this.ignore_list.has(classname); } + + static #rt_nodes = new Map(Array.from(Object.entries(YTNodes))); + static #dynamic_nodes = new Map(); + + static getParserByName(classname: string) { + const ParserConstructor = this.#rt_nodes.get(classname); + + if (!ParserConstructor) { + const error = new Error(`Module not found: ${classname}`); + (error as any).code = 'MODULE_NOT_FOUND'; + throw error; + } + + return ParserConstructor; + } + + static hasParser(classname: string) { + return this.#rt_nodes.has(classname); + } + + static addRuntimeParser(classname: string, ParserConstructor: YTNodeConstructor) { + this.#rt_nodes.set(classname, ParserConstructor); + this.#dynamic_nodes.set(classname, ParserConstructor); + } + + static getDynamicParsers() { + return Object.fromEntries(this.#dynamic_nodes); + } } // Continuation From 31c7d854ec29858aabcf9c592ac595cbd1203baf Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Mon, 27 Feb 2023 10:26:49 +0200 Subject: [PATCH 05/17] refactor: cleanup YTNodeGenerator --- src/parser/generator.ts | 146 +++++++++++++++------------------------- 1 file changed, 53 insertions(+), 93 deletions(-) diff --git a/src/parser/generator.ts b/src/parser/generator.ts index e34adf968..80f718edc 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -31,11 +31,11 @@ export type MiscInferenceType = { export type InferenceType = { type: 'renderer', - renderers: Record, + renderers: string[], optional: boolean, } | { type: 'renderer_list', - renderers: Record, + renderers: string[], optional: boolean, } | MiscInferenceType | { type: 'unknown', @@ -48,16 +48,20 @@ export class YTNodeGenerator { static #ignored_keys = new Set([ 'trackingParams', 'accessibility', 'accessibilityData' ]); - static get ignored_keys() { - return this.#ignored_keys; - } - static camelToSnake(str: string) { + static #renderers_examples: Record = {}; + static #camelToSnake(str: string) { return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } + static #logNewClass(classname: string, key_info: KeyInfo) { + console.warn(`${classname} not found!\nThis 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}!\nIntrospected and JIT generated this class in the meantime:\n${this.generateTypescriptClass(classname, key_info)}`); + } + static #logChangedKeys(classname: string, key_info: KeyInfo, changed_keys: KeyInfo) { + console.warn(`${classname} changed!\nThe following keys where altered: ${changed_keys.map(([ key ]) => this.#camelToSnake(key)).join(', ')}\nThe class has changed to:\n${this.generateTypescriptClass(classname, key_info)}`); + } static isIgnoredKey(key: string | symbol) { - return typeof key === 'string' && this.ignored_keys.has(key); + return typeof key === 'string' && this.#ignored_keys.has(key); } - static resolveKeyInfo(key_info: KeyInfo, new_key_info: KeyInfo) { + static mergeKeyInfo(key_info: KeyInfo, new_key_info: KeyInfo) { const changed_keys = new Map(); const current_keys = new Set(key_info.map(([ key ]) => key)); const new_keys = new Set(new_key_info.map(([ key ]) => key)); @@ -186,7 +190,7 @@ export class YTNodeGenerator { }; } static createRuntimeClass(classname: string, key_info: KeyInfo): YTNodeConstructor { - this.logNewClass(classname, key_info); + this.#logNewClass(classname, key_info); const node = class extends YTNode { static type = classname; static #key_info = new Map(); @@ -201,30 +205,28 @@ export class YTNodeGenerator { const { key_info, unimplemented_dependencies - } = YTNodeGenerator.introspectWithDependencies(data); + } = YTNodeGenerator.introspect(data); const { resolved_key_info, changed_keys - } = YTNodeGenerator.resolveKeyInfo(node.key_info, key_info); + } = YTNodeGenerator.mergeKeyInfo(node.key_info, key_info); const did_change = changed_keys.length > 0; if (did_change) { node.key_info = resolved_key_info; - YTNodeGenerator.logChangedKeys(classname, node.key_info, changed_keys); + YTNodeGenerator.#logChangedKeys(classname, node.key_info, changed_keys); } for (const [ name, data ] of unimplemented_dependencies) YTNodeGenerator.generateRuntimeClass(name, data); for (const [ key, value ] of key_info) { - let snake_key = YTNodeGenerator.camelToSnake(key); + let snake_key = YTNodeGenerator.#camelToSnake(key); if (value.type === 'misc' && value.misc_type === 'NavigationEndpoint') snake_key = 'endpoint'; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this[snake_key] = YTNodeGenerator.inferenceTypeParseValue(key, value, data); + Reflect.set(this, snake_key, YTNodeGenerator.parse(key, value, data)); } } }; @@ -232,42 +234,18 @@ export class YTNodeGenerator { Object.defineProperty(node, 'name', { value: classname, writable: false }); return node; } - static logNewClass(classname: string, key_info: KeyInfo) { - console.warn(`${classname} not found!\nThis 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}!\nIntrospected and JIT generated this class in the meantime:\n${this.generateTypescriptClass(classname, key_info)}`); - } - static logChangedKeys(classname: string, key_info: KeyInfo, changed_keys: KeyInfo) { - console.warn(`${classname} changed!\nThe following keys where altered: ${changed_keys.map(([ key ]) => this.camelToSnake(key)).join(', ')}\nThe class has changed to:\n${this.generateTypescriptClass(classname, key_info)}`); - } - static stringifyKeyInfo(key_info: KeyInfo) { - return JSON.stringify(key_info, (key, value) => { - if (typeof value === 'object' && value !== null && Reflect.has(value, 'renderers')) { - return { - ...value, - renderers: Object.keys(value.renderers) - }; - } - return value; - }); - } - static parseKeyInfo(key_info: string) { - return JSON.parse(key_info, (key, value) => { - if (typeof value === 'object' && value !== null && Reflect.has(value, 'renderers')) { - return { - ...value, - renderers: Object.fromEntries(value.renderers.map((keys: any) => [ keys, null ])) - }; - } - return value; - }) as KeyInfo; - } - static introspectWithDependencies(classdata: string) { - const key_info = this.introspect(classdata); + static introspect(classdata: string) { + const key_info = this.#introspect(classdata); const dependencies = new Map(); for (const [ , value ] of key_info) { if (value.type === 'renderer' || value.type === 'renderer_list') - Object.entries(value.renderers).forEach(([ key, value ]) => dependencies.set(key, value)); + value.renderers.forEach((renderer) => { + const example = this.#renderers_examples.get(renderer); + if (example) + dependencies.set(renderer, example); + }); } - const unimplemented_dependencies = Array.from(dependencies).filter(([ classname ]) => !this.isImplemented(classname)); + const unimplemented_dependencies = Array.from(dependencies).filter(([ classname ]) => !Parser.hasParser(classname)); return { key_info, @@ -278,7 +256,7 @@ export class YTNodeGenerator { const { key_info, unimplemented_dependencies - } = this.introspectWithDependencies(classdata); + } = this.introspect(classdata); const JITNode = this.createRuntimeClass(classname, key_info); Parser.addRuntimeParser(classname, JITNode); @@ -294,68 +272,51 @@ export class YTNodeGenerator { 'super();' ]; for (const [ key, value ] of key_info) { - let snake_key = this.camelToSnake(key); + let snake_key = this.#camelToSnake(key); if (value.type === 'misc' && value.misc_type === 'NavigationEndpoint') snake_key = 'endpoint'; - props.push(`${snake_key}${value.optional ? '?' : ''}: ${this.inferenceTypeToTS(value)};`); - constructor_lines.push(`this.${snake_key} = ${this.inferenceTypeToParser(key, value)};`); + props.push(`${snake_key}${value.optional ? '?' : ''}: ${this.toTypeDeclaration(value)};`); + constructor_lines.push(`this.${snake_key} = ${this.toParser(key, value)};`); } return `class ${classname} extends YTNode {\n static type = '${classname}';\n\n ${props.join('\n ')}\n\n constructor(data: any) {\n ${constructor_lines.join('\n ')}\n }\n}\n`; } - static isImplemented(classname: string) { - return Parser.hasParser(classname); - } - static inferenceTypeToTS(inference_type: InferenceType) { + static toTypeDeclaration(inference_type: InferenceType) { switch (inference_type.type) { case 'renderer': { - const renderers = Object.entries(inference_type.renderers); - return `${renderers.map(([ type ]) => `YTNodes.${type}`).join(' | ')}`; + return `${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')}`; } case 'renderer_list': { - const renderers = Object.entries(inference_type.renderers); - return `ObservedArray<${renderers.map(([ type ]) => `YTNodes.${type}`).join(' | ')}>`; + return `ObservedArray<${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')}>`; } case 'misc': switch (inference_type.misc_type) { - case 'NavigationEndpoint': - return 'NavigationEndpoint'; - case 'Text': - return 'Text'; case 'Thumbnail': return 'Thumbnail[]'; - case 'Author': - return 'Author'; + default: + return inference_type.misc_type; } throw new Error('Unreachable code reached! Switch missing case!'); case 'unknown': return '/* TODO: determine correct type */ unknown'; } } - static inferenceTypeToParser(key: string, inference_type: InferenceType) { + static toParser(key: string, inference_type: InferenceType) { let parser = 'undefined'; switch (inference_type.type) { case 'renderer': { - const renderers = Object.entries(inference_type.renderers); - parser = `Parser.parseItem(data.${key}, [${renderers.map(([ type ]) => `YTNodes.${type}`).join(', ')}])`; + parser = `Parser.parseItem(data.${key}, [${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')}])`; } break; case 'renderer_list': { - const renderers = Object.entries(inference_type.renderers); - parser = `Parser.parse(data.${key}, true, [${renderers.map(([ type ]) => `YTNodes.${type}`).join(', ')}])`; + parser = `Parser.parse(data.${key}, true, [${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')}])`; } break; case 'misc': switch (inference_type.misc_type) { - case 'NavigationEndpoint': - parser = `new NavigationEndpoint(data.${key})`; - break; - case 'Text': - parser = `new Text(data.${key})`; - break; case 'Thumbnail': parser = `Thumbnail.fromResponse(data.${key})`; break; @@ -366,6 +327,9 @@ export class YTNodeGenerator { return `Reflect.has(data, '${inference_type.params[0]}') ? ${author_parser} : undefined`; return author_parser; } + default: + parser = `new ${inference_type.misc_type}(data.${key})`; + break; } if (parser === 'undefined') throw new Error('Unreachable code reached! Switch missing case!'); @@ -378,18 +342,16 @@ export class YTNodeGenerator { return `Reflect.has(data, '${key}') ? ${parser} : undefined`; return parser; } - static inferenceTypeParseValue(key: string, inference_type: InferenceType, data: any) { + static parse(key: string, inference_type: InferenceType, data: any) { const should_optional = !inference_type.optional || Reflect.has(data, key); switch (inference_type.type) { case 'renderer': { - const renderers = Object.entries(inference_type.renderers); - return should_optional ? Parser.parseItem(data[key], renderers.map(([ type ]) => Parser.getParserByName(type))) : undefined; + return should_optional ? Parser.parseItem(data[key], inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined; } case 'renderer_list': { - const renderers = Object.entries(inference_type.renderers); - return should_optional ? Parser.parse(data[key], true, renderers.map(([ type ]) => Parser.getParserByName(type))) : undefined; + return should_optional ? Parser.parse(data[key], true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined; } case 'misc': switch (inference_type.misc_type) { @@ -480,25 +442,27 @@ export class YTNodeGenerator { return key_info.filter(([ key ]) => !excluded_keys.has(key)); } - static introspect(classdata: any) { + static #introspect(classdata: any) { const key_info = this.#passOne(classdata); return this.#passTwo(key_info); } static inferType(key: string, value: any): InferenceType { let return_value: string | Record | boolean | MiscInferenceType = false; if (return_value = this.isRenderer(value)) { + this.#renderers_examples[return_value] = value[Reflect.ownKeys(value)[0]]; return { type: 'renderer', - renderers: { - [return_value]: value[Reflect.ownKeys(value)[0]] - }, + renderers: [ return_value ], optional: false }; } if (return_value = this.isRendererList(value)) { + for (const [ key, value ] of Object.entries(return_value)) { + this.#renderers_examples[key] = value; + } return { type: 'renderer_list', - renderers: return_value, + renderers: Object.keys(return_value), optional: false }; } @@ -517,7 +481,7 @@ export class YTNodeGenerator { is_list ? Object.fromEntries(value.map((item) => { const key = Reflect.ownKeys(item)[0].toString(); - return [ this.getRendererClass(key), item[key] ]; + return [ Parser.sanitizeClassName(key), item[key] ]; })) : false ); @@ -553,16 +517,12 @@ export class YTNodeGenerator { } return false; } - static getRendererClass(renderer: string) { - // Remove 'Renderer' from the end of the string and capitalize the first letter - return renderer.replace(/Renderer$/, '').replace(/^[a-z]/, (letter) => letter.toUpperCase()); - } static isRenderer(value: any) { const is_object = typeof value === 'object'; if (!is_object) return false; const keys = Reflect.ownKeys(value); if (keys.length === 1 && keys[0].toString().includes('Renderer')) { - return this.getRendererClass(keys[0].toString()); + return Parser.sanitizeClassName(keys[0].toString()); } return false; } From c3eca6ddf866998694dd076843784010a4bb8478 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Thu, 2 Mar 2023 09:03:23 +0200 Subject: [PATCH 06/17] fix: YTNode map imports --- scripts/build-parser-map.cjs | 27 +- src/parser/index.ts | 4 +- src/parser/map.ts | 988 ----------------------------------- src/parser/misc.ts | 11 + src/parser/nodes.ts | 323 ++++++++++++ src/parser/parser.ts | 2 +- 6 files changed, 348 insertions(+), 1007 deletions(-) delete mode 100644 src/parser/map.ts create mode 100644 src/parser/misc.ts create mode 100644 src/parser/nodes.ts diff --git a/scripts/build-parser-map.cjs b/scripts/build-parser-map.cjs index 9f5c063c7..7b1e3e315 100644 --- a/scripts/build-parser-map.cjs +++ b/scripts/build-parser-map.cjs @@ -3,9 +3,7 @@ const fs = require('fs'); const path = require('path'); const import_list = []; - -const json = []; -const misc_exports = []; +const misc_imports = []; glob.sync('../src/parser/classes/**/*.{js,ts}', { cwd: __dirname }) .forEach((file) => { @@ -16,29 +14,26 @@ glob.sync('../src/parser/classes/**/*.{js,ts}', { cwd: __dirname }) if (is_misc) { const class_name = file.split('/').pop().replace('.js', '').replace('.ts', ''); - import_list.push(`import { default as ${class_name} } from './classes/${file}.js';`); - misc_exports.push(class_name); + misc_imports.push(`export { default as ${class_name} } from './classes/${file}.js';`); } else { - import_list.push(`import { default as ${import_name} } from './classes/${file}.js'; -export { ${import_name} };`); - json.push(import_name); + import_list.push(`export { default as ${import_name} } from './classes/${file}.js';`); } }); fs.writeFileSync( - path.resolve(__dirname, '../src/parser/map.ts'), + path.resolve(__dirname, '../src/parser/nodes.ts'), `// This file was auto generated, do not edit. // See ./scripts/build-parser-map.js -import { YTNodeConstructor } from './helpers.js'; ${import_list.join('\n')} +` +); -const map: Record = { - ${json.join(',\n ')} -}; +fs.writeFileSync( + path.resolve(__dirname, '../src/parser/misc.ts'), + `// This file was auto generated, do not edit. +// See ./scripts/build-parser-map.js -export const Misc = { - ${misc_exports.join(',\n ')} -}; +${misc_imports.join('\n')} ` ); \ No newline at end of file diff --git a/src/parser/index.ts b/src/parser/index.ts index af45a97cd..1c5d23d6f 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -1,8 +1,8 @@ export { default as Parser } from './parser.js'; export * from './parser.js'; export * from './types/index.js'; -export { Misc } from '../parser/map.js'; -export * as YTNodes from '../parser/map.js'; +export * as Misc from './misc.js'; +export * as YTNodes from './nodes.js'; export * as YT from './youtube/index.js'; export * as YTMusic from './ytmusic/index.js'; export * as YTKids from './ytkids/index.js'; diff --git a/src/parser/map.ts b/src/parser/map.ts deleted file mode 100644 index 2fcf2af42..000000000 --- a/src/parser/map.ts +++ /dev/null @@ -1,988 +0,0 @@ -// This file was auto generated, do not edit. -// See ./scripts/build-parser-map.js -import { YTNodeConstructor } from './helpers.js'; - -import { default as AccountChannel } from './classes/AccountChannel.js'; -export { AccountChannel }; -import { default as AccountItemSection } from './classes/AccountItemSection.js'; -export { AccountItemSection }; -import { default as AccountItemSectionHeader } from './classes/AccountItemSectionHeader.js'; -export { AccountItemSectionHeader }; -import { default as AccountSectionList } from './classes/AccountSectionList.js'; -export { AccountSectionList }; -import { default as AppendContinuationItemsAction } from './classes/actions/AppendContinuationItemsAction.js'; -export { AppendContinuationItemsAction }; -import { default as OpenPopupAction } from './classes/actions/OpenPopupAction.js'; -export { OpenPopupAction }; -import { default as Alert } from './classes/Alert.js'; -export { Alert }; -import { default as AnalyticsMainAppKeyMetrics } from './classes/analytics/AnalyticsMainAppKeyMetrics.js'; -export { AnalyticsMainAppKeyMetrics }; -import { default as AnalyticsRoot } from './classes/analytics/AnalyticsRoot.js'; -export { AnalyticsRoot }; -import { default as AnalyticsShortsCarouselCard } from './classes/analytics/AnalyticsShortsCarouselCard.js'; -export { AnalyticsShortsCarouselCard }; -import { default as AnalyticsVideo } from './classes/analytics/AnalyticsVideo.js'; -export { AnalyticsVideo }; -import { default as AnalyticsVodCarouselCard } from './classes/analytics/AnalyticsVodCarouselCard.js'; -export { AnalyticsVodCarouselCard }; -import { default as CtaGoToCreatorStudio } from './classes/analytics/CtaGoToCreatorStudio.js'; -export { CtaGoToCreatorStudio }; -import { default as DataModelSection } from './classes/analytics/DataModelSection.js'; -export { DataModelSection }; -import { default as StatRow } from './classes/analytics/StatRow.js'; -export { StatRow }; -import { default as AudioOnlyPlayability } from './classes/AudioOnlyPlayability.js'; -export { AudioOnlyPlayability }; -import { default as AutomixPreviewVideo } from './classes/AutomixPreviewVideo.js'; -export { AutomixPreviewVideo }; -import { default as BackstageImage } from './classes/BackstageImage.js'; -export { BackstageImage }; -import { default as BackstagePost } from './classes/BackstagePost.js'; -export { BackstagePost }; -import { default as BackstagePostThread } from './classes/BackstagePostThread.js'; -export { BackstagePostThread }; -import { default as BrowseFeedActions } from './classes/BrowseFeedActions.js'; -export { BrowseFeedActions }; -import { default as BrowserMediaSession } from './classes/BrowserMediaSession.js'; -export { BrowserMediaSession }; -import { default as Button } from './classes/Button.js'; -export { Button }; -import { default as C4TabbedHeader } from './classes/C4TabbedHeader.js'; -export { C4TabbedHeader }; -import { default as CallToActionButton } from './classes/CallToActionButton.js'; -export { CallToActionButton }; -import { default as Card } from './classes/Card.js'; -export { Card }; -import { default as CardCollection } from './classes/CardCollection.js'; -export { CardCollection }; -import { default as CarouselHeader } from './classes/CarouselHeader.js'; -export { CarouselHeader }; -import { default as CarouselItem } from './classes/CarouselItem.js'; -export { CarouselItem }; -import { default as Channel } from './classes/Channel.js'; -export { Channel }; -import { default as ChannelAboutFullMetadata } from './classes/ChannelAboutFullMetadata.js'; -export { ChannelAboutFullMetadata }; -import { default as ChannelAgeGate } from './classes/ChannelAgeGate.js'; -export { ChannelAgeGate }; -import { default as ChannelFeaturedContent } from './classes/ChannelFeaturedContent.js'; -export { ChannelFeaturedContent }; -import { default as ChannelHeaderLinks } from './classes/ChannelHeaderLinks.js'; -export { ChannelHeaderLinks }; -import { default as ChannelMetadata } from './classes/ChannelMetadata.js'; -export { ChannelMetadata }; -import { default as ChannelMobileHeader } from './classes/ChannelMobileHeader.js'; -export { ChannelMobileHeader }; -import { default as ChannelOptions } from './classes/ChannelOptions.js'; -export { ChannelOptions }; -import { default as ChannelSubMenu } from './classes/ChannelSubMenu.js'; -export { ChannelSubMenu }; -import { default as ChannelThumbnailWithLink } from './classes/ChannelThumbnailWithLink.js'; -export { ChannelThumbnailWithLink }; -import { default as ChannelVideoPlayer } from './classes/ChannelVideoPlayer.js'; -export { ChannelVideoPlayer }; -import { default as Chapter } from './classes/Chapter.js'; -export { Chapter }; -import { default as ChildVideo } from './classes/ChildVideo.js'; -export { ChildVideo }; -import { default as ChipCloud } from './classes/ChipCloud.js'; -export { ChipCloud }; -import { default as ChipCloudChip } from './classes/ChipCloudChip.js'; -export { ChipCloudChip }; -import { default as CollaboratorInfoCardContent } from './classes/CollaboratorInfoCardContent.js'; -export { CollaboratorInfoCardContent }; -import { default as CollageHeroImage } from './classes/CollageHeroImage.js'; -export { CollageHeroImage }; -import { default as AuthorCommentBadge } from './classes/comments/AuthorCommentBadge.js'; -export { AuthorCommentBadge }; -import { default as Comment } from './classes/comments/Comment.js'; -export { Comment }; -import { default as CommentActionButtons } from './classes/comments/CommentActionButtons.js'; -export { CommentActionButtons }; -import { default as CommentDialog } from './classes/comments/CommentDialog.js'; -export { CommentDialog }; -import { default as CommentReplies } from './classes/comments/CommentReplies.js'; -export { CommentReplies }; -import { default as CommentReplyDialog } from './classes/comments/CommentReplyDialog.js'; -export { CommentReplyDialog }; -import { default as CommentsEntryPointHeader } from './classes/comments/CommentsEntryPointHeader.js'; -export { CommentsEntryPointHeader }; -import { default as CommentsHeader } from './classes/comments/CommentsHeader.js'; -export { CommentsHeader }; -import { default as CommentSimplebox } from './classes/comments/CommentSimplebox.js'; -export { CommentSimplebox }; -import { default as CommentThread } from './classes/comments/CommentThread.js'; -export { CommentThread }; -import { default as CreatorHeart } from './classes/comments/CreatorHeart.js'; -export { CreatorHeart }; -import { default as EmojiPicker } from './classes/comments/EmojiPicker.js'; -export { EmojiPicker }; -import { default as PdgCommentChip } from './classes/comments/PdgCommentChip.js'; -export { PdgCommentChip }; -import { default as SponsorCommentBadge } from './classes/comments/SponsorCommentBadge.js'; -export { SponsorCommentBadge }; -import { default as CompactChannel } from './classes/CompactChannel.js'; -export { CompactChannel }; -import { default as CompactLink } from './classes/CompactLink.js'; -export { CompactLink }; -import { default as CompactMix } from './classes/CompactMix.js'; -export { CompactMix }; -import { default as CompactPlaylist } from './classes/CompactPlaylist.js'; -export { CompactPlaylist }; -import { default as CompactStation } from './classes/CompactStation.js'; -export { CompactStation }; -import { default as CompactVideo } from './classes/CompactVideo.js'; -export { CompactVideo }; -import { default as ConfirmDialog } from './classes/ConfirmDialog.js'; -export { ConfirmDialog }; -import { default as ContinuationItem } from './classes/ContinuationItem.js'; -export { ContinuationItem }; -import { default as CopyLink } from './classes/CopyLink.js'; -export { CopyLink }; -import { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog.js'; -export { CreatePlaylistDialog }; -import { default as DecoratedPlayerBar } from './classes/DecoratedPlayerBar.js'; -export { DecoratedPlayerBar }; -import { default as DefaultPromoPanel } from './classes/DefaultPromoPanel.js'; -export { DefaultPromoPanel }; -import { default as DidYouMean } from './classes/DidYouMean.js'; -export { DidYouMean }; -import { default as DownloadButton } from './classes/DownloadButton.js'; -export { DownloadButton }; -import { default as Dropdown } from './classes/Dropdown.js'; -export { Dropdown }; -import { default as DropdownItem } from './classes/DropdownItem.js'; -export { DropdownItem }; -import { default as Element } from './classes/Element.js'; -export { Element }; -import { default as EmergencyOnebox } from './classes/EmergencyOnebox.js'; -export { EmergencyOnebox }; -import { default as EmojiPickerCategory } from './classes/EmojiPickerCategory.js'; -export { EmojiPickerCategory }; -import { default as EmojiPickerCategoryButton } from './classes/EmojiPickerCategoryButton.js'; -export { EmojiPickerCategoryButton }; -import { default as EmojiPickerUpsellCategory } from './classes/EmojiPickerUpsellCategory.js'; -export { EmojiPickerUpsellCategory }; -import { default as Endscreen } from './classes/Endscreen.js'; -export { Endscreen }; -import { default as EndscreenElement } from './classes/EndscreenElement.js'; -export { EndscreenElement }; -import { default as EndScreenPlaylist } from './classes/EndScreenPlaylist.js'; -export { EndScreenPlaylist }; -import { default as EndScreenVideo } from './classes/EndScreenVideo.js'; -export { EndScreenVideo }; -import { default as ExpandableMetadata } from './classes/ExpandableMetadata.js'; -export { ExpandableMetadata }; -import { default as ExpandableTab } from './classes/ExpandableTab.js'; -export { ExpandableTab }; -import { default as ExpandedShelfContents } from './classes/ExpandedShelfContents.js'; -export { ExpandedShelfContents }; -import { default as FeedFilterChipBar } from './classes/FeedFilterChipBar.js'; -export { FeedFilterChipBar }; -import { default as FeedTabbedHeader } from './classes/FeedTabbedHeader.js'; -export { FeedTabbedHeader }; -import { default as GameCard } from './classes/GameCard.js'; -export { GameCard }; -import { default as GameDetails } from './classes/GameDetails.js'; -export { GameDetails }; -import { default as Grid } from './classes/Grid.js'; -export { Grid }; -import { default as GridChannel } from './classes/GridChannel.js'; -export { GridChannel }; -import { default as GridHeader } from './classes/GridHeader.js'; -export { GridHeader }; -import { default as GridPlaylist } from './classes/GridPlaylist.js'; -export { GridPlaylist }; -import { default as GridVideo } from './classes/GridVideo.js'; -export { GridVideo }; -import { default as HashtagHeader } from './classes/HashtagHeader.js'; -export { HashtagHeader }; -import { default as Heatmap } from './classes/Heatmap.js'; -export { Heatmap }; -import { default as HeatMarker } from './classes/HeatMarker.js'; -export { HeatMarker }; -import { default as HighlightsCarousel } from './classes/HighlightsCarousel.js'; -export { HighlightsCarousel }; -import { default as HistorySuggestion } from './classes/HistorySuggestion.js'; -export { HistorySuggestion }; -import { default as HorizontalCardList } from './classes/HorizontalCardList.js'; -export { HorizontalCardList }; -import { default as HorizontalList } from './classes/HorizontalList.js'; -export { HorizontalList }; -import { default as IconLink } from './classes/IconLink.js'; -export { IconLink }; -import { default as InteractiveTabbedHeader } from './classes/InteractiveTabbedHeader.js'; -export { InteractiveTabbedHeader }; -import { default as ItemSection } from './classes/ItemSection.js'; -export { ItemSection }; -import { default as ItemSectionHeader } from './classes/ItemSectionHeader.js'; -export { ItemSectionHeader }; -import { default as ItemSectionTab } from './classes/ItemSectionTab.js'; -export { ItemSectionTab }; -import { default as ItemSectionTabbedHeader } from './classes/ItemSectionTabbedHeader.js'; -export { ItemSectionTabbedHeader }; -import { default as LikeButton } from './classes/LikeButton.js'; -export { LikeButton }; -import { default as LiveChat } from './classes/LiveChat.js'; -export { LiveChat }; -import { default as AddBannerToLiveChatCommand } from './classes/livechat/AddBannerToLiveChatCommand.js'; -export { AddBannerToLiveChatCommand }; -import { default as AddChatItemAction } from './classes/livechat/AddChatItemAction.js'; -export { AddChatItemAction }; -import { default as AddLiveChatTickerItemAction } from './classes/livechat/AddLiveChatTickerItemAction.js'; -export { AddLiveChatTickerItemAction }; -import { default as DimChatItemAction } from './classes/livechat/DimChatItemAction.js'; -export { DimChatItemAction }; -import { default as LiveChatAutoModMessage } from './classes/livechat/items/LiveChatAutoModMessage.js'; -export { LiveChatAutoModMessage }; -import { default as LiveChatBanner } from './classes/livechat/items/LiveChatBanner.js'; -export { LiveChatBanner }; -import { default as LiveChatBannerHeader } from './classes/livechat/items/LiveChatBannerHeader.js'; -export { LiveChatBannerHeader }; -import { default as LiveChatBannerPoll } from './classes/livechat/items/LiveChatBannerPoll.js'; -export { LiveChatBannerPoll }; -import { default as LiveChatMembershipItem } from './classes/livechat/items/LiveChatMembershipItem.js'; -export { LiveChatMembershipItem }; -import { default as LiveChatPaidMessage } from './classes/livechat/items/LiveChatPaidMessage.js'; -export { LiveChatPaidMessage }; -import { default as LiveChatPaidSticker } from './classes/livechat/items/LiveChatPaidSticker.js'; -export { LiveChatPaidSticker }; -import { default as LiveChatPlaceholderItem } from './classes/livechat/items/LiveChatPlaceholderItem.js'; -export { LiveChatPlaceholderItem }; -import { default as LiveChatProductItem } from './classes/livechat/items/LiveChatProductItem.js'; -export { LiveChatProductItem }; -import { default as LiveChatRestrictedParticipation } from './classes/livechat/items/LiveChatRestrictedParticipation.js'; -export { LiveChatRestrictedParticipation }; -import { default as LiveChatTextMessage } from './classes/livechat/items/LiveChatTextMessage.js'; -export { LiveChatTextMessage }; -import { default as LiveChatTickerPaidMessageItem } from './classes/livechat/items/LiveChatTickerPaidMessageItem.js'; -export { LiveChatTickerPaidMessageItem }; -import { default as LiveChatTickerPaidStickerItem } from './classes/livechat/items/LiveChatTickerPaidStickerItem.js'; -export { LiveChatTickerPaidStickerItem }; -import { default as LiveChatTickerSponsorItem } from './classes/livechat/items/LiveChatTickerSponsorItem.js'; -export { LiveChatTickerSponsorItem }; -import { default as LiveChatViewerEngagementMessage } from './classes/livechat/items/LiveChatViewerEngagementMessage.js'; -export { LiveChatViewerEngagementMessage }; -import { default as PollHeader } from './classes/livechat/items/PollHeader.js'; -export { PollHeader }; -import { default as LiveChatActionPanel } from './classes/livechat/LiveChatActionPanel.js'; -export { LiveChatActionPanel }; -import { default as MarkChatItemAsDeletedAction } from './classes/livechat/MarkChatItemAsDeletedAction.js'; -export { MarkChatItemAsDeletedAction }; -import { default as MarkChatItemsByAuthorAsDeletedAction } from './classes/livechat/MarkChatItemsByAuthorAsDeletedAction.js'; -export { MarkChatItemsByAuthorAsDeletedAction }; -import { default as RemoveBannerForLiveChatCommand } from './classes/livechat/RemoveBannerForLiveChatCommand.js'; -export { RemoveBannerForLiveChatCommand }; -import { default as RemoveChatItemAction } from './classes/livechat/RemoveChatItemAction.js'; -export { RemoveChatItemAction }; -import { default as RemoveChatItemByAuthorAction } from './classes/livechat/RemoveChatItemByAuthorAction.js'; -export { RemoveChatItemByAuthorAction }; -import { default as ReplaceChatItemAction } from './classes/livechat/ReplaceChatItemAction.js'; -export { ReplaceChatItemAction }; -import { default as ReplayChatItemAction } from './classes/livechat/ReplayChatItemAction.js'; -export { ReplayChatItemAction }; -import { default as ShowLiveChatActionPanelAction } from './classes/livechat/ShowLiveChatActionPanelAction.js'; -export { ShowLiveChatActionPanelAction }; -import { default as ShowLiveChatDialogAction } from './classes/livechat/ShowLiveChatDialogAction.js'; -export { ShowLiveChatDialogAction }; -import { default as ShowLiveChatTooltipCommand } from './classes/livechat/ShowLiveChatTooltipCommand.js'; -export { ShowLiveChatTooltipCommand }; -import { default as UpdateDateTextAction } from './classes/livechat/UpdateDateTextAction.js'; -export { UpdateDateTextAction }; -import { default as UpdateDescriptionAction } from './classes/livechat/UpdateDescriptionAction.js'; -export { UpdateDescriptionAction }; -import { default as UpdateLiveChatPollAction } from './classes/livechat/UpdateLiveChatPollAction.js'; -export { UpdateLiveChatPollAction }; -import { default as UpdateTitleAction } from './classes/livechat/UpdateTitleAction.js'; -export { UpdateTitleAction }; -import { default as UpdateToggleButtonTextAction } from './classes/livechat/UpdateToggleButtonTextAction.js'; -export { UpdateToggleButtonTextAction }; -import { default as UpdateViewershipAction } from './classes/livechat/UpdateViewershipAction.js'; -export { UpdateViewershipAction }; -import { default as LiveChatAuthorBadge } from './classes/LiveChatAuthorBadge.js'; -export { LiveChatAuthorBadge }; -import { default as LiveChatDialog } from './classes/LiveChatDialog.js'; -export { LiveChatDialog }; -import { default as LiveChatHeader } from './classes/LiveChatHeader.js'; -export { LiveChatHeader }; -import { default as LiveChatItemList } from './classes/LiveChatItemList.js'; -export { LiveChatItemList }; -import { default as LiveChatMessageInput } from './classes/LiveChatMessageInput.js'; -export { LiveChatMessageInput }; -import { default as LiveChatParticipant } from './classes/LiveChatParticipant.js'; -export { LiveChatParticipant }; -import { default as LiveChatParticipantsList } from './classes/LiveChatParticipantsList.js'; -export { LiveChatParticipantsList }; -import { default as MacroMarkersListItem } from './classes/MacroMarkersListItem.js'; -export { MacroMarkersListItem }; -import { default as Menu } from './classes/menus/Menu.js'; -export { Menu }; -import { default as MenuNavigationItem } from './classes/menus/MenuNavigationItem.js'; -export { MenuNavigationItem }; -import { default as MenuServiceItem } from './classes/menus/MenuServiceItem.js'; -export { MenuServiceItem }; -import { default as MenuServiceItemDownload } from './classes/menus/MenuServiceItemDownload.js'; -export { MenuServiceItemDownload }; -import { default as MultiPageMenu } from './classes/menus/MultiPageMenu.js'; -export { MultiPageMenu }; -import { default as MultiPageMenuNotificationSection } from './classes/menus/MultiPageMenuNotificationSection.js'; -export { MultiPageMenuNotificationSection }; -import { default as MusicMenuItemDivider } from './classes/menus/MusicMenuItemDivider.js'; -export { MusicMenuItemDivider }; -import { default as MusicMultiSelectMenu } from './classes/menus/MusicMultiSelectMenu.js'; -export { MusicMultiSelectMenu }; -import { default as MusicMultiSelectMenuItem } from './classes/menus/MusicMultiSelectMenuItem.js'; -export { MusicMultiSelectMenuItem }; -import { default as SimpleMenuHeader } from './classes/menus/SimpleMenuHeader.js'; -export { SimpleMenuHeader }; -import { default as MerchandiseItem } from './classes/MerchandiseItem.js'; -export { MerchandiseItem }; -import { default as MerchandiseShelf } from './classes/MerchandiseShelf.js'; -export { MerchandiseShelf }; -import { default as Message } from './classes/Message.js'; -export { Message }; -import { default as MetadataBadge } from './classes/MetadataBadge.js'; -export { MetadataBadge }; -import { default as MetadataRow } from './classes/MetadataRow.js'; -export { MetadataRow }; -import { default as MetadataRowContainer } from './classes/MetadataRowContainer.js'; -export { MetadataRowContainer }; -import { default as MetadataRowHeader } from './classes/MetadataRowHeader.js'; -export { MetadataRowHeader }; -import { default as MetadataScreen } from './classes/MetadataScreen.js'; -export { MetadataScreen }; -import { default as MicroformatData } from './classes/MicroformatData.js'; -export { MicroformatData }; -import { default as Author } from './classes/misc/Author.js'; -import { default as ChildElement } from './classes/misc/ChildElement.js'; -import { default as EmojiRun } from './classes/misc/EmojiRun.js'; -import { default as Format } from './classes/misc/Format.js'; -import { default as PlaylistAuthor } from './classes/misc/PlaylistAuthor.js'; -import { default as Text } from './classes/misc/Text.js'; -import { default as TextRun } from './classes/misc/TextRun.js'; -import { default as Thumbnail } from './classes/misc/Thumbnail.js'; -import { default as VideoDetails } from './classes/misc/VideoDetails.js'; -import { default as Mix } from './classes/Mix.js'; -export { Mix }; -import { default as Movie } from './classes/Movie.js'; -export { Movie }; -import { default as MovingThumbnail } from './classes/MovingThumbnail.js'; -export { MovingThumbnail }; -import { default as MultiMarkersPlayerBar } from './classes/MultiMarkersPlayerBar.js'; -export { MultiMarkersPlayerBar }; -import { default as MusicCarouselShelf } from './classes/MusicCarouselShelf.js'; -export { MusicCarouselShelf }; -import { default as MusicCarouselShelfBasicHeader } from './classes/MusicCarouselShelfBasicHeader.js'; -export { MusicCarouselShelfBasicHeader }; -import { default as MusicDescriptionShelf } from './classes/MusicDescriptionShelf.js'; -export { MusicDescriptionShelf }; -import { default as MusicDetailHeader } from './classes/MusicDetailHeader.js'; -export { MusicDetailHeader }; -import { default as MusicDownloadStateBadge } from './classes/MusicDownloadStateBadge.js'; -export { MusicDownloadStateBadge }; -import { default as MusicEditablePlaylistDetailHeader } from './classes/MusicEditablePlaylistDetailHeader.js'; -export { MusicEditablePlaylistDetailHeader }; -import { default as MusicElementHeader } from './classes/MusicElementHeader.js'; -export { MusicElementHeader }; -import { default as MusicHeader } from './classes/MusicHeader.js'; -export { MusicHeader }; -import { default as MusicImmersiveHeader } from './classes/MusicImmersiveHeader.js'; -export { MusicImmersiveHeader }; -import { default as MusicInlineBadge } from './classes/MusicInlineBadge.js'; -export { MusicInlineBadge }; -import { default as MusicItemThumbnailOverlay } from './classes/MusicItemThumbnailOverlay.js'; -export { MusicItemThumbnailOverlay }; -import { default as MusicLargeCardItemCarousel } from './classes/MusicLargeCardItemCarousel.js'; -export { MusicLargeCardItemCarousel }; -import { default as MusicNavigationButton } from './classes/MusicNavigationButton.js'; -export { MusicNavigationButton }; -import { default as MusicPlayButton } from './classes/MusicPlayButton.js'; -export { MusicPlayButton }; -import { default as MusicPlaylistShelf } from './classes/MusicPlaylistShelf.js'; -export { MusicPlaylistShelf }; -import { default as MusicQueue } from './classes/MusicQueue.js'; -export { MusicQueue }; -import { default as MusicResponsiveListItem } from './classes/MusicResponsiveListItem.js'; -export { MusicResponsiveListItem }; -import { default as MusicResponsiveListItemFixedColumn } from './classes/MusicResponsiveListItemFixedColumn.js'; -export { MusicResponsiveListItemFixedColumn }; -import { default as MusicResponsiveListItemFlexColumn } from './classes/MusicResponsiveListItemFlexColumn.js'; -export { MusicResponsiveListItemFlexColumn }; -import { default as MusicShelf } from './classes/MusicShelf.js'; -export { MusicShelf }; -import { default as MusicSideAlignedItem } from './classes/MusicSideAlignedItem.js'; -export { MusicSideAlignedItem }; -import { default as MusicSortFilterButton } from './classes/MusicSortFilterButton.js'; -export { MusicSortFilterButton }; -import { default as MusicThumbnail } from './classes/MusicThumbnail.js'; -export { MusicThumbnail }; -import { default as MusicTwoRowItem } from './classes/MusicTwoRowItem.js'; -export { MusicTwoRowItem }; -import { default as MusicVisualHeader } from './classes/MusicVisualHeader.js'; -export { MusicVisualHeader }; -import { default as NavigationEndpoint } from './classes/NavigationEndpoint.js'; -export { NavigationEndpoint }; -import { default as Notification } from './classes/Notification.js'; -export { Notification }; -import { default as PageIntroduction } from './classes/PageIntroduction.js'; -export { PageIntroduction }; -import { default as PlayerAnnotationsExpanded } from './classes/PlayerAnnotationsExpanded.js'; -export { PlayerAnnotationsExpanded }; -import { default as PlayerCaptionsTracklist } from './classes/PlayerCaptionsTracklist.js'; -export { PlayerCaptionsTracklist }; -import { default as PlayerErrorMessage } from './classes/PlayerErrorMessage.js'; -export { PlayerErrorMessage }; -import { default as PlayerLiveStoryboardSpec } from './classes/PlayerLiveStoryboardSpec.js'; -export { PlayerLiveStoryboardSpec }; -import { default as PlayerMicroformat } from './classes/PlayerMicroformat.js'; -export { PlayerMicroformat }; -import { default as PlayerOverlay } from './classes/PlayerOverlay.js'; -export { PlayerOverlay }; -import { default as PlayerOverlayAutoplay } from './classes/PlayerOverlayAutoplay.js'; -export { PlayerOverlayAutoplay }; -import { default as PlayerStoryboardSpec } from './classes/PlayerStoryboardSpec.js'; -export { PlayerStoryboardSpec }; -import { default as Playlist } from './classes/Playlist.js'; -export { Playlist }; -import { default as PlaylistCustomThumbnail } from './classes/PlaylistCustomThumbnail.js'; -export { PlaylistCustomThumbnail }; -import { default as PlaylistHeader } from './classes/PlaylistHeader.js'; -export { PlaylistHeader }; -import { default as PlaylistInfoCardContent } from './classes/PlaylistInfoCardContent.js'; -export { PlaylistInfoCardContent }; -import { default as PlaylistMetadata } from './classes/PlaylistMetadata.js'; -export { PlaylistMetadata }; -import { default as PlaylistPanel } from './classes/PlaylistPanel.js'; -export { PlaylistPanel }; -import { default as PlaylistPanelVideo } from './classes/PlaylistPanelVideo.js'; -export { PlaylistPanelVideo }; -import { default as PlaylistPanelVideoWrapper } from './classes/PlaylistPanelVideoWrapper.js'; -export { PlaylistPanelVideoWrapper }; -import { default as PlaylistSidebar } from './classes/PlaylistSidebar.js'; -export { PlaylistSidebar }; -import { default as PlaylistSidebarPrimaryInfo } from './classes/PlaylistSidebarPrimaryInfo.js'; -export { PlaylistSidebarPrimaryInfo }; -import { default as PlaylistSidebarSecondaryInfo } from './classes/PlaylistSidebarSecondaryInfo.js'; -export { PlaylistSidebarSecondaryInfo }; -import { default as PlaylistVideo } from './classes/PlaylistVideo.js'; -export { PlaylistVideo }; -import { default as PlaylistVideoList } from './classes/PlaylistVideoList.js'; -export { PlaylistVideoList }; -import { default as PlaylistVideoThumbnail } from './classes/PlaylistVideoThumbnail.js'; -export { PlaylistVideoThumbnail }; -import { default as Poll } from './classes/Poll.js'; -export { Poll }; -import { default as Post } from './classes/Post.js'; -export { Post }; -import { default as PostMultiImage } from './classes/PostMultiImage.js'; -export { PostMultiImage }; -import { default as ProfileColumn } from './classes/ProfileColumn.js'; -export { ProfileColumn }; -import { default as ProfileColumnStats } from './classes/ProfileColumnStats.js'; -export { ProfileColumnStats }; -import { default as ProfileColumnStatsEntry } from './classes/ProfileColumnStatsEntry.js'; -export { ProfileColumnStatsEntry }; -import { default as ProfileColumnUserInfo } from './classes/ProfileColumnUserInfo.js'; -export { ProfileColumnUserInfo }; -import { default as RecognitionShelf } from './classes/RecognitionShelf.js'; -export { RecognitionShelf }; -import { default as ReelItem } from './classes/ReelItem.js'; -export { ReelItem }; -import { default as ReelShelf } from './classes/ReelShelf.js'; -export { ReelShelf }; -import { default as RelatedChipCloud } from './classes/RelatedChipCloud.js'; -export { RelatedChipCloud }; -import { default as RichGrid } from './classes/RichGrid.js'; -export { RichGrid }; -import { default as RichItem } from './classes/RichItem.js'; -export { RichItem }; -import { default as RichListHeader } from './classes/RichListHeader.js'; -export { RichListHeader }; -import { default as RichSection } from './classes/RichSection.js'; -export { RichSection }; -import { default as RichShelf } from './classes/RichShelf.js'; -export { RichShelf }; -import { default as SearchBox } from './classes/SearchBox.js'; -export { SearchBox }; -import { default as SearchRefinementCard } from './classes/SearchRefinementCard.js'; -export { SearchRefinementCard }; -import { default as SearchSuggestion } from './classes/SearchSuggestion.js'; -export { SearchSuggestion }; -import { default as SearchSuggestionsSection } from './classes/SearchSuggestionsSection.js'; -export { SearchSuggestionsSection }; -import { default as SecondarySearchContainer } from './classes/SecondarySearchContainer.js'; -export { SecondarySearchContainer }; -import { default as SectionList } from './classes/SectionList.js'; -export { SectionList }; -import { default as SegmentedLikeDislikeButton } from './classes/SegmentedLikeDislikeButton.js'; -export { SegmentedLikeDislikeButton }; -import { default as SettingBoolean } from './classes/SettingBoolean.js'; -export { SettingBoolean }; -import { default as SettingsCheckbox } from './classes/SettingsCheckbox.js'; -export { SettingsCheckbox }; -import { default as SettingsOptions } from './classes/SettingsOptions.js'; -export { SettingsOptions }; -import { default as SettingsSidebar } from './classes/SettingsSidebar.js'; -export { SettingsSidebar }; -import { default as SettingsSwitch } from './classes/SettingsSwitch.js'; -export { SettingsSwitch }; -import { default as Shelf } from './classes/Shelf.js'; -export { Shelf }; -import { default as ShowingResultsFor } from './classes/ShowingResultsFor.js'; -export { ShowingResultsFor }; -import { default as SimpleCardContent } from './classes/SimpleCardContent.js'; -export { SimpleCardContent }; -import { default as SimpleCardTeaser } from './classes/SimpleCardTeaser.js'; -export { SimpleCardTeaser }; -import { default as SimpleTextSection } from './classes/SimpleTextSection.js'; -export { SimpleTextSection }; -import { default as SingleActionEmergencySupport } from './classes/SingleActionEmergencySupport.js'; -export { SingleActionEmergencySupport }; -import { default as SingleColumnBrowseResults } from './classes/SingleColumnBrowseResults.js'; -export { SingleColumnBrowseResults }; -import { default as SingleColumnMusicWatchNextResults } from './classes/SingleColumnMusicWatchNextResults.js'; -export { SingleColumnMusicWatchNextResults }; -import { default as SingleHeroImage } from './classes/SingleHeroImage.js'; -export { SingleHeroImage }; -import { default as SlimOwner } from './classes/SlimOwner.js'; -export { SlimOwner }; -import { default as SlimVideoMetadata } from './classes/SlimVideoMetadata.js'; -export { SlimVideoMetadata }; -import { default as SortFilterSubMenu } from './classes/SortFilterSubMenu.js'; -export { SortFilterSubMenu }; -import { default as SubFeedOption } from './classes/SubFeedOption.js'; -export { SubFeedOption }; -import { default as SubFeedSelector } from './classes/SubFeedSelector.js'; -export { SubFeedSelector }; -import { default as SubscribeButton } from './classes/SubscribeButton.js'; -export { SubscribeButton }; -import { default as SubscriptionNotificationToggleButton } from './classes/SubscriptionNotificationToggleButton.js'; -export { SubscriptionNotificationToggleButton }; -import { default as Tab } from './classes/Tab.js'; -export { Tab }; -import { default as Tabbed } from './classes/Tabbed.js'; -export { Tabbed }; -import { default as TabbedSearchResults } from './classes/TabbedSearchResults.js'; -export { TabbedSearchResults }; -import { default as TextHeader } from './classes/TextHeader.js'; -export { TextHeader }; -import { default as ThumbnailLandscapePortrait } from './classes/ThumbnailLandscapePortrait.js'; -export { ThumbnailLandscapePortrait }; -import { default as ThumbnailOverlayBottomPanel } from './classes/ThumbnailOverlayBottomPanel.js'; -export { ThumbnailOverlayBottomPanel }; -import { default as ThumbnailOverlayEndorsement } from './classes/ThumbnailOverlayEndorsement.js'; -export { ThumbnailOverlayEndorsement }; -import { default as ThumbnailOverlayHoverText } from './classes/ThumbnailOverlayHoverText.js'; -export { ThumbnailOverlayHoverText }; -import { default as ThumbnailOverlayInlineUnplayable } from './classes/ThumbnailOverlayInlineUnplayable.js'; -export { ThumbnailOverlayInlineUnplayable }; -import { default as ThumbnailOverlayLoadingPreview } from './classes/ThumbnailOverlayLoadingPreview.js'; -export { ThumbnailOverlayLoadingPreview }; -import { default as ThumbnailOverlayNowPlaying } from './classes/ThumbnailOverlayNowPlaying.js'; -export { ThumbnailOverlayNowPlaying }; -import { default as ThumbnailOverlayPinking } from './classes/ThumbnailOverlayPinking.js'; -export { ThumbnailOverlayPinking }; -import { default as ThumbnailOverlayPlaybackStatus } from './classes/ThumbnailOverlayPlaybackStatus.js'; -export { ThumbnailOverlayPlaybackStatus }; -import { default as ThumbnailOverlayResumePlayback } from './classes/ThumbnailOverlayResumePlayback.js'; -export { ThumbnailOverlayResumePlayback }; -import { default as ThumbnailOverlaySidePanel } from './classes/ThumbnailOverlaySidePanel.js'; -export { ThumbnailOverlaySidePanel }; -import { default as ThumbnailOverlayTimeStatus } from './classes/ThumbnailOverlayTimeStatus.js'; -export { ThumbnailOverlayTimeStatus }; -import { default as ThumbnailOverlayToggleButton } from './classes/ThumbnailOverlayToggleButton.js'; -export { ThumbnailOverlayToggleButton }; -import { default as TimedMarkerDecoration } from './classes/TimedMarkerDecoration.js'; -export { TimedMarkerDecoration }; -import { default as TitleAndButtonListHeader } from './classes/TitleAndButtonListHeader.js'; -export { TitleAndButtonListHeader }; -import { default as ToggleButton } from './classes/ToggleButton.js'; -export { ToggleButton }; -import { default as ToggleMenuServiceItem } from './classes/ToggleMenuServiceItem.js'; -export { ToggleMenuServiceItem }; -import { default as Tooltip } from './classes/Tooltip.js'; -export { Tooltip }; -import { default as TopicChannelDetails } from './classes/TopicChannelDetails.js'; -export { TopicChannelDetails }; -import { default as TwoColumnBrowseResults } from './classes/TwoColumnBrowseResults.js'; -export { TwoColumnBrowseResults }; -import { default as TwoColumnSearchResults } from './classes/TwoColumnSearchResults.js'; -export { TwoColumnSearchResults }; -import { default as TwoColumnWatchNextResults } from './classes/TwoColumnWatchNextResults.js'; -export { TwoColumnWatchNextResults }; -import { default as UniversalWatchCard } from './classes/UniversalWatchCard.js'; -export { UniversalWatchCard }; -import { default as UpsellDialog } from './classes/UpsellDialog.js'; -export { UpsellDialog }; -import { default as VerticalList } from './classes/VerticalList.js'; -export { VerticalList }; -import { default as VerticalWatchCardList } from './classes/VerticalWatchCardList.js'; -export { VerticalWatchCardList }; -import { default as Video } from './classes/Video.js'; -export { Video }; -import { default as VideoCard } from './classes/VideoCard.js'; -export { VideoCard }; -import { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.js'; -export { VideoInfoCardContent }; -import { default as VideoOwner } from './classes/VideoOwner.js'; -export { VideoOwner }; -import { default as VideoPrimaryInfo } from './classes/VideoPrimaryInfo.js'; -export { VideoPrimaryInfo }; -import { default as VideoSecondaryInfo } from './classes/VideoSecondaryInfo.js'; -export { VideoSecondaryInfo }; -import { default as WatchCardCompactVideo } from './classes/WatchCardCompactVideo.js'; -export { WatchCardCompactVideo }; -import { default as WatchCardHeroVideo } from './classes/WatchCardHeroVideo.js'; -export { WatchCardHeroVideo }; -import { default as WatchCardRichHeader } from './classes/WatchCardRichHeader.js'; -export { WatchCardRichHeader }; -import { default as WatchCardSectionSequence } from './classes/WatchCardSectionSequence.js'; -export { WatchCardSectionSequence }; -import { default as WatchNextEndScreen } from './classes/WatchNextEndScreen.js'; -export { WatchNextEndScreen }; -import { default as WatchNextTabbedResults } from './classes/WatchNextTabbedResults.js'; -export { WatchNextTabbedResults }; -import { default as AnchoredSection } from './classes/ytkids/AnchoredSection.js'; -export { AnchoredSection }; -import { default as KidsCategoriesHeader } from './classes/ytkids/KidsCategoriesHeader.js'; -export { KidsCategoriesHeader }; -import { default as KidsCategoryTab } from './classes/ytkids/KidsCategoryTab.js'; -export { KidsCategoryTab }; -import { default as KidsHomeScreen } from './classes/ytkids/KidsHomeScreen.js'; -export { KidsHomeScreen }; - -const map: Record = { - AccountChannel, - AccountItemSection, - AccountItemSectionHeader, - AccountSectionList, - AppendContinuationItemsAction, - OpenPopupAction, - Alert, - AnalyticsMainAppKeyMetrics, - AnalyticsRoot, - AnalyticsShortsCarouselCard, - AnalyticsVideo, - AnalyticsVodCarouselCard, - CtaGoToCreatorStudio, - DataModelSection, - StatRow, - AudioOnlyPlayability, - AutomixPreviewVideo, - BackstageImage, - BackstagePost, - BackstagePostThread, - BrowseFeedActions, - BrowserMediaSession, - Button, - C4TabbedHeader, - CallToActionButton, - Card, - CardCollection, - CarouselHeader, - CarouselItem, - Channel, - ChannelAboutFullMetadata, - ChannelAgeGate, - ChannelFeaturedContent, - ChannelHeaderLinks, - ChannelMetadata, - ChannelMobileHeader, - ChannelOptions, - ChannelSubMenu, - ChannelThumbnailWithLink, - ChannelVideoPlayer, - Chapter, - ChildVideo, - ChipCloud, - ChipCloudChip, - CollaboratorInfoCardContent, - CollageHeroImage, - AuthorCommentBadge, - Comment, - CommentActionButtons, - CommentDialog, - CommentReplies, - CommentReplyDialog, - CommentsEntryPointHeader, - CommentsHeader, - CommentSimplebox, - CommentThread, - CreatorHeart, - EmojiPicker, - PdgCommentChip, - SponsorCommentBadge, - CompactChannel, - CompactLink, - CompactMix, - CompactPlaylist, - CompactStation, - CompactVideo, - ConfirmDialog, - ContinuationItem, - CopyLink, - CreatePlaylistDialog, - DecoratedPlayerBar, - DefaultPromoPanel, - DidYouMean, - DownloadButton, - Dropdown, - DropdownItem, - Element, - EmergencyOnebox, - EmojiPickerCategory, - EmojiPickerCategoryButton, - EmojiPickerUpsellCategory, - Endscreen, - EndscreenElement, - EndScreenPlaylist, - EndScreenVideo, - ExpandableMetadata, - ExpandableTab, - ExpandedShelfContents, - FeedFilterChipBar, - FeedTabbedHeader, - GameCard, - GameDetails, - Grid, - GridChannel, - GridHeader, - GridPlaylist, - GridVideo, - HashtagHeader, - Heatmap, - HeatMarker, - HighlightsCarousel, - HistorySuggestion, - HorizontalCardList, - HorizontalList, - IconLink, - InteractiveTabbedHeader, - ItemSection, - ItemSectionHeader, - ItemSectionTab, - ItemSectionTabbedHeader, - LikeButton, - LiveChat, - AddBannerToLiveChatCommand, - AddChatItemAction, - AddLiveChatTickerItemAction, - DimChatItemAction, - LiveChatAutoModMessage, - LiveChatBanner, - LiveChatBannerHeader, - LiveChatBannerPoll, - LiveChatMembershipItem, - LiveChatPaidMessage, - LiveChatPaidSticker, - LiveChatPlaceholderItem, - LiveChatProductItem, - LiveChatRestrictedParticipation, - LiveChatTextMessage, - LiveChatTickerPaidMessageItem, - LiveChatTickerPaidStickerItem, - LiveChatTickerSponsorItem, - LiveChatViewerEngagementMessage, - PollHeader, - LiveChatActionPanel, - MarkChatItemAsDeletedAction, - MarkChatItemsByAuthorAsDeletedAction, - RemoveBannerForLiveChatCommand, - RemoveChatItemAction, - RemoveChatItemByAuthorAction, - ReplaceChatItemAction, - ReplayChatItemAction, - ShowLiveChatActionPanelAction, - ShowLiveChatDialogAction, - ShowLiveChatTooltipCommand, - UpdateDateTextAction, - UpdateDescriptionAction, - UpdateLiveChatPollAction, - UpdateTitleAction, - UpdateToggleButtonTextAction, - UpdateViewershipAction, - LiveChatAuthorBadge, - LiveChatDialog, - LiveChatHeader, - LiveChatItemList, - LiveChatMessageInput, - LiveChatParticipant, - LiveChatParticipantsList, - MacroMarkersListItem, - Menu, - MenuNavigationItem, - MenuServiceItem, - MenuServiceItemDownload, - MultiPageMenu, - MultiPageMenuNotificationSection, - MusicMenuItemDivider, - MusicMultiSelectMenu, - MusicMultiSelectMenuItem, - SimpleMenuHeader, - MerchandiseItem, - MerchandiseShelf, - Message, - MetadataBadge, - MetadataRow, - MetadataRowContainer, - MetadataRowHeader, - MetadataScreen, - MicroformatData, - Mix, - Movie, - MovingThumbnail, - MultiMarkersPlayerBar, - MusicCarouselShelf, - MusicCarouselShelfBasicHeader, - MusicDescriptionShelf, - MusicDetailHeader, - MusicDownloadStateBadge, - MusicEditablePlaylistDetailHeader, - MusicElementHeader, - MusicHeader, - MusicImmersiveHeader, - MusicInlineBadge, - MusicItemThumbnailOverlay, - MusicLargeCardItemCarousel, - MusicNavigationButton, - MusicPlayButton, - MusicPlaylistShelf, - MusicQueue, - MusicResponsiveListItem, - MusicResponsiveListItemFixedColumn, - MusicResponsiveListItemFlexColumn, - MusicShelf, - MusicSideAlignedItem, - MusicSortFilterButton, - MusicThumbnail, - MusicTwoRowItem, - MusicVisualHeader, - NavigationEndpoint, - Notification, - PageIntroduction, - PlayerAnnotationsExpanded, - PlayerCaptionsTracklist, - PlayerErrorMessage, - PlayerLiveStoryboardSpec, - PlayerMicroformat, - PlayerOverlay, - PlayerOverlayAutoplay, - PlayerStoryboardSpec, - Playlist, - PlaylistCustomThumbnail, - PlaylistHeader, - PlaylistInfoCardContent, - PlaylistMetadata, - PlaylistPanel, - PlaylistPanelVideo, - PlaylistPanelVideoWrapper, - PlaylistSidebar, - PlaylistSidebarPrimaryInfo, - PlaylistSidebarSecondaryInfo, - PlaylistVideo, - PlaylistVideoList, - PlaylistVideoThumbnail, - Poll, - Post, - PostMultiImage, - ProfileColumn, - ProfileColumnStats, - ProfileColumnStatsEntry, - ProfileColumnUserInfo, - RecognitionShelf, - ReelItem, - ReelShelf, - RelatedChipCloud, - RichGrid, - RichItem, - RichListHeader, - RichSection, - RichShelf, - SearchBox, - SearchRefinementCard, - SearchSuggestion, - SearchSuggestionsSection, - SecondarySearchContainer, - SectionList, - SegmentedLikeDislikeButton, - SettingBoolean, - SettingsCheckbox, - SettingsOptions, - SettingsSidebar, - SettingsSwitch, - Shelf, - ShowingResultsFor, - SimpleCardContent, - SimpleCardTeaser, - SimpleTextSection, - SingleActionEmergencySupport, - SingleColumnBrowseResults, - SingleColumnMusicWatchNextResults, - SingleHeroImage, - SlimOwner, - SlimVideoMetadata, - SortFilterSubMenu, - SubFeedOption, - SubFeedSelector, - SubscribeButton, - SubscriptionNotificationToggleButton, - Tab, - Tabbed, - TabbedSearchResults, - TextHeader, - ThumbnailLandscapePortrait, - ThumbnailOverlayBottomPanel, - ThumbnailOverlayEndorsement, - ThumbnailOverlayHoverText, - ThumbnailOverlayInlineUnplayable, - ThumbnailOverlayLoadingPreview, - ThumbnailOverlayNowPlaying, - ThumbnailOverlayPinking, - ThumbnailOverlayPlaybackStatus, - ThumbnailOverlayResumePlayback, - ThumbnailOverlaySidePanel, - ThumbnailOverlayTimeStatus, - ThumbnailOverlayToggleButton, - TimedMarkerDecoration, - TitleAndButtonListHeader, - ToggleButton, - ToggleMenuServiceItem, - Tooltip, - TopicChannelDetails, - TwoColumnBrowseResults, - TwoColumnSearchResults, - TwoColumnWatchNextResults, - UniversalWatchCard, - UpsellDialog, - VerticalList, - VerticalWatchCardList, - Video, - VideoCard, - VideoInfoCardContent, - VideoOwner, - VideoPrimaryInfo, - VideoSecondaryInfo, - WatchCardCompactVideo, - WatchCardHeroVideo, - WatchCardRichHeader, - WatchCardSectionSequence, - WatchNextEndScreen, - WatchNextTabbedResults, - AnchoredSection, - KidsCategoriesHeader, - KidsCategoryTab, - KidsHomeScreen -}; - -export const Misc = { - Author, - ChildElement, - EmojiRun, - Format, - PlaylistAuthor, - Text, - TextRun, - Thumbnail, - VideoDetails -}; diff --git a/src/parser/misc.ts b/src/parser/misc.ts new file mode 100644 index 000000000..87ab1e4d5 --- /dev/null +++ b/src/parser/misc.ts @@ -0,0 +1,11 @@ +// This file was auto generated, do not edit. +// See ./scripts/build-parser-map.js + +export { default as Author } from './classes/misc/Author.js'; +export { default as ChildElement } from './classes/misc/ChildElement.js'; +export { default as EmojiRun } from './classes/misc/EmojiRun.js'; +export { default as Format } from './classes/misc/Format.js'; +export { default as Text } from './classes/misc/Text.js'; +export { default as TextRun } from './classes/misc/TextRun.js'; +export { default as Thumbnail } from './classes/misc/Thumbnail.js'; +export { default as VideoDetails } from './classes/misc/VideoDetails.js'; diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts new file mode 100644 index 000000000..28c1a377b --- /dev/null +++ b/src/parser/nodes.ts @@ -0,0 +1,323 @@ +// This file was auto generated, do not edit. +// See ./scripts/build-parser-map.js + +export { default as AccountChannel } from './classes/AccountChannel.js'; +export { default as AccountItemSection } from './classes/AccountItemSection.js'; +export { default as AccountItemSectionHeader } from './classes/AccountItemSectionHeader.js'; +export { default as AccountSectionList } from './classes/AccountSectionList.js'; +export { default as AppendContinuationItemsAction } from './classes/actions/AppendContinuationItemsAction.js'; +export { default as OpenPopupAction } from './classes/actions/OpenPopupAction.js'; +export { default as Alert } from './classes/Alert.js'; +export { default as AnalyticsMainAppKeyMetrics } from './classes/analytics/AnalyticsMainAppKeyMetrics.js'; +export { default as AnalyticsRoot } from './classes/analytics/AnalyticsRoot.js'; +export { default as AnalyticsShortsCarouselCard } from './classes/analytics/AnalyticsShortsCarouselCard.js'; +export { default as AnalyticsVideo } from './classes/analytics/AnalyticsVideo.js'; +export { default as AnalyticsVodCarouselCard } from './classes/analytics/AnalyticsVodCarouselCard.js'; +export { default as CtaGoToCreatorStudio } from './classes/analytics/CtaGoToCreatorStudio.js'; +export { default as DataModelSection } from './classes/analytics/DataModelSection.js'; +export { default as StatRow } from './classes/analytics/StatRow.js'; +export { default as AudioOnlyPlayability } from './classes/AudioOnlyPlayability.js'; +export { default as AutomixPreviewVideo } from './classes/AutomixPreviewVideo.js'; +export { default as BackstageImage } from './classes/BackstageImage.js'; +export { default as BackstagePost } from './classes/BackstagePost.js'; +export { default as BackstagePostThread } from './classes/BackstagePostThread.js'; +export { default as BrowseFeedActions } from './classes/BrowseFeedActions.js'; +export { default as BrowserMediaSession } from './classes/BrowserMediaSession.js'; +export { default as Button } from './classes/Button.js'; +export { default as C4TabbedHeader } from './classes/C4TabbedHeader.js'; +export { default as CallToActionButton } from './classes/CallToActionButton.js'; +export { default as Card } from './classes/Card.js'; +export { default as CardCollection } from './classes/CardCollection.js'; +export { default as CarouselHeader } from './classes/CarouselHeader.js'; +export { default as CarouselItem } from './classes/CarouselItem.js'; +export { default as Channel } from './classes/Channel.js'; +export { default as ChannelAboutFullMetadata } from './classes/ChannelAboutFullMetadata.js'; +export { default as ChannelAgeGate } from './classes/ChannelAgeGate.js'; +export { default as ChannelFeaturedContent } from './classes/ChannelFeaturedContent.js'; +export { default as ChannelHeaderLinks } from './classes/ChannelHeaderLinks.js'; +export { default as ChannelMetadata } from './classes/ChannelMetadata.js'; +export { default as ChannelMobileHeader } from './classes/ChannelMobileHeader.js'; +export { default as ChannelOptions } from './classes/ChannelOptions.js'; +export { default as ChannelSubMenu } from './classes/ChannelSubMenu.js'; +export { default as ChannelThumbnailWithLink } from './classes/ChannelThumbnailWithLink.js'; +export { default as ChannelVideoPlayer } from './classes/ChannelVideoPlayer.js'; +export { default as Chapter } from './classes/Chapter.js'; +export { default as ChildVideo } from './classes/ChildVideo.js'; +export { default as ChipCloud } from './classes/ChipCloud.js'; +export { default as ChipCloudChip } from './classes/ChipCloudChip.js'; +export { default as CollaboratorInfoCardContent } from './classes/CollaboratorInfoCardContent.js'; +export { default as CollageHeroImage } from './classes/CollageHeroImage.js'; +export { default as AuthorCommentBadge } from './classes/comments/AuthorCommentBadge.js'; +export { default as Comment } from './classes/comments/Comment.js'; +export { default as CommentActionButtons } from './classes/comments/CommentActionButtons.js'; +export { default as CommentDialog } from './classes/comments/CommentDialog.js'; +export { default as CommentReplies } from './classes/comments/CommentReplies.js'; +export { default as CommentReplyDialog } from './classes/comments/CommentReplyDialog.js'; +export { default as CommentsEntryPointHeader } from './classes/comments/CommentsEntryPointHeader.js'; +export { default as CommentsHeader } from './classes/comments/CommentsHeader.js'; +export { default as CommentSimplebox } from './classes/comments/CommentSimplebox.js'; +export { default as CommentThread } from './classes/comments/CommentThread.js'; +export { default as CreatorHeart } from './classes/comments/CreatorHeart.js'; +export { default as EmojiPicker } from './classes/comments/EmojiPicker.js'; +export { default as PdgCommentChip } from './classes/comments/PdgCommentChip.js'; +export { default as SponsorCommentBadge } from './classes/comments/SponsorCommentBadge.js'; +export { default as CompactChannel } from './classes/CompactChannel.js'; +export { default as CompactLink } from './classes/CompactLink.js'; +export { default as CompactMix } from './classes/CompactMix.js'; +export { default as CompactPlaylist } from './classes/CompactPlaylist.js'; +export { default as CompactStation } from './classes/CompactStation.js'; +export { default as CompactVideo } from './classes/CompactVideo.js'; +export { default as ConfirmDialog } from './classes/ConfirmDialog.js'; +export { default as ContinuationItem } from './classes/ContinuationItem.js'; +export { default as CopyLink } from './classes/CopyLink.js'; +export { default as CreatePlaylistDialog } from './classes/CreatePlaylistDialog.js'; +export { default as DecoratedPlayerBar } from './classes/DecoratedPlayerBar.js'; +export { default as DefaultPromoPanel } from './classes/DefaultPromoPanel.js'; +export { default as DidYouMean } from './classes/DidYouMean.js'; +export { default as DownloadButton } from './classes/DownloadButton.js'; +export { default as Dropdown } from './classes/Dropdown.js'; +export { default as DropdownItem } from './classes/DropdownItem.js'; +export { default as Element } from './classes/Element.js'; +export { default as EmergencyOnebox } from './classes/EmergencyOnebox.js'; +export { default as EmojiPickerCategory } from './classes/EmojiPickerCategory.js'; +export { default as EmojiPickerCategoryButton } from './classes/EmojiPickerCategoryButton.js'; +export { default as EmojiPickerUpsellCategory } from './classes/EmojiPickerUpsellCategory.js'; +export { default as Endscreen } from './classes/Endscreen.js'; +export { default as EndscreenElement } from './classes/EndscreenElement.js'; +export { default as EndScreenPlaylist } from './classes/EndScreenPlaylist.js'; +export { default as EndScreenVideo } from './classes/EndScreenVideo.js'; +export { default as ExpandableMetadata } from './classes/ExpandableMetadata.js'; +export { default as ExpandableTab } from './classes/ExpandableTab.js'; +export { default as ExpandedShelfContents } from './classes/ExpandedShelfContents.js'; +export { default as FeedFilterChipBar } from './classes/FeedFilterChipBar.js'; +export { default as FeedTabbedHeader } from './classes/FeedTabbedHeader.js'; +export { default as GameCard } from './classes/GameCard.js'; +export { default as GameDetails } from './classes/GameDetails.js'; +export { default as Grid } from './classes/Grid.js'; +export { default as GridChannel } from './classes/GridChannel.js'; +export { default as GridHeader } from './classes/GridHeader.js'; +export { default as GridPlaylist } from './classes/GridPlaylist.js'; +export { default as GridVideo } from './classes/GridVideo.js'; +export { default as HashtagHeader } from './classes/HashtagHeader.js'; +export { default as Heatmap } from './classes/Heatmap.js'; +export { default as HeatMarker } from './classes/HeatMarker.js'; +export { default as HighlightsCarousel } from './classes/HighlightsCarousel.js'; +export { default as HistorySuggestion } from './classes/HistorySuggestion.js'; +export { default as HorizontalCardList } from './classes/HorizontalCardList.js'; +export { default as HorizontalList } from './classes/HorizontalList.js'; +export { default as IconLink } from './classes/IconLink.js'; +export { default as InteractiveTabbedHeader } from './classes/InteractiveTabbedHeader.js'; +export { default as ItemSection } from './classes/ItemSection.js'; +export { default as ItemSectionHeader } from './classes/ItemSectionHeader.js'; +export { default as ItemSectionTab } from './classes/ItemSectionTab.js'; +export { default as ItemSectionTabbedHeader } from './classes/ItemSectionTabbedHeader.js'; +export { default as LikeButton } from './classes/LikeButton.js'; +export { default as LiveChat } from './classes/LiveChat.js'; +export { default as AddBannerToLiveChatCommand } from './classes/livechat/AddBannerToLiveChatCommand.js'; +export { default as AddChatItemAction } from './classes/livechat/AddChatItemAction.js'; +export { default as AddLiveChatTickerItemAction } from './classes/livechat/AddLiveChatTickerItemAction.js'; +export { default as DimChatItemAction } from './classes/livechat/DimChatItemAction.js'; +export { default as LiveChatAutoModMessage } from './classes/livechat/items/LiveChatAutoModMessage.js'; +export { default as LiveChatBanner } from './classes/livechat/items/LiveChatBanner.js'; +export { default as LiveChatBannerHeader } from './classes/livechat/items/LiveChatBannerHeader.js'; +export { default as LiveChatBannerPoll } from './classes/livechat/items/LiveChatBannerPoll.js'; +export { default as LiveChatMembershipItem } from './classes/livechat/items/LiveChatMembershipItem.js'; +export { default as LiveChatPaidMessage } from './classes/livechat/items/LiveChatPaidMessage.js'; +export { default as LiveChatPaidSticker } from './classes/livechat/items/LiveChatPaidSticker.js'; +export { default as LiveChatPlaceholderItem } from './classes/livechat/items/LiveChatPlaceholderItem.js'; +export { default as LiveChatProductItem } from './classes/livechat/items/LiveChatProductItem.js'; +export { default as LiveChatRestrictedParticipation } from './classes/livechat/items/LiveChatRestrictedParticipation.js'; +export { default as LiveChatTextMessage } from './classes/livechat/items/LiveChatTextMessage.js'; +export { default as LiveChatTickerPaidMessageItem } from './classes/livechat/items/LiveChatTickerPaidMessageItem.js'; +export { default as LiveChatTickerPaidStickerItem } from './classes/livechat/items/LiveChatTickerPaidStickerItem.js'; +export { default as LiveChatTickerSponsorItem } from './classes/livechat/items/LiveChatTickerSponsorItem.js'; +export { default as LiveChatViewerEngagementMessage } from './classes/livechat/items/LiveChatViewerEngagementMessage.js'; +export { default as PollHeader } from './classes/livechat/items/PollHeader.js'; +export { default as LiveChatActionPanel } from './classes/livechat/LiveChatActionPanel.js'; +export { default as MarkChatItemAsDeletedAction } from './classes/livechat/MarkChatItemAsDeletedAction.js'; +export { default as MarkChatItemsByAuthorAsDeletedAction } from './classes/livechat/MarkChatItemsByAuthorAsDeletedAction.js'; +export { default as RemoveBannerForLiveChatCommand } from './classes/livechat/RemoveBannerForLiveChatCommand.js'; +export { default as RemoveChatItemAction } from './classes/livechat/RemoveChatItemAction.js'; +export { default as RemoveChatItemByAuthorAction } from './classes/livechat/RemoveChatItemByAuthorAction.js'; +export { default as ReplaceChatItemAction } from './classes/livechat/ReplaceChatItemAction.js'; +export { default as ReplayChatItemAction } from './classes/livechat/ReplayChatItemAction.js'; +export { default as ShowLiveChatActionPanelAction } from './classes/livechat/ShowLiveChatActionPanelAction.js'; +export { default as ShowLiveChatDialogAction } from './classes/livechat/ShowLiveChatDialogAction.js'; +export { default as ShowLiveChatTooltipCommand } from './classes/livechat/ShowLiveChatTooltipCommand.js'; +export { default as UpdateDateTextAction } from './classes/livechat/UpdateDateTextAction.js'; +export { default as UpdateDescriptionAction } from './classes/livechat/UpdateDescriptionAction.js'; +export { default as UpdateLiveChatPollAction } from './classes/livechat/UpdateLiveChatPollAction.js'; +export { default as UpdateTitleAction } from './classes/livechat/UpdateTitleAction.js'; +export { default as UpdateToggleButtonTextAction } from './classes/livechat/UpdateToggleButtonTextAction.js'; +export { default as UpdateViewershipAction } from './classes/livechat/UpdateViewershipAction.js'; +export { default as LiveChatAuthorBadge } from './classes/LiveChatAuthorBadge.js'; +export { default as LiveChatDialog } from './classes/LiveChatDialog.js'; +export { default as LiveChatHeader } from './classes/LiveChatHeader.js'; +export { default as LiveChatItemList } from './classes/LiveChatItemList.js'; +export { default as LiveChatMessageInput } from './classes/LiveChatMessageInput.js'; +export { default as LiveChatParticipant } from './classes/LiveChatParticipant.js'; +export { default as LiveChatParticipantsList } from './classes/LiveChatParticipantsList.js'; +export { default as MacroMarkersListItem } from './classes/MacroMarkersListItem.js'; +export { default as Menu } from './classes/menus/Menu.js'; +export { default as MenuNavigationItem } from './classes/menus/MenuNavigationItem.js'; +export { default as MenuServiceItem } from './classes/menus/MenuServiceItem.js'; +export { default as MenuServiceItemDownload } from './classes/menus/MenuServiceItemDownload.js'; +export { default as MultiPageMenu } from './classes/menus/MultiPageMenu.js'; +export { default as MultiPageMenuNotificationSection } from './classes/menus/MultiPageMenuNotificationSection.js'; +export { default as MusicMenuItemDivider } from './classes/menus/MusicMenuItemDivider.js'; +export { default as MusicMultiSelectMenu } from './classes/menus/MusicMultiSelectMenu.js'; +export { default as MusicMultiSelectMenuItem } from './classes/menus/MusicMultiSelectMenuItem.js'; +export { default as SimpleMenuHeader } from './classes/menus/SimpleMenuHeader.js'; +export { default as MerchandiseItem } from './classes/MerchandiseItem.js'; +export { default as MerchandiseShelf } from './classes/MerchandiseShelf.js'; +export { default as Message } from './classes/Message.js'; +export { default as MetadataBadge } from './classes/MetadataBadge.js'; +export { default as MetadataRow } from './classes/MetadataRow.js'; +export { default as MetadataRowContainer } from './classes/MetadataRowContainer.js'; +export { default as MetadataRowHeader } from './classes/MetadataRowHeader.js'; +export { default as MetadataScreen } from './classes/MetadataScreen.js'; +export { default as MicroformatData } from './classes/MicroformatData.js'; +export { default as Mix } from './classes/Mix.js'; +export { default as Movie } from './classes/Movie.js'; +export { default as MovingThumbnail } from './classes/MovingThumbnail.js'; +export { default as MultiMarkersPlayerBar } from './classes/MultiMarkersPlayerBar.js'; +export { default as MusicCarouselShelf } from './classes/MusicCarouselShelf.js'; +export { default as MusicCarouselShelfBasicHeader } from './classes/MusicCarouselShelfBasicHeader.js'; +export { default as MusicDescriptionShelf } from './classes/MusicDescriptionShelf.js'; +export { default as MusicDetailHeader } from './classes/MusicDetailHeader.js'; +export { default as MusicDownloadStateBadge } from './classes/MusicDownloadStateBadge.js'; +export { default as MusicEditablePlaylistDetailHeader } from './classes/MusicEditablePlaylistDetailHeader.js'; +export { default as MusicElementHeader } from './classes/MusicElementHeader.js'; +export { default as MusicHeader } from './classes/MusicHeader.js'; +export { default as MusicImmersiveHeader } from './classes/MusicImmersiveHeader.js'; +export { default as MusicInlineBadge } from './classes/MusicInlineBadge.js'; +export { default as MusicItemThumbnailOverlay } from './classes/MusicItemThumbnailOverlay.js'; +export { default as MusicLargeCardItemCarousel } from './classes/MusicLargeCardItemCarousel.js'; +export { default as MusicNavigationButton } from './classes/MusicNavigationButton.js'; +export { default as MusicPlayButton } from './classes/MusicPlayButton.js'; +export { default as MusicPlaylistShelf } from './classes/MusicPlaylistShelf.js'; +export { default as MusicQueue } from './classes/MusicQueue.js'; +export { default as MusicResponsiveListItem } from './classes/MusicResponsiveListItem.js'; +export { default as MusicResponsiveListItemFixedColumn } from './classes/MusicResponsiveListItemFixedColumn.js'; +export { default as MusicResponsiveListItemFlexColumn } from './classes/MusicResponsiveListItemFlexColumn.js'; +export { default as MusicShelf } from './classes/MusicShelf.js'; +export { default as MusicSideAlignedItem } from './classes/MusicSideAlignedItem.js'; +export { default as MusicSortFilterButton } from './classes/MusicSortFilterButton.js'; +export { default as MusicThumbnail } from './classes/MusicThumbnail.js'; +export { default as MusicTwoRowItem } from './classes/MusicTwoRowItem.js'; +export { default as MusicVisualHeader } from './classes/MusicVisualHeader.js'; +export { default as NavigationEndpoint } from './classes/NavigationEndpoint.js'; +export { default as Notification } from './classes/Notification.js'; +export { default as PageIntroduction } from './classes/PageIntroduction.js'; +export { default as PlayerAnnotationsExpanded } from './classes/PlayerAnnotationsExpanded.js'; +export { default as PlayerCaptionsTracklist } from './classes/PlayerCaptionsTracklist.js'; +export { default as PlayerErrorMessage } from './classes/PlayerErrorMessage.js'; +export { default as PlayerLiveStoryboardSpec } from './classes/PlayerLiveStoryboardSpec.js'; +export { default as PlayerMicroformat } from './classes/PlayerMicroformat.js'; +export { default as PlayerOverlay } from './classes/PlayerOverlay.js'; +export { default as PlayerOverlayAutoplay } from './classes/PlayerOverlayAutoplay.js'; +export { default as PlayerStoryboardSpec } from './classes/PlayerStoryboardSpec.js'; +export { default as Playlist } from './classes/Playlist.js'; +export { default as PlaylistCustomThumbnail } from './classes/PlaylistCustomThumbnail.js'; +export { default as PlaylistHeader } from './classes/PlaylistHeader.js'; +export { default as PlaylistInfoCardContent } from './classes/PlaylistInfoCardContent.js'; +export { default as PlaylistMetadata } from './classes/PlaylistMetadata.js'; +export { default as PlaylistPanel } from './classes/PlaylistPanel.js'; +export { default as PlaylistPanelVideo } from './classes/PlaylistPanelVideo.js'; +export { default as PlaylistPanelVideoWrapper } from './classes/PlaylistPanelVideoWrapper.js'; +export { default as PlaylistSidebar } from './classes/PlaylistSidebar.js'; +export { default as PlaylistSidebarPrimaryInfo } from './classes/PlaylistSidebarPrimaryInfo.js'; +export { default as PlaylistSidebarSecondaryInfo } from './classes/PlaylistSidebarSecondaryInfo.js'; +export { default as PlaylistVideo } from './classes/PlaylistVideo.js'; +export { default as PlaylistVideoList } from './classes/PlaylistVideoList.js'; +export { default as PlaylistVideoThumbnail } from './classes/PlaylistVideoThumbnail.js'; +export { default as Poll } from './classes/Poll.js'; +export { default as Post } from './classes/Post.js'; +export { default as PostMultiImage } from './classes/PostMultiImage.js'; +export { default as ProfileColumn } from './classes/ProfileColumn.js'; +export { default as ProfileColumnStats } from './classes/ProfileColumnStats.js'; +export { default as ProfileColumnStatsEntry } from './classes/ProfileColumnStatsEntry.js'; +export { default as ProfileColumnUserInfo } from './classes/ProfileColumnUserInfo.js'; +export { default as RecognitionShelf } from './classes/RecognitionShelf.js'; +export { default as ReelItem } from './classes/ReelItem.js'; +export { default as ReelShelf } from './classes/ReelShelf.js'; +export { default as RelatedChipCloud } from './classes/RelatedChipCloud.js'; +export { default as RichGrid } from './classes/RichGrid.js'; +export { default as RichItem } from './classes/RichItem.js'; +export { default as RichListHeader } from './classes/RichListHeader.js'; +export { default as RichSection } from './classes/RichSection.js'; +export { default as RichShelf } from './classes/RichShelf.js'; +export { default as SearchBox } from './classes/SearchBox.js'; +export { default as SearchRefinementCard } from './classes/SearchRefinementCard.js'; +export { default as SearchSuggestion } from './classes/SearchSuggestion.js'; +export { default as SearchSuggestionsSection } from './classes/SearchSuggestionsSection.js'; +export { default as SecondarySearchContainer } from './classes/SecondarySearchContainer.js'; +export { default as SectionList } from './classes/SectionList.js'; +export { default as SegmentedLikeDislikeButton } from './classes/SegmentedLikeDislikeButton.js'; +export { default as SettingBoolean } from './classes/SettingBoolean.js'; +export { default as SettingsCheckbox } from './classes/SettingsCheckbox.js'; +export { default as SettingsOptions } from './classes/SettingsOptions.js'; +export { default as SettingsSidebar } from './classes/SettingsSidebar.js'; +export { default as SettingsSwitch } from './classes/SettingsSwitch.js'; +export { default as Shelf } from './classes/Shelf.js'; +export { default as ShowingResultsFor } from './classes/ShowingResultsFor.js'; +export { default as SimpleCardContent } from './classes/SimpleCardContent.js'; +export { default as SimpleCardTeaser } from './classes/SimpleCardTeaser.js'; +export { default as SimpleTextSection } from './classes/SimpleTextSection.js'; +export { default as SingleActionEmergencySupport } from './classes/SingleActionEmergencySupport.js'; +export { default as SingleColumnBrowseResults } from './classes/SingleColumnBrowseResults.js'; +export { default as SingleColumnMusicWatchNextResults } from './classes/SingleColumnMusicWatchNextResults.js'; +export { default as SingleHeroImage } from './classes/SingleHeroImage.js'; +export { default as SlimOwner } from './classes/SlimOwner.js'; +export { default as SlimVideoMetadata } from './classes/SlimVideoMetadata.js'; +export { default as SortFilterSubMenu } from './classes/SortFilterSubMenu.js'; +export { default as SubFeedOption } from './classes/SubFeedOption.js'; +export { default as SubFeedSelector } from './classes/SubFeedSelector.js'; +export { default as SubscribeButton } from './classes/SubscribeButton.js'; +export { default as SubscriptionNotificationToggleButton } from './classes/SubscriptionNotificationToggleButton.js'; +export { default as Tab } from './classes/Tab.js'; +export { default as Tabbed } from './classes/Tabbed.js'; +export { default as TabbedSearchResults } from './classes/TabbedSearchResults.js'; +export { default as TextHeader } from './classes/TextHeader.js'; +export { default as ThumbnailLandscapePortrait } from './classes/ThumbnailLandscapePortrait.js'; +export { default as ThumbnailOverlayBottomPanel } from './classes/ThumbnailOverlayBottomPanel.js'; +export { default as ThumbnailOverlayEndorsement } from './classes/ThumbnailOverlayEndorsement.js'; +export { default as ThumbnailOverlayHoverText } from './classes/ThumbnailOverlayHoverText.js'; +export { default as ThumbnailOverlayInlineUnplayable } from './classes/ThumbnailOverlayInlineUnplayable.js'; +export { default as ThumbnailOverlayLoadingPreview } from './classes/ThumbnailOverlayLoadingPreview.js'; +export { default as ThumbnailOverlayNowPlaying } from './classes/ThumbnailOverlayNowPlaying.js'; +export { default as ThumbnailOverlayPinking } from './classes/ThumbnailOverlayPinking.js'; +export { default as ThumbnailOverlayPlaybackStatus } from './classes/ThumbnailOverlayPlaybackStatus.js'; +export { default as ThumbnailOverlayResumePlayback } from './classes/ThumbnailOverlayResumePlayback.js'; +export { default as ThumbnailOverlaySidePanel } from './classes/ThumbnailOverlaySidePanel.js'; +export { default as ThumbnailOverlayTimeStatus } from './classes/ThumbnailOverlayTimeStatus.js'; +export { default as ThumbnailOverlayToggleButton } from './classes/ThumbnailOverlayToggleButton.js'; +export { default as TimedMarkerDecoration } from './classes/TimedMarkerDecoration.js'; +export { default as TitleAndButtonListHeader } from './classes/TitleAndButtonListHeader.js'; +export { default as ToggleButton } from './classes/ToggleButton.js'; +export { default as ToggleMenuServiceItem } from './classes/ToggleMenuServiceItem.js'; +export { default as Tooltip } from './classes/Tooltip.js'; +export { default as TopicChannelDetails } from './classes/TopicChannelDetails.js'; +export { default as TwoColumnBrowseResults } from './classes/TwoColumnBrowseResults.js'; +export { default as TwoColumnSearchResults } from './classes/TwoColumnSearchResults.js'; +export { default as TwoColumnWatchNextResults } from './classes/TwoColumnWatchNextResults.js'; +export { default as UniversalWatchCard } from './classes/UniversalWatchCard.js'; +export { default as UpsellDialog } from './classes/UpsellDialog.js'; +export { default as VerticalList } from './classes/VerticalList.js'; +export { default as VerticalWatchCardList } from './classes/VerticalWatchCardList.js'; +export { default as Video } from './classes/Video.js'; +export { default as VideoCard } from './classes/VideoCard.js'; +export { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.js'; +export { default as VideoOwner } from './classes/VideoOwner.js'; +export { default as VideoPrimaryInfo } from './classes/VideoPrimaryInfo.js'; +export { default as VideoSecondaryInfo } from './classes/VideoSecondaryInfo.js'; +export { default as WatchCardCompactVideo } from './classes/WatchCardCompactVideo.js'; +export { default as WatchCardHeroVideo } from './classes/WatchCardHeroVideo.js'; +export { default as WatchCardRichHeader } from './classes/WatchCardRichHeader.js'; +export { default as WatchCardSectionSequence } from './classes/WatchCardSectionSequence.js'; +export { default as WatchNextEndScreen } from './classes/WatchNextEndScreen.js'; +export { default as WatchNextTabbedResults } from './classes/WatchNextTabbedResults.js'; +export { default as AnchoredSection } from './classes/ytkids/AnchoredSection.js'; +export { default as KidsCategoriesHeader } from './classes/ytkids/KidsCategoriesHeader.js'; +export { default as KidsCategoryTab } from './classes/ytkids/KidsCategoryTab.js'; +export { default as KidsHomeScreen } from './classes/ytkids/KidsHomeScreen.js'; diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 31597f552..e7da56474 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -21,7 +21,7 @@ import Thumbnail from './classes/misc/Thumbnail.js'; import { InnertubeError, ParsingError, Platform } from '../utils/Utils.js'; import { Memo, observe, ObservedArray, SuperParsedResult, YTNode, YTNodeConstructor } from './helpers.js'; -import { YTNodes } from './map.js'; +import * as YTNodes from './nodes.js'; import { YTNodeGenerator } from './generator.js'; export type ParserError = { classname: string, classdata: any, err: any }; From ad3aeee6eb7546fd24f946d0f385faf3029c5b17 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Thu, 2 Mar 2023 09:16:05 +0200 Subject: [PATCH 07/17] feat(YTNodeGenerator): primative types Add support for inferring primatives types --- src/parser/generator.ts | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/parser/generator.ts b/src/parser/generator.ts index 80f718edc..6ba60b8fb 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -38,9 +38,13 @@ export type InferenceType = { renderers: string[], optional: boolean, } | MiscInferenceType | { + type: 'primative', + typeof: ('string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'undefined' | 'object' | 'function')[], + optional: boolean, +} | { type: 'unknown', optional: boolean, -}; +} export type KeyInfo = (readonly [string, InferenceType])[]; @@ -162,6 +166,18 @@ export class YTNodeGenerator { } } break; + case 'primative': + { + if (new_type.type !== 'primative') continue; + const resolved_key: InferenceType = { + type: 'primative', + typeof: Array.from(new Set([ ...new_type.typeof, ...type.typeof ])), + 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; } } @@ -239,11 +255,11 @@ export class YTNodeGenerator { const dependencies = new Map(); for (const [ , value ] of key_info) { if (value.type === 'renderer' || value.type === 'renderer_list') - value.renderers.forEach((renderer) => { - const example = this.#renderers_examples.get(renderer); + for (const renderer of value.renderers) { + const example = this.#renderers_examples[renderer]; if (example) dependencies.set(renderer, example); - }); + } } const unimplemented_dependencies = Array.from(dependencies).filter(([ classname ]) => !Parser.hasParser(classname)); @@ -298,6 +314,8 @@ export class YTNodeGenerator { return inference_type.misc_type; } throw new Error('Unreachable code reached! Switch missing case!'); + case 'primative': + return inference_type.typeof.join(' | '); case 'unknown': return '/* TODO: determine correct type */ unknown'; } @@ -334,6 +352,7 @@ export class YTNodeGenerator { if (parser === 'undefined') throw new Error('Unreachable code reached! Switch missing case!'); break; + case 'primative': case 'unknown': parser = `data.${key}`; break; @@ -368,6 +387,7 @@ export class YTNodeGenerator { } } throw new Error('Unreachable code reached! Switch missing case!'); + case 'primative': case 'unknown': return data[key]; } @@ -470,7 +490,8 @@ export class YTNodeGenerator { return return_value as MiscInferenceType; } return { - type: 'unknown', + type: 'primative', + typeof: [ typeof value ], optional: false }; } @@ -526,4 +547,4 @@ export class YTNodeGenerator { } return false; } -} +} \ No newline at end of file From 777c8a98fd5f92d24e3c9931419b166820386a54 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Thu, 2 Mar 2023 10:13:57 +0200 Subject: [PATCH 08/17] fix(YTNodeGenerator): NavigationEndpoint detection --- src/parser/generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/generator.ts b/src/parser/generator.ts index 6ba60b8fb..da0e0d80b 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -509,7 +509,7 @@ export class YTNodeGenerator { } static isMiscType(key: string, value: any): MiscInferenceType | false { // NavigationEndpoint - if ((key.endsWith('Endpoint') || key.endsWith('Command')) && typeof value === 'object') { + if ((key.endsWith('Endpoint') || key.endsWith('Command') || key === 'endpoint') && typeof value === 'object') { return { type: 'misc', endpoint: new NavigationEndpoint(value), From 92e5f9960b811503008360caa8618d6ce31164c4 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Thu, 2 Mar 2023 10:27:03 +0200 Subject: [PATCH 09/17] fix(YTNodeGenerator): fix generated typescript Correct types and linting for generated typescript class --- src/parser/generator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser/generator.ts b/src/parser/generator.ts index da0e0d80b..44339871e 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -300,11 +300,11 @@ export class YTNodeGenerator { switch (inference_type.type) { case 'renderer': { - return `${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')}`; + return `${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')} | null`; } case 'renderer_list': { - return `ObservedArray<${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')}>`; + return `ObservedArray<${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')}> | null`; } case 'misc': switch (inference_type.misc_type) { @@ -325,12 +325,12 @@ export class YTNodeGenerator { switch (inference_type.type) { case 'renderer': { - parser = `Parser.parseItem(data.${key}, [${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')}])`; + parser = `Parser.parseItem(data.${key}, [ ${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')} ])`; } break; case 'renderer_list': { - parser = `Parser.parse(data.${key}, true, [${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')}])`; + parser = `Parser.parse(data.${key}, true, [ ${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')} ])`; } break; case 'misc': From 13f7f228351e3b6ca944f68cd1adef1500c9adea Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Wed, 8 Mar 2023 16:36:58 +0200 Subject: [PATCH 10/17] chore: update parsers after merge --- src/parser/classes/SharedPost.ts | 2 +- src/parser/misc.ts | 1 + src/parser/nodes.ts | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/parser/classes/SharedPost.ts b/src/parser/classes/SharedPost.ts index 5782cfa76..418f00a03 100644 --- a/src/parser/classes/SharedPost.ts +++ b/src/parser/classes/SharedPost.ts @@ -1,5 +1,5 @@ import { YTNode } from '../helpers.js'; -import { Menu } from '../map.js'; +import { Menu } from '../nodes.js'; import Parser from '../parser.js'; import BackstagePost from './BackstagePost.js'; import Button from './Button.js'; diff --git a/src/parser/misc.ts b/src/parser/misc.ts index 87ab1e4d5..d34d34126 100644 --- a/src/parser/misc.ts +++ b/src/parser/misc.ts @@ -5,6 +5,7 @@ export { default as Author } from './classes/misc/Author.js'; export { default as ChildElement } from './classes/misc/ChildElement.js'; export { default as EmojiRun } from './classes/misc/EmojiRun.js'; export { default as Format } from './classes/misc/Format.js'; +export { default as PlaylistAuthor } from './classes/misc/PlaylistAuthor.js'; export { default as Text } from './classes/misc/Text.js'; export { default as TextRun } from './classes/misc/TextRun.js'; export { default as Thumbnail } from './classes/misc/Thumbnail.js'; diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index 28c1a377b..a5ca8840c 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -98,9 +98,16 @@ export { default as GridChannel } from './classes/GridChannel.js'; export { default as GridHeader } from './classes/GridHeader.js'; export { default as GridPlaylist } from './classes/GridPlaylist.js'; export { default as GridVideo } from './classes/GridVideo.js'; +export { default as GuideCollapsibleEntry } from './classes/GuideCollapsibleEntry.js'; +export { default as GuideCollapsibleSectionEntry } from './classes/GuideCollapsibleSectionEntry.js'; +export { default as GuideDownloadsEntry } from './classes/GuideDownloadsEntry.js'; +export { default as GuideEntry } from './classes/GuideEntry.js'; +export { default as GuideSection } from './classes/GuideSection.js'; +export { default as GuideSubscriptionsSection } from './classes/GuideSubscriptionsSection.js'; export { default as HashtagHeader } from './classes/HashtagHeader.js'; export { default as Heatmap } from './classes/Heatmap.js'; export { default as HeatMarker } from './classes/HeatMarker.js'; +export { default as HeroPlaylistThumbnail } from './classes/HeroPlaylistThumbnail.js'; export { default as HighlightsCarousel } from './classes/HighlightsCarousel.js'; export { default as HistorySuggestion } from './classes/HistorySuggestion.js'; export { default as HorizontalCardList } from './classes/HorizontalCardList.js'; @@ -212,6 +219,7 @@ export { default as PageIntroduction } from './classes/PageIntroduction.js'; export { default as PlayerAnnotationsExpanded } from './classes/PlayerAnnotationsExpanded.js'; export { default as PlayerCaptionsTracklist } from './classes/PlayerCaptionsTracklist.js'; export { default as PlayerErrorMessage } from './classes/PlayerErrorMessage.js'; +export { default as PlayerLegacyDesktopYpcOffer } from './classes/PlayerLegacyDesktopYpcOffer.js'; export { default as PlayerLiveStoryboardSpec } from './classes/PlayerLiveStoryboardSpec.js'; export { default as PlayerMicroformat } from './classes/PlayerMicroformat.js'; export { default as PlayerOverlay } from './classes/PlayerOverlay.js'; @@ -245,10 +253,15 @@ export { default as RelatedChipCloud } from './classes/RelatedChipCloud.js'; export { default as RichGrid } from './classes/RichGrid.js'; export { default as RichItem } from './classes/RichItem.js'; export { default as RichListHeader } from './classes/RichListHeader.js'; +export { default as RichMetadata } from './classes/RichMetadata.js'; +export { default as RichMetadataRow } from './classes/RichMetadataRow.js'; export { default as RichSection } from './classes/RichSection.js'; export { default as RichShelf } from './classes/RichShelf.js'; export { default as SearchBox } from './classes/SearchBox.js'; +export { default as SearchFilter } from './classes/SearchFilter.js'; +export { default as SearchFilterGroup } from './classes/SearchFilterGroup.js'; export { default as SearchRefinementCard } from './classes/SearchRefinementCard.js'; +export { default as SearchSubMenu } from './classes/SearchSubMenu.js'; export { default as SearchSuggestion } from './classes/SearchSuggestion.js'; export { default as SearchSuggestionsSection } from './classes/SearchSuggestionsSection.js'; export { default as SecondarySearchContainer } from './classes/SecondarySearchContainer.js'; @@ -259,6 +272,7 @@ export { default as SettingsCheckbox } from './classes/SettingsCheckbox.js'; export { default as SettingsOptions } from './classes/SettingsOptions.js'; export { default as SettingsSidebar } from './classes/SettingsSidebar.js'; export { default as SettingsSwitch } from './classes/SettingsSwitch.js'; +export { default as SharedPost } from './classes/SharedPost.js'; export { default as Shelf } from './classes/Shelf.js'; export { default as ShowingResultsFor } from './classes/ShowingResultsFor.js'; export { default as SimpleCardContent } from './classes/SimpleCardContent.js'; From 611021ad1d3bf47284b648448e25ea08a1467116 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Wed, 8 Mar 2023 18:59:15 +0200 Subject: [PATCH 11/17] feat: add support for object type inference --- src/parser/generator.ts | 108 +++++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 23 deletions(-) diff --git a/src/parser/generator.ts b/src/parser/generator.ts index 44339871e..e7f777d98 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -38,8 +38,12 @@ export type InferenceType = { renderers: string[], optional: boolean, } | MiscInferenceType | { + type: 'object', + keys: KeyInfo, + optional: boolean, +} | { type: 'primative', - typeof: ('string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'undefined' | 'object' | 'function')[], + typeof: ('string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'undefined' | 'function')[], optional: boolean, } | { type: 'unknown', @@ -90,6 +94,19 @@ export class YTNodeGenerator { } // We've got the same type, so we can now resolve the changes switch (type.type) { + case 'object': + { + if (new_type.type !== 'object') continue; + const { resolved_key_info } = this.mergeKeyInfo(type.keys, new_type.keys); + const resolved_key: InferenceType = { + type: 'object', + keys: resolved_key_info, + 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 'renderer': { if (new_type.type !== 'renderer') continue; @@ -294,9 +311,9 @@ export class YTNodeGenerator { props.push(`${snake_key}${value.optional ? '?' : ''}: ${this.toTypeDeclaration(value)};`); constructor_lines.push(`this.${snake_key} = ${this.toParser(key, value)};`); } - return `class ${classname} extends YTNode {\n static type = '${classname}';\n\n ${props.join('\n ')}\n\n constructor(data: any) {\n ${constructor_lines.join('\n ')}\n }\n}\n`; + 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`; } - static toTypeDeclaration(inference_type: InferenceType) { + static toTypeDeclaration(inference_type: InferenceType, indentation = 0): string { switch (inference_type.type) { case 'renderer': { @@ -306,6 +323,10 @@ export class YTNodeGenerator { { return `ObservedArray<${inference_type.renderers.map((type) => `YTNodes.${type}`).join(' | ')}> | null`; } + case 'object': + { + return `{\n${inference_type.keys.map(([ key, value ]) => `${' '.repeat((indentation + 2) * 2)}${key}${value.optional ? '?' : ''}: ${this.toTypeDeclaration(value, indentation + 1)}`).join(',\n')}\n${' '.repeat((indentation + 1) * 2)}}`; + } case 'misc': switch (inference_type.misc_type) { case 'Thumbnail': @@ -320,33 +341,39 @@ export class YTNodeGenerator { return '/* TODO: determine correct type */ unknown'; } } - static toParser(key: string, inference_type: InferenceType) { + static toParser(key: string, inference_type: InferenceType, key_path: string[] = [ 'data' ], indentation = 1) { let parser = 'undefined'; switch (inference_type.type) { case 'renderer': { - parser = `Parser.parseItem(data.${key}, [ ${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')} ])`; + parser = `Parser.parseItem(${key_path.join('.')}.${key}, [ ${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')} ])`; } break; case 'renderer_list': { - parser = `Parser.parse(data.${key}, true, [ ${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')} ])`; + parser = `Parser.parse(${key_path.join('.')}.${key}, true, [ ${inference_type.renderers.map((type) => `YTNodes.${type}`).join(', ')} ])`; + } + break; + case 'object': + { + const new_keypath = [ ...key_path, key ]; + parser = `{\n${inference_type.keys.map(([ key, value ]) => `${' '.repeat((indentation + 2) * 2)}${this.#camelToSnake(key)}: ${this.toParser(key, value, new_keypath, indentation + 1)}`).join(',\n')}\n${' '.repeat((indentation + 1) * 2)}}`; } break; case 'misc': switch (inference_type.misc_type) { case 'Thumbnail': - parser = `Thumbnail.fromResponse(data.${key})`; + parser = `Thumbnail.fromResponse(${key_path.join('.')}.${key})`; break; case 'Author': { - const author_parser = `new Author(data.${inference_type.params[0]}, ${inference_type.params[1] ? `data.${inference_type.params[1]}` : 'undefined'})`; + const author_parser = `new Author(${key_path.join('.')}.${inference_type.params[0]}, ${inference_type.params[1] ? `${key_path.join('.')}.${inference_type.params[1]}` : 'undefined'})`; if (inference_type.optional) - return `Reflect.has(data, '${inference_type.params[0]}') ? ${author_parser} : undefined`; + return `Reflect.has(${key_path.join('.')}, '${inference_type.params[0]}') ? ${author_parser} : undefined`; return author_parser; } default: - parser = `new ${inference_type.misc_type}(data.${key})`; + parser = `new ${inference_type.misc_type}(${key_path.join('.')}.${key})`; break; } if (parser === 'undefined') @@ -354,42 +381,70 @@ export class YTNodeGenerator { break; case 'primative': case 'unknown': - parser = `data.${key}`; + parser = `${key_path.join('.')}.${key}`; break; } if (inference_type.optional) - return `Reflect.has(data, '${key}') ? ${parser} : undefined`; + return `Reflect.has(${key_path.join('.')}, '${key}') ? ${parser} : undefined`; return parser; } - static parse(key: string, inference_type: InferenceType, data: any) { - const should_optional = !inference_type.optional || Reflect.has(data, key); + static #accessDataFromKeyPath(root: any, key_path: string[]) { + let data = root; + for (const key of key_path) + data = data[key]; + return data; + } + static #hasDataFromKeyPath(root: any, key_path: string[]) { + let data = root; + for (const key of key_path) + if (!Reflect.has(data, key)) + return false; + else + data = data[key]; + return true; + } + static parse(key: string, inference_type: InferenceType, data: any, key_path: string[] = [ 'data' ]) { + const should_optional = !inference_type.optional || this.#hasDataFromKeyPath({data}, [ ...key_path, key ]); switch (inference_type.type) { case 'renderer': { - return should_optional ? Parser.parseItem(data[key], inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined; + return should_optional ? Parser.parseItem(this.#accessDataFromKeyPath({data}, [ ...key_path, key ]), inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined; } case 'renderer_list': { - return should_optional ? Parser.parse(data[key], true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined; + return should_optional ? Parser.parse(this.#accessDataFromKeyPath({data}, [ ...key_path, key ]), true, inference_type.renderers.map((type) => Parser.getParserByName(type))) : undefined; + } + case 'object': + { + const obj: any = {}; + const new_key_path = [ ...key_path, key ]; + for (const [ key, value ] of inference_type.keys) { + obj[key] = should_optional ? this.parse(key, value, data, new_key_path) : undefined; + } + return obj; } case 'misc': switch (inference_type.misc_type) { case 'NavigationEndpoint': - return should_optional ? new NavigationEndpoint(data[key]) : undefined; + return should_optional ? new NavigationEndpoint(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined; case 'Text': - return should_optional ? new Text(data[key]) : undefined; + return should_optional ? new Text(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined; case 'Thumbnail': - return should_optional ? Thumbnail.fromResponse(data[key]) : undefined; + return should_optional ? Thumbnail.fromResponse(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined; case 'Author': { - const author_should_optional = !inference_type.optional || Reflect.has(data, inference_type.params[0]); - return author_should_optional ? new Author(data[inference_type.params[0]], inference_type.params[1] ? data[inference_type.params[1]] : undefined) : undefined; + const author_should_optional = !inference_type.optional || this.#hasDataFromKeyPath({data}, [ ...key_path, inference_type.params[0] ]); + return author_should_optional ? new Author( + this.#accessDataFromKeyPath({data}, [ ...key_path, inference_type.params[0] ]), + inference_type.params[1] ? + this.#accessDataFromKeyPath({data}, [ ...key_path, inference_type.params[1] ]) : undefined + ) : undefined; } } throw new Error('Unreachable code reached! Switch missing case!'); case 'primative': case 'unknown': - return data[key]; + return this.#accessDataFromKeyPath({data}, [ ...key_path, key ]); } } static #passOne(classdata: any) { @@ -489,9 +544,16 @@ export class YTNodeGenerator { if (return_value = this.isMiscType(key, value)) { return return_value as MiscInferenceType; } + const primative_type = typeof value; + if (primative_type === 'object') + return { + type: 'object', + keys: Object.entries(value).map(([ key, value ]) => [ key, this.inferType(key, value) ]), + optional: false + }; return { type: 'primative', - typeof: [ typeof value ], + typeof: [ primative_type ], optional: false }; } From 24ee1a1963acb4311ff7990e94fbb97c57cfc935 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Wed, 8 Mar 2023 19:04:19 +0200 Subject: [PATCH 12/17] fix: object type def --- src/parser/generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/generator.ts b/src/parser/generator.ts index e7f777d98..a690ccdbf 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -325,7 +325,7 @@ export class YTNodeGenerator { } case 'object': { - return `{\n${inference_type.keys.map(([ key, value ]) => `${' '.repeat((indentation + 2) * 2)}${key}${value.optional ? '?' : ''}: ${this.toTypeDeclaration(value, indentation + 1)}`).join(',\n')}\n${' '.repeat((indentation + 1) * 2)}}`; + return `{\n${inference_type.keys.map(([ key, value ]) => `${' '.repeat((indentation + 2) * 2)}${this.#camelToSnake(key)}${value.optional ? '?' : ''}: ${this.toTypeDeclaration(value, indentation + 1)}`).join(',\n')}\n${' '.repeat((indentation + 1) * 2)}}`; } case 'misc': switch (inference_type.misc_type) { From 72129bffefc74c3eabd3133c74b9af406523eb0c Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Thu, 9 Mar 2023 07:32:34 +0200 Subject: [PATCH 13/17] docs: basic YTNodeGenerator explanation --- src/parser/README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++ src/parser/index.ts | 1 + 2 files changed, 47 insertions(+) diff --git a/src/parser/README.md b/src/parser/README.md index 35b826824..cc42c2519 100644 --- a/src/parser/README.md +++ b/src/parser/README.md @@ -29,6 +29,7 @@ Structure:
  • Adding new nodes
  • +
  • Generating nodes at runtime
  • How it works
  • @@ -258,6 +259,51 @@ const videos = response.contents_memo.getType(Video); ## Adding new nodes Instructions can be found [here](https://github.com/LuanRT/YouTube.js/blob/main/docs/updating-the-parser.md). +## Generating nodes at runtime +YouTube constantly updates their client, and sometimes they add new nodes to the response. The parser needs to know about these new nodes in order to parse them correctly. Once a new node is dicovered by the parser, it will attempt to generate a new node class for it. + +Using our existing `YTNode` class, you may interact with these new nodes in a type-safe way, however will not be able to cast them to the node's specific type, as this requires the node to be defined at compile-time. + +The current implementation recognises the following values: +- Renderers +- Renderer arrays +- Text +- Navigation endpoints +- Author (does not currently detect the author thumbnails) +- Thumbnails +- Objects (key-value pairs) +- Primatives (string, number, boolean, etc.) + +This may be expanded in the future. + +At runtime, these JIT generated nodes will revalidate themselves when constructed, so that when the types changes, the node will be re-generated. + +You may also generate your own nodes ahead of time, given you have an example of one of the nodes. + +```ts +import { Generator } from "youtube.js"; + +// Provided you have an example of the node `Example` +const example_data = { + "title": { + "runs": [ + { + "text": "Example" + } + ] + } +} + +// The first argument is the name of the class, the second is the data you have for the node. +// It will return a class that extends YTNode. +const Example = Generator.YTNodeGenerator.generateRuntimeClass('Example', example_data); + +// You may now use this class as you would any other node. +const example = new Example(example_data); + +const title = example.key('title').instanceof(Text).toString(); +``` + ## How it works If you decompile a YouTube client and analyze it for a while you will notice that it has classes named `protos/youtube/api/innertube/MusicItemRenderer`, `protos/youtube/api/innertube/SectionListRenderer`, etc. diff --git a/src/parser/index.ts b/src/parser/index.ts index 1c5d23d6f..2b06e605a 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -7,5 +7,6 @@ export * as YT from './youtube/index.js'; export * as YTMusic from './ytmusic/index.js'; export * as YTKids from './ytkids/index.js'; export * as Helpers from './helpers.js'; +export * as Generator from './generator.js'; import Parser from './parser.js'; export default Parser; \ No newline at end of file From 1ee26e08ad7983980862cdd9862f33afe04e58d1 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Thu, 9 Mar 2023 08:20:20 +0200 Subject: [PATCH 14/17] docs: tsdoc for YTNodeGenerator --- src/parser/generator.ts | 79 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/parser/generator.ts b/src/parser/generator.ts index a690ccdbf..b08e8d550 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -66,9 +66,20 @@ export class YTNodeGenerator { static #logChangedKeys(classname: string, key_info: KeyInfo, changed_keys: KeyInfo) { console.warn(`${classname} changed!\nThe following keys where altered: ${changed_keys.map(([ key ]) => this.#camelToSnake(key)).join(', ')}\nThe class has changed to:\n${this.generateTypescriptClass(classname, key_info)}`); } + /** + * Is this key ignored by the parser? + * @param key - The key to check + * @returns Whether or not the key is ignored + */ static isIgnoredKey(key: string | symbol) { return typeof key === 'string' && this.#ignored_keys.has(key); } + /** + * Merges two sets of key info, resolving any conflicts + * @param key_info - The current key info + * @param new_key_info - The new key info + * @returns The merged key info + */ static mergeKeyInfo(key_info: KeyInfo, new_key_info: KeyInfo) { const changed_keys = new Map(); const current_keys = new Set(key_info.map(([ key ]) => key)); @@ -222,6 +233,12 @@ export class YTNodeGenerator { changed_keys: [ ...changed_keys.entries() ] }; } + /** + * Given a classname and its resolved key info, create a new class + * @param classname - The name of the class + * @param key_info - The resolved key info + * @returns Class based on the key info extending YTNode + */ static createRuntimeClass(classname: string, key_info: KeyInfo): YTNodeConstructor { this.#logNewClass(classname, key_info); const node = class extends YTNode { @@ -267,6 +284,11 @@ export class YTNodeGenerator { Object.defineProperty(node, 'name', { value: classname, writable: false }); return node; } + /** + * Introspect an example of a class in order to determine its key info and dependencies + * @param classdata - The example of the class + * @returns The key info and any unimplemented dependencies + */ static introspect(classdata: string) { const key_info = this.#introspect(classdata); const dependencies = new Map(); @@ -285,6 +307,12 @@ export class YTNodeGenerator { unimplemented_dependencies }; } + /** + * Given example data for a class, introspect, implement dependencies, and create a new class + * @param classname - The name of the class + * @param classdata - The example of the class + * @returns Class based on the example classdata extending YTNode + */ static generateRuntimeClass(classname: string, classdata: any) { const { key_info, @@ -299,6 +327,12 @@ export class YTNodeGenerator { return JITNode; } + /** + * Generate a typescript class based on the key info + * @param classname - The name of the class + * @param key_info - The key info, as returned by {@link YTNodeGenerator.introspect} + * @returns Typescript class file + */ static generateTypescriptClass(classname: string, key_info: KeyInfo) { const props: string[] = []; const constructor_lines = [ @@ -313,6 +347,12 @@ export class YTNodeGenerator { } 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`; } + /** + * For a given inference type, get the typescript type declaration + * @param inference_type - The inference type to get the declaration for + * @param indentation - The indentation level (used for objects) + * @returns Typescript type declaration + */ static toTypeDeclaration(inference_type: InferenceType, indentation = 0): string { switch (inference_type.type) { case 'renderer': @@ -341,6 +381,14 @@ export class YTNodeGenerator { return '/* TODO: determine correct type */ unknown'; } } + /** + * Generate statements to parse a given inference type + * @param key - The key to parse + * @param inference_type - The inference type to parse + * @param key_path - The path to the key (excluding the key itself) + * @param indentation - The indentation level (used for objects) + * @returns Statement to parse the given key + */ static toParser(key: string, inference_type: InferenceType, key_path: string[] = [ 'data' ], indentation = 1) { let parser = 'undefined'; switch (inference_type.type) { @@ -403,6 +451,14 @@ export class YTNodeGenerator { data = data[key]; return true; } + /** + * Parse a value from a given key path using the given inference type + * @param key - The key to parse + * @param inference_type - The inference type to parse + * @param data - The data to parse from + * @param key_path - The path to the key (excluding the key itself) + * @returns The parsed value + */ static parse(key: string, inference_type: InferenceType, data: any, key_path: string[] = [ 'data' ]) { const should_optional = !inference_type.optional || this.#hasDataFromKeyPath({data}, [ ...key_path, key ]); switch (inference_type.type) { @@ -521,6 +577,12 @@ export class YTNodeGenerator { const key_info = this.#passOne(classdata); return this.#passTwo(key_info); } + /** + * Infer the type of a key given its value + * @param key - The key to infer the type of + * @param value - The value of the key + * @returns The inferred type + */ static inferType(key: string, value: any): InferenceType { let return_value: string | Record | boolean | MiscInferenceType = false; if (return_value = this.isRenderer(value)) { @@ -557,6 +619,12 @@ export class YTNodeGenerator { optional: false }; } + /** + * Checks if the given value is a array of renderers + * @param value - The value to check + * @returns If it is a renderer list, return an object with keys being the classnames, and values being an example of that class. + * Otherwise, return false. + */ static isRendererList(value: any) { const arr = Array.isArray(value); const is_list = arr && value.every((item) => this.isRenderer(item)); @@ -569,6 +637,12 @@ export class YTNodeGenerator { false ); } + /** + * Check if the given value is a misc type. + * @param key - The key of the value + * @param value - The value to check + * @returns If it is a misc type, return the InferenceType. Otherwise, return false. + */ static isMiscType(key: string, value: any): MiscInferenceType | false { // NavigationEndpoint if ((key.endsWith('Endpoint') || key.endsWith('Command') || key === 'endpoint') && typeof value === 'object') { @@ -600,6 +674,11 @@ export class YTNodeGenerator { } return false; } + /** + * Check if the given value is a renderer + * @param value - The value to check + * @returns If it is a renderer, return the class name. Otherwise, return false. + */ static isRenderer(value: any) { const is_object = typeof value === 'object'; if (!is_object) return false; From f72449ffda53fc6100e562a2ebf620cb67c9df39 Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Thu, 9 Mar 2023 08:30:33 +0200 Subject: [PATCH 15/17] docs: update parser updating guide --- docs/updating-the-parser.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/updating-the-parser.md b/docs/updating-the-parser.md index 45a04ad89..c996b8372 100644 --- a/docs/updating-the-parser.md +++ b/docs/updating-the-parser.md @@ -5,16 +5,18 @@ YouTube is constantly changing, so it is not rare to see YouTube crawlers/scrape Our parser, on the other hand, was written so that it behaves similarly to an official client, parsing and mapping renderers (a.k.a YTNodes) dynamically without hard-coding their path in the response. This way, whenever a new renderer pops up (e.g; YouTube adds a new feature / minor UI changes) the library will print a warning similar to this: ``` -InnertubeError: SomeRenderer not found! +SomeRenderer not found! This is a bug, want to help us fix it? Follow the instructions at https://github.com/LuanRT/YouTube.js/blob/main/docs/updating-the-parser.md or report it at https://github.com/LuanRT/YouTube.js/issues! - at Parser.printError (...) - at Parser.parseItem (...) - at Parser.parseArray (...) { - info: { - // renderer data, can be used as a reference to implement the renderer parser - }, - date: 2022-05-22T22:16:06.831Z, - version: '2.2.3' +Introspected and JIT generated this class in the meantime: +class SomeRenderer extends YTNode { + static type = 'SomeRenderer'; + + // ... + + constructor(data: RawNode) { + super(); + // ... + } } ``` @@ -24,7 +26,7 @@ This warning **does not** throw an error. The parser itself will continue workin Thanks to the modularity of the parser, a renderer can be implemented by simply adding a new file anywhere in the [classes directory](../src/parser/classes)! -For example, say we found a new renderer named `verticalListRenderer`, to let the parser know it exists we would have to create a file with the following structure: +For example, say we found a new renderer named `verticalListRenderer`, to let the parser know it exists at compile-time we would have to create a file with the following structure: > `../classes/VerticalList.ts` @@ -49,6 +51,8 @@ class VerticalList extends YTNode { export default VerticalList; ``` +You may use the parser's generated class for the new renderer as a starting point for your own implementation. + Then update the parser map: ```bash From 75c714ca6088a647c14035b7128283fdcb37eeff Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Mon, 13 Mar 2023 09:08:44 +0200 Subject: [PATCH 16/17] fix: apply suggested changes --- src/parser/README.md | 4 ++-- src/parser/generator.ts | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/parser/README.md b/src/parser/README.md index cc42c2519..b41b6be69 100644 --- a/src/parser/README.md +++ b/src/parser/README.md @@ -262,7 +262,7 @@ Instructions can be found [here](https://github.com/LuanRT/YouTube.js/blob/main/ ## Generating nodes at runtime YouTube constantly updates their client, and sometimes they add new nodes to the response. The parser needs to know about these new nodes in order to parse them correctly. Once a new node is dicovered by the parser, it will attempt to generate a new node class for it. -Using our existing `YTNode` class, you may interact with these new nodes in a type-safe way, however will not be able to cast them to the node's specific type, as this requires the node to be defined at compile-time. +Using the existing `YTNode` class, you may interact with these new nodes in a type-safe way. However, you will not be able to cast them to the node's specific type, as this requires the node to be defined at compile-time. The current implementation recognises the following values: - Renderers @@ -276,7 +276,7 @@ The current implementation recognises the following values: This may be expanded in the future. -At runtime, these JIT generated nodes will revalidate themselves when constructed, so that when the types changes, the node will be re-generated. +At runtime, these JIT-generated nodes will revalidate themselves when constructed so that when the types change, the node will be re-generated. You may also generate your own nodes ahead of time, given you have an example of one of the nodes. diff --git a/src/parser/generator.ts b/src/parser/generator.ts index b08e8d550..8156cb805 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -374,7 +374,6 @@ export class YTNodeGenerator { default: return inference_type.misc_type; } - throw new Error('Unreachable code reached! Switch missing case!'); case 'primative': return inference_type.typeof.join(' | '); case 'unknown': @@ -620,7 +619,7 @@ export class YTNodeGenerator { }; } /** - * Checks if the given value is a array of renderers + * Checks if the given value is an array of renderers * @param value - The value to check * @returns If it is a renderer list, return an object with keys being the classnames, and values being an example of that class. * Otherwise, return false. From 6199f903c429d0fed596474fc5c9749f72bc433d Mon Sep 17 00:00:00 2001 From: Daniel Wykerd Date: Mon, 13 Mar 2023 10:25:19 +0200 Subject: [PATCH 17/17] docs: accessing generated nodes --- src/parser/README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/parser/README.md b/src/parser/README.md index b41b6be69..ee72d51ee 100644 --- a/src/parser/README.md +++ b/src/parser/README.md @@ -278,10 +278,24 @@ This may be expanded in the future. At runtime, these JIT-generated nodes will revalidate themselves when constructed so that when the types change, the node will be re-generated. +To access these nodes that have been generated at runtime, you may use the `Parser.getParserByName(name: string)` method. You may also check if a parser has been generated for a node by using the `Parser.hasParser(name: string)` method. + +```ts +import { Parser } from "youtubei.js"; + +// We may check if we have a parser for a node. +if (Parser.hasParser('Example')) { + // Then retrieve it. + const Example = Parser.getParserByName('Example'); + // We may then use the parser as normal. + const example = new Example(data); +} +``` + You may also generate your own nodes ahead of time, given you have an example of one of the nodes. ```ts -import { Generator } from "youtube.js"; +import { Generator } from "youtubei.js"; // Provided you have an example of the node `Example` const example_data = {