diff --git a/src/parser/classes/CarouselLockup.ts b/src/parser/classes/CarouselLockup.ts index 781cf31c..c1a794ef 100644 --- a/src/parser/classes/CarouselLockup.ts +++ b/src/parser/classes/CarouselLockup.ts @@ -7,14 +7,11 @@ export default class CarouselLockup extends YTNode { static type = 'CarouselLockup'; info_rows: ObservedArray; - video_lockup?: CompactVideo; + video_lockup?: CompactVideo | null; constructor(data: RawNode) { super(); this.info_rows = Parser.parseArray(data.infoRows, InfoRow); - const video_lockup = Parser.parseItem(data.videoLockup, CompactVideo); - if (video_lockup != null) { - this.video_lockup = video_lockup; - } + this.video_lockup = Parser.parseItem(data.videoLockup, CompactVideo); } } diff --git a/src/parser/classes/EngagementPanelSectionList.ts b/src/parser/classes/EngagementPanelSectionList.ts index fcdb7656..be3e1199 100644 --- a/src/parser/classes/EngagementPanelSectionList.ts +++ b/src/parser/classes/EngagementPanelSectionList.ts @@ -1,20 +1,25 @@ import { YTNode } from '../helpers.js'; import Parser, { type RawNode } from '../index.js'; import ContinuationItem from './ContinuationItem.js'; +import EngagementPanelTitleHeader from './EngagementPanelTitleHeader.js'; import SectionList from './SectionList.js'; import StructuredDescriptionContent from './StructuredDescriptionContent.js'; export default class EngagementPanelSectionList extends YTNode { static type = 'EngagementPanelSectionList'; - target_id: String; - content?: SectionList|ContinuationItem|StructuredDescriptionContent; + header: EngagementPanelTitleHeader | null; + content: SectionList | ContinuationItem | StructuredDescriptionContent | null; + target_id?: string; + panel_identifier?: string; + visibility?: string; + constructor(data: RawNode) { super(); + this.header = Parser.parseItem(data.header, EngagementPanelTitleHeader); + this.content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent ]); + this.panel_identifier = data.panelIdentifier; this.target_id = data.targetId; - const content = Parser.parseItem(data.content, [ SectionList, ContinuationItem, StructuredDescriptionContent ]); - if (content !== null) { - this.content = content; - } + this.visibility = data.visibility; } } diff --git a/src/parser/classes/EngagementPanelTitleHeader.ts b/src/parser/classes/EngagementPanelTitleHeader.ts new file mode 100644 index 00000000..6c6b0d7e --- /dev/null +++ b/src/parser/classes/EngagementPanelTitleHeader.ts @@ -0,0 +1,17 @@ +import { YTNode } from '../helpers.js'; +import { Parser, type RawNode } from '../index.js'; +import Text from './misc/Text.js'; +import Button from './Button.js'; + +export default class EngagementPanelTitleHeader extends YTNode { + static type = 'EngagementPanelTitleHeader'; + + title: Text; + visibility_button: Button | null; + + constructor(data: RawNode) { + super(); + this.title = new Text(data.title); + this.visibility_button = Parser.parseItem(data.visibilityButton, Button); + } +} \ No newline at end of file diff --git a/src/parser/classes/ExpandableVideoDescriptionBody.ts b/src/parser/classes/ExpandableVideoDescriptionBody.ts index e83b9aa0..aef441ea 100644 --- a/src/parser/classes/ExpandableVideoDescriptionBody.ts +++ b/src/parser/classes/ExpandableVideoDescriptionBody.ts @@ -1,22 +1,22 @@ import { YTNode } from '../helpers.js'; -import { type RawNode } from '../index.js'; import { Text } from '../misc.js'; +import type { RawNode } from '../index.js'; + export default class ExpandableVideoDescriptionBody extends YTNode { static type = 'ExpandableVideoDescriptionBody'; show_more_text: Text; show_less_text: Text; - attributed_description_body_text: { - content: String - }; + attributed_description_body_text?: string; constructor(data: RawNode) { super(); this.show_more_text = new Text(data.showMoreText); this.show_less_text = new Text(data.showLessText); - this.attributed_description_body_text = { - content: data.attributedDescriptionBodyText.content - }; + + if (Reflect.has(data, 'attributedDescriptionBodyText')) { + this.attributed_description_body_text = data.attributedDescriptionBodyText?.content; + } } } diff --git a/src/parser/classes/Factoid.ts b/src/parser/classes/Factoid.ts index 7b73ac55..ef069340 100644 --- a/src/parser/classes/Factoid.ts +++ b/src/parser/classes/Factoid.ts @@ -4,6 +4,7 @@ import { Text } from '../misc.js'; export default class Factoid extends YTNode { static type = 'Factoid'; + label: Text; value: Text; accessibility_text: String; diff --git a/src/parser/classes/InfoRow.ts b/src/parser/classes/InfoRow.ts index c81f971d..a5f32b64 100644 --- a/src/parser/classes/InfoRow.ts +++ b/src/parser/classes/InfoRow.ts @@ -1,38 +1,29 @@ import { YTNode } from '../helpers.js'; -import Parser, { type RawNode } from '../index.js'; import { Text } from '../misc.js'; -import NavigationEndpoint from './NavigationEndpoint.js'; +import type { RawNode } from '../index.js'; export default class InfoRow extends YTNode { static type = 'InfoRow'; - metadata_text?: Text; - metadata_endpoint?: NavigationEndpoint; - info_row_expand_status_key: String; + title: Text; + default_metadata?: Text; + expanded_metadata?: Text; + info_row_expand_status_key?: String; constructor(data: RawNode) { super(); - if ('defaultMetadata' in data && 'runs' in data.defaultMetadata) { - const runs = data.defaultMetadata.runs; - if (runs.length > 0) { - const run = runs[0]; - this.metadata_text = run?.text; - if ('navigationEndpoint' in run) { - this.metadata_endpoint = Parser.parseItem({ navigationEndpoint: run.navigationEndpoint }, NavigationEndpoint) || undefined; - } - } - } - if ('expandedMetadata' in data && 'runs' in data.expandedMetadata) { - this.metadata_text = new Text(data.expandedMetadata); - } - if (this.metadata_text === undefined) { - this.metadata_text = data.expandedMetadata?.simpleText - ? new Text(data.expandedMetadata) - : data.defaultMetadata?.simpleText - ? new Text(data.defaultMetadata) - : undefined; - } - this.info_row_expand_status_key = data.infoRowExpandStatusKey; this.title = new Text(data.title); + + if (Reflect.has(data, 'defaultMetadata')) { + this.default_metadata = new Text(data.defaultMetadata); + } + + if (Reflect.has(data, 'expandedMetadata')) { + this.expanded_metadata = new Text(data.expandedMetadata); + } + + if (Reflect.has(data, 'infoRowExpandStatusKey')) { + this.info_row_expand_status_key = data.infoRowExpandStatusKey; + } } -} +} \ No newline at end of file diff --git a/src/parser/classes/StructuredDescriptionContent.ts b/src/parser/classes/StructuredDescriptionContent.ts index 0b1b391b..9626993e 100644 --- a/src/parser/classes/StructuredDescriptionContent.ts +++ b/src/parser/classes/StructuredDescriptionContent.ts @@ -1,16 +1,17 @@ -import { type ObservedArray, YTNode } from '../helpers.js'; +import { YTNode, type ObservedArray } from '../helpers.js'; import Parser, { type RawNode } from '../index.js'; import ExpandableVideoDescriptionBody from './ExpandableVideoDescriptionBody.js'; import VideoDescriptionHeader from './VideoDescriptionHeader.js'; +import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.js'; import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js'; export default class StructuredDescriptionContent extends YTNode { static type = 'StructuredDescriptionContent'; - items: ObservedArray; + items: ObservedArray; constructor(data: RawNode) { super(); - this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection ]); + this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection ]); } -} +} \ No newline at end of file diff --git a/src/parser/classes/VideoDescriptionHeader.ts b/src/parser/classes/VideoDescriptionHeader.ts index 5bb341e5..1a94c2f6 100644 --- a/src/parser/classes/VideoDescriptionHeader.ts +++ b/src/parser/classes/VideoDescriptionHeader.ts @@ -1,7 +1,6 @@ -import { type ObservedArray, YTNode } from '../helpers.js'; +import { YTNode, type ObservedArray } from '../helpers.js'; import Parser, { type RawNode } from '../index.js'; -import { Text } from '../misc.js'; -import type { Thumbnail } from '../misc.js'; +import { Text, Thumbnail } from '../misc.js'; import Factoid from './Factoid.js'; import NavigationEndpoint from './NavigationEndpoint.js'; @@ -10,7 +9,7 @@ export default class VideoDescriptionHeader extends YTNode { channel: Text; channel_navigation_endpoint?: NavigationEndpoint; - channel_thumbnails: String[]; + channel_thumbnail: Thumbnail[]; factoids: ObservedArray; publish_date: Text; title: Text; @@ -20,8 +19,8 @@ export default class VideoDescriptionHeader extends YTNode { super(); this.title = new Text(data.title); this.channel = new Text(data.channel); - this.channel_navigation_endpoint = Parser.parseItem(data.channelNavigationEndpoint, NavigationEndpoint) || undefined; - this.channel_thumbnails = data.channelThumbnail.thumbnails.map((thumbnail: Thumbnail) => thumbnail.url); + this.channel_navigation_endpoint = new NavigationEndpoint(data.channelNavigationEndpoint); + this.channel_thumbnail = Thumbnail.fromResponse(data.channelThumbnail); this.publish_date = new Text(data.publishDate); this.views = new Text(data.views); this.factoids = Parser.parseArray(data.factoid, Factoid); diff --git a/src/parser/classes/VideoDescriptionInfocardsSection.ts b/src/parser/classes/VideoDescriptionInfocardsSection.ts new file mode 100644 index 00000000..88aaaed3 --- /dev/null +++ b/src/parser/classes/VideoDescriptionInfocardsSection.ts @@ -0,0 +1,28 @@ +import Parser, { type RawNode } from '../index.js'; + +import { YTNode } from '../helpers.js'; +import Text from './misc/Text.js'; +import Thumbnail from './misc/Thumbnail.js'; +import Button from './Button.js'; +import NavigationEndpoint from './NavigationEndpoint.js'; + +export default class VideoDescriptionInfocardsSection extends YTNode { + static type = 'VideoDescriptionInfocardsSection'; + + section_title: Text; + creator_videos_button: Button | null; + creator_about_button: Button | null; + section_subtitle: Text; + channel_avatar: Thumbnail[]; + channel_endpoint: NavigationEndpoint; + + constructor(data: RawNode) { + super(); + this.section_title = new Text(data.sectionTitle); + this.creator_videos_button = Parser.parseItem(data.creatorVideosButton, Button); + this.creator_about_button = Parser.parseItem(data.creatorAboutButton, Button); + this.section_subtitle = new Text(data.sectionSubtitle); + this.channel_avatar = Thumbnail.fromResponse(data.channelAvatar); + this.channel_endpoint = new NavigationEndpoint(data.channelEndpoint); + } +} \ No newline at end of file diff --git a/src/parser/classes/VideoDescriptionMusicSection.ts b/src/parser/classes/VideoDescriptionMusicSection.ts index 681cf2c2..10af8657 100644 --- a/src/parser/classes/VideoDescriptionMusicSection.ts +++ b/src/parser/classes/VideoDescriptionMusicSection.ts @@ -1,13 +1,14 @@ -import { type ObservedArray, YTNode } from '../helpers.js'; -import CarouselLockup from './CarouselLockup.js'; +import { YTNode, type ObservedArray } from '../helpers.js'; import Parser, { type RawNode } from '../index.js'; import { Text } from '../misc.js'; +import CarouselLockup from './CarouselLockup.js'; export default class VideoDescriptionMusicSection extends YTNode { static type = 'VideoDescriptionMusicSection'; carousel_lockups: ObservedArray; section_title: Text; + constructor(data: RawNode) { super(); this.carousel_lockups = Parser.parseArray(data.carouselLockups, CarouselLockup); diff --git a/src/parser/generator.ts b/src/parser/generator.ts index 3dcf81dc..dab2d457 100644 --- a/src/parser/generator.ts +++ b/src/parser/generator.ts @@ -1,5 +1,5 @@ /* eslint-disable no-cond-assign */ -import { Platform } from '../utils/Utils.js'; +import { InnertubeError, 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'; @@ -62,7 +62,9 @@ export class YTNodeGenerator { 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)}`); + console.warn( + new InnertubeError(`${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)}`); @@ -460,15 +462,15 @@ export class YTNodeGenerator { * @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 ]); + const should_optional = !inference_type.optional || this.#hasDataFromKeyPath({ data }, [ ...key_path, key ]); switch (inference_type.type) { case 'renderer': { - return should_optional ? Parser.parseItem(this.#accessDataFromKeyPath({data}, [ ...key_path, 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(this.#accessDataFromKeyPath({data}, [ ...key_path, 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': { @@ -482,25 +484,25 @@ export class YTNodeGenerator { case 'misc': switch (inference_type.misc_type) { case 'NavigationEndpoint': - return should_optional ? new NavigationEndpoint(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined; + return should_optional ? new NavigationEndpoint(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined; case 'Text': - return should_optional ? new Text(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined; + return should_optional ? new Text(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined; case 'Thumbnail': - return should_optional ? Thumbnail.fromResponse(this.#accessDataFromKeyPath({data}, [ ...key_path, key ])) : undefined; + return should_optional ? Thumbnail.fromResponse(this.#accessDataFromKeyPath({ data }, [ ...key_path, key ])) : undefined; case 'Author': { - const author_should_optional = !inference_type.optional || this.#hasDataFromKeyPath({data}, [ ...key_path, inference_type.params[0] ]); + 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] ]), + this.#accessDataFromKeyPath({ data }, [ ...key_path, inference_type.params[0] ]), inference_type.params[1] ? - this.#accessDataFromKeyPath({data}, [ ...key_path, inference_type.params[1] ]) : undefined + 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 this.#accessDataFromKeyPath({data}, [ ...key_path, key ]); + return this.#accessDataFromKeyPath({ data }, [ ...key_path, key ]); } } static #passOne(classdata: any) { diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index 3cf68240..56e1bc1d 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -90,6 +90,7 @@ 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 EngagementPanelSectionList } from './classes/EngagementPanelSectionList.js'; +export { default as EngagementPanelTitleHeader } from './classes/EngagementPanelTitleHeader.js'; export { default as ExpandableMetadata } from './classes/ExpandableMetadata.js'; export { default as ExpandableTab } from './classes/ExpandableTab.js'; export { default as ExpandableVideoDescriptionBody } from './classes/ExpandableVideoDescriptionBody.js'; @@ -342,6 +343,7 @@ export { default as VerticalWatchCardList } from './classes/VerticalWatchCardLis export { default as Video } from './classes/Video.js'; export { default as VideoCard } from './classes/VideoCard.js'; export { default as VideoDescriptionHeader } from './classes/VideoDescriptionHeader.js'; +export { default as VideoDescriptionInfocardsSection } from './classes/VideoDescriptionInfocardsSection.js'; export { default as VideoDescriptionMusicSection } from './classes/VideoDescriptionMusicSection.js'; export { default as VideoInfoCardContent } from './classes/VideoInfoCardContent.js'; export { default as VideoOwner } from './classes/VideoOwner.js'; diff --git a/src/parser/youtube/VideoInfo.ts b/src/parser/youtube/VideoInfo.ts index 724bbaf7..94132ee0 100644 --- a/src/parser/youtube/VideoInfo.ts +++ b/src/parser/youtube/VideoInfo.ts @@ -339,36 +339,45 @@ class VideoInfo extends MediaInfo { * Get songs used in the video. */ get music_tracks() { + // @TODO: Refactor this. const description_content = this.page[1]?.engagement_panels?.filter((panel) => panel.content?.is(StructuredDescriptionContent)); if (description_content !== undefined && description_content.length > 0) { - const music_section = (description_content[0].content as StructuredDescriptionContent)?.items.filter((item: YTNode) => item?.is(VideoDescriptionMusicSection)) as VideoDescriptionMusicSection[]; + const music_section = description_content[0].content?.as(StructuredDescriptionContent)?.items?.filterType(VideoDescriptionMusicSection); if (music_section !== undefined && music_section.length > 0) { return music_section[0].carousel_lockups?.map((lookup) => { - let song, artist, album, license, videoId, channelId; + let song: string | undefined; + let artist: string | undefined; + let album: string | undefined; + let license: string | undefined; + let videoId: string | undefined; + let channelId: string | undefined; + // If the song isn't in the video_lockup, it should be in the info_rows - song = lookup.video_lockup?.title; + song = lookup.video_lockup?.title?.toString(); // If the video id isn't in the video_lockup, it should be in the info_rows videoId = lookup.video_lockup?.endpoint.payload.videoId; for (let i = 0; i < lookup.info_rows.length; i++) { const info_row = lookup.info_rows[i]; if (info_row.info_row_expand_status_key === undefined) { if (song === undefined) { - song = info_row.metadata_text; + song = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString(); if (videoId === undefined) { - videoId = info_row.metadata_endpoint?.payload; + const endpoint = info_row.default_metadata?.endpoint || info_row.expanded_metadata?.endpoint; + videoId = endpoint?.payload?.videoId; } } else { - album = info_row.metadata_text; + album = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString(); } } else { if (info_row.info_row_expand_status_key?.indexOf('structured-description-music-section-artists-row-state-id') !== -1) { - artist = info_row.metadata_text; + artist = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString(); if (channelId === undefined) { - channelId = info_row.metadata_endpoint?.payload?.browseId; + const endpoint = info_row.default_metadata?.endpoint || info_row.expanded_metadata?.endpoint; + channelId = endpoint?.payload?.browseId; } } if (info_row.info_row_expand_status_key?.indexOf('structured-description-music-section-licenses-row-state-id') !== -1) { - license = info_row.metadata_text; + license = info_row.default_metadata?.toString() || info_row.expanded_metadata?.toString(); } } }