From 593573da423768c5734ba36713640debe6ded407 Mon Sep 17 00:00:00 2001 From: rsandbach Date: Sat, 16 Sep 2023 13:38:18 -0400 Subject: [PATCH 1/7] initial commit --- src/core/clients/Music.ts | 21 +++++++++++++++++++++ src/parser/ytmusic/Profile.ts | 28 ++++++++++++++++++++++++++++ test/main.test.ts | 7 +++++++ 3 files changed, 56 insertions(+) create mode 100644 src/parser/ytmusic/Profile.ts diff --git a/src/core/clients/Music.ts b/src/core/clients/Music.ts index b9e7fbeb3..874378425 100644 --- a/src/core/clients/Music.ts +++ b/src/core/clients/Music.ts @@ -1,5 +1,6 @@ import Album from '../../parser/ytmusic/Album.js'; import Artist from '../../parser/ytmusic/Artist.js'; +import Profile from '../../parser/ytmusic/Profile.js'; import Explore from '../../parser/ytmusic/Explore.js'; import HomeFeed from '../../parser/ytmusic/HomeFeed.js'; import Library from '../../parser/ytmusic/Library.js'; @@ -368,4 +369,24 @@ export default class Music { return search_suggestions_section.contents; } + + /** + * Retrieves artist's profile. + * @param artist_id - The artist id. + */ + async getProfile(artist_id: string): Promise { + throwIfMissing({ artist_id }); + + if (!artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist')) + throw new InnertubeError('Invalid artist id', artist_id); + + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + client: 'YTMUSIC', + browse_id: artist_id + }) + ); + + return new Profile(response, this.#actions); + } } \ No newline at end of file diff --git a/src/parser/ytmusic/Profile.ts b/src/parser/ytmusic/Profile.ts new file mode 100644 index 000000000..5fafe1342 --- /dev/null +++ b/src/parser/ytmusic/Profile.ts @@ -0,0 +1,28 @@ +import Parser from '../index.js'; +import type Actions from '../../core/Actions.js'; +import type { ApiResponse } from '../../core/Actions.js'; +//import { InnertubeError } from '../../utils/Utils.js'; +import type { IBrowseResponse } from '../types/ParsedResponse.js'; + +class Profile { + #page: IBrowseResponse; + #actions: Actions; + + constructor(response: ApiResponse, actions: Actions) { + this.#page = Parser.parseResponse(response.data); + this.#actions = actions; + + //this.header = this.page.header?.item().as(MusicImmersiveHeader, MusicVisualHeader, MusicHeader); + + //const music_shelf = this.#page.contents_memo?.getType(MusicShelf) || []; + //const music_carousel_shelf = this.#page.contents_memo?.getType(MusicCarouselShelf) || []; + + //this.sections = [ ...music_shelf, ...music_carousel_shelf ]; + } + + get page(): IBrowseResponse { + return this.#page; + } +} + +export default Profile; \ No newline at end of file diff --git a/test/main.test.ts b/test/main.test.ts index 3f78eb645..b7031522d 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -344,6 +344,13 @@ describe('YouTube.js Tests', () => { expect(artist.sections).toBeDefined(); }); + test('Innertube#music.getProfile', async () => { + const profile = await innertube.music.getProfile('UC52ZqHVQz5OoGhvbWiRal6g'); + expect(profile).toBeDefined(); + expect(profile.header).toBeDefined(); + expect(profile.sections).toBeDefined(); + }); + test('Innertube#music.getAlbum', async () => { const album = await innertube.music.getAlbum('MPREb_YpQ7SWMPLvu'); expect(album).toBeDefined(); From 3f0a41f8615aaef548d1121eabca8934d547e2da Mon Sep 17 00:00:00 2001 From: rsandbach Date: Sun, 17 Sep 2023 04:13:39 -0400 Subject: [PATCH 2/7] first pass at #207 --- examples/music/index.js | 25 ++++++++ src/core/clients/Music.ts | 19 ++---- .../classes/menus/MultiPageMenuSection.ts | 16 +++++ .../classes/ytmusic/ActiveAccountHeader.ts | 22 +++++++ src/parser/nodes.ts | 2 + src/parser/ytmusic/Account.ts | 58 +++++++++++++++++++ src/parser/ytmusic/Profile.ts | 28 --------- 7 files changed, 129 insertions(+), 41 deletions(-) create mode 100644 examples/music/index.js create mode 100644 src/parser/classes/menus/MultiPageMenuSection.ts create mode 100644 src/parser/classes/ytmusic/ActiveAccountHeader.ts create mode 100644 src/parser/ytmusic/Account.ts delete mode 100644 src/parser/ytmusic/Profile.ts diff --git a/examples/music/index.js b/examples/music/index.js new file mode 100644 index 000000000..89cb20d43 --- /dev/null +++ b/examples/music/index.js @@ -0,0 +1,25 @@ +import { Innertube, UniversalCache } from 'youtubei.js'; + +(async () => { + const yt = await Innertube.create({ cache: new UniversalCache(true, '~/ytjs-credcache') }); + + yt.session.on('auth-pending', (data) => { + console.log(`Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`); + }); + yt.session.on('auth', async () => { + console.log('Sign in successful'); + await yt.session.oauth.cacheCredentials(); + }); + yt.session.on('update-credentials', async () => { + await yt.session.oauth.cacheCredentials(); + }); + + // Attempt to sign in + await yt.session.signIn(); + const account = await yt.music.getAccount(); + + console.log(account.account_name, account.channel_handle, account.channel_id); + + const profile = await account.getProfile(); + console.debug(profile); +})(); \ No newline at end of file diff --git a/src/core/clients/Music.ts b/src/core/clients/Music.ts index 874378425..46d5e0f28 100644 --- a/src/core/clients/Music.ts +++ b/src/core/clients/Music.ts @@ -1,6 +1,5 @@ import Album from '../../parser/ytmusic/Album.js'; import Artist from '../../parser/ytmusic/Artist.js'; -import Profile from '../../parser/ytmusic/Profile.js'; import Explore from '../../parser/ytmusic/Explore.js'; import HomeFeed from '../../parser/ytmusic/HomeFeed.js'; import Library from '../../parser/ytmusic/Library.js'; @@ -35,6 +34,7 @@ import { } from '../endpoints/index.js'; import { GetSearchSuggestionsEndpoint } from '../endpoints/music/index.js'; +import Account from '../../parser/ytmusic/Account.js'; export default class Music { #session: Session; @@ -374,19 +374,12 @@ export default class Music { * Retrieves artist's profile. * @param artist_id - The artist id. */ - async getProfile(artist_id: string): Promise { - throwIfMissing({ artist_id }); + async getAccount(): Promise { + if (!this.#session.logged_in) + throw new InnertubeError('You must be signed in to perform this operation.'); - if (!artist_id.startsWith('UC') && !artist_id.startsWith('FEmusic_library_privately_owned_artist')) - throw new InnertubeError('Invalid artist id', artist_id); - - const response = await this.#actions.execute( - BrowseEndpoint.PATH, BrowseEndpoint.build({ - client: 'YTMUSIC', - browse_id: artist_id - }) - ); + const response = await this.#actions.execute('account/account_menu', { client: 'YTMUSIC' }); - return new Profile(response, this.#actions); + return new Account(response, this.#actions); } } \ No newline at end of file diff --git a/src/parser/classes/menus/MultiPageMenuSection.ts b/src/parser/classes/menus/MultiPageMenuSection.ts new file mode 100644 index 000000000..f7c10ff34 --- /dev/null +++ b/src/parser/classes/menus/MultiPageMenuSection.ts @@ -0,0 +1,16 @@ +import Parser from '../../index.js'; +import { YTNode } from '../../helpers.js'; +import type { RawNode } from '../../index.js'; +import { YTNodes } from '../../index.js'; +import type { ObservedArray } from '../../helpers.js'; + +export default class MultiPageMenuSection extends YTNode { + static type = 'MultiPageMenuSection'; + + items: ObservedArray | null; + + constructor(data: RawNode) { + super(); + this.items = Parser.parse(data.items, true, [ YTNodes.CompactLink ]); + } +} \ No newline at end of file diff --git a/src/parser/classes/ytmusic/ActiveAccountHeader.ts b/src/parser/classes/ytmusic/ActiveAccountHeader.ts new file mode 100644 index 000000000..e18ef5240 --- /dev/null +++ b/src/parser/classes/ytmusic/ActiveAccountHeader.ts @@ -0,0 +1,22 @@ +import type { RawNode } from '../../../platform/lib.js'; +import { YTNode } from '../../helpers.js'; +import { Thumbnail } from '../../misc.js'; +import { NavigationEndpoint } from '../../nodes.js'; +import Text from '../misc/Text.js'; + +export default class ActiveAccountHeader extends YTNode { + static type = 'ActiveAccountHeader'; + + account_name: Text; + account_photo: Thumbnail[]; + endpoint: NavigationEndpoint; + channel_handle: Text; + + constructor(data: RawNode) { + super(); + this.account_name = new Text(data.accountName); + this.account_photo = Thumbnail.fromResponse(data.accountPhoto); + this.endpoint = new NavigationEndpoint(data.settingsEndpoint); + this.channel_handle = new Text(data.channelHandle); + } +} \ No newline at end of file diff --git a/src/parser/nodes.ts b/src/parser/nodes.ts index c1cc83314..fe14c762e 100644 --- a/src/parser/nodes.ts +++ b/src/parser/nodes.ts @@ -198,6 +198,7 @@ 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 MultiPageMenuSection } from './classes/menus/MultiPageMenuSection.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'; @@ -391,3 +392,4 @@ 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'; +export { default as ActiveAccountHeader } from './classes/ytmusic/ActiveAccountHeader.js'; diff --git a/src/parser/ytmusic/Account.ts b/src/parser/ytmusic/Account.ts new file mode 100644 index 000000000..936fd7a4f --- /dev/null +++ b/src/parser/ytmusic/Account.ts @@ -0,0 +1,58 @@ +import { BrowseEndpoint } from '../../core/endpoints/index.js'; +import type { Actions, ApiResponse, IParsedResponse } from '../../platform/lib.js'; +import { Parser } from '../../platform/lib.js'; +import type { Memo, ObservedArray } from '../helpers.js'; +import type { Thumbnail } from '../misc.js'; +import { ActiveAccountHeader, CompactLink } from '../nodes.js'; +import { Artist } from './index.js'; + +export default class Account { + #page: IParsedResponse; + #actions: Actions; + #memos: Memo | undefined; + #artist: Artist | undefined; + + account_name: string; + account_photo: Thumbnail[]; + channel_handle: string; + channel_id: string; + items: ObservedArray; + + constructor(response: ApiResponse, actions: Actions) { + this.#page = Parser.parseResponse(response.data); + this.#actions = actions; + this.#memos = this.#page.actions_memo; + + if (this.#memos === undefined) + throw new Error('Could not find actions'); + + const profile = this.#memos.getType(ActiveAccountHeader).first(); + + if (profile === undefined) + throw new Error('Could not find ActiveAccountHeader'); + + this.account_name = profile.account_name.toString(); + this.account_photo = profile.account_photo; + this.channel_handle = profile.channel_handle.toString(); + this.items = this.#memos.getType(CompactLink); + const your_channel = this.items.find((lnk) => lnk.title.toString() === 'Your channel') as CompactLink; + this.channel_id = your_channel.endpoint.payload.browseId; + } + + get page(): IParsedResponse { + return this.#page; + } + + async getProfile(): Promise { + if (this.#artist !== undefined) return this.#artist; + + const response = await this.#actions.execute( + BrowseEndpoint.PATH, BrowseEndpoint.build({ + client: 'YTMUSIC', + browse_id: this.channel_id + }) + ); + + return new Artist(response, this.#actions); + } +} \ No newline at end of file diff --git a/src/parser/ytmusic/Profile.ts b/src/parser/ytmusic/Profile.ts deleted file mode 100644 index 5fafe1342..000000000 --- a/src/parser/ytmusic/Profile.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Parser from '../index.js'; -import type Actions from '../../core/Actions.js'; -import type { ApiResponse } from '../../core/Actions.js'; -//import { InnertubeError } from '../../utils/Utils.js'; -import type { IBrowseResponse } from '../types/ParsedResponse.js'; - -class Profile { - #page: IBrowseResponse; - #actions: Actions; - - constructor(response: ApiResponse, actions: Actions) { - this.#page = Parser.parseResponse(response.data); - this.#actions = actions; - - //this.header = this.page.header?.item().as(MusicImmersiveHeader, MusicVisualHeader, MusicHeader); - - //const music_shelf = this.#page.contents_memo?.getType(MusicShelf) || []; - //const music_carousel_shelf = this.#page.contents_memo?.getType(MusicCarouselShelf) || []; - - //this.sections = [ ...music_shelf, ...music_carousel_shelf ]; - } - - get page(): IBrowseResponse { - return this.#page; - } -} - -export default Profile; \ No newline at end of file From 3a03de82b470c5c342dc4105e4a17751cf3c3b45 Mon Sep 17 00:00:00 2001 From: rsandbach Date: Sun, 17 Sep 2023 04:16:32 -0400 Subject: [PATCH 3/7] Revert test. --- test/main.test.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/test/main.test.ts b/test/main.test.ts index b7031522d..3f78eb645 100644 --- a/test/main.test.ts +++ b/test/main.test.ts @@ -344,13 +344,6 @@ describe('YouTube.js Tests', () => { expect(artist.sections).toBeDefined(); }); - test('Innertube#music.getProfile', async () => { - const profile = await innertube.music.getProfile('UC52ZqHVQz5OoGhvbWiRal6g'); - expect(profile).toBeDefined(); - expect(profile.header).toBeDefined(); - expect(profile.sections).toBeDefined(); - }); - test('Innertube#music.getAlbum', async () => { const album = await innertube.music.getAlbum('MPREb_YpQ7SWMPLvu'); expect(album).toBeDefined(); From ba643c23fab286f86f16cab0d476cb14673d6ef7 Mon Sep 17 00:00:00 2001 From: rsandbach Date: Mon, 6 Nov 2023 21:49:25 -0500 Subject: [PATCH 4/7] Code Review Updates --- src/core/clients/Music.ts | 2 +- src/parser/classes/menus/MultiPageMenuSection.ts | 8 ++++---- src/parser/classes/ytmusic/ActiveAccountHeader.ts | 6 +++--- src/parser/ytmusic/Account.ts | 11 +++++++---- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/core/clients/Music.ts b/src/core/clients/Music.ts index 46d5e0f28..cec718dfa 100644 --- a/src/core/clients/Music.ts +++ b/src/core/clients/Music.ts @@ -1,3 +1,4 @@ +import Account from '../../parser/ytmusic/Account.js'; import Album from '../../parser/ytmusic/Album.js'; import Artist from '../../parser/ytmusic/Artist.js'; import Explore from '../../parser/ytmusic/Explore.js'; @@ -34,7 +35,6 @@ import { } from '../endpoints/index.js'; import { GetSearchSuggestionsEndpoint } from '../endpoints/music/index.js'; -import Account from '../../parser/ytmusic/Account.js'; export default class Music { #session: Session; diff --git a/src/parser/classes/menus/MultiPageMenuSection.ts b/src/parser/classes/menus/MultiPageMenuSection.ts index f7c10ff34..98404d5a8 100644 --- a/src/parser/classes/menus/MultiPageMenuSection.ts +++ b/src/parser/classes/menus/MultiPageMenuSection.ts @@ -1,16 +1,16 @@ -import Parser from '../../index.js'; +import { Parser } from '../../index.js'; import { YTNode } from '../../helpers.js'; import type { RawNode } from '../../index.js'; -import { YTNodes } from '../../index.js'; +import CompactLink from '../CompactLink.js'; import type { ObservedArray } from '../../helpers.js'; export default class MultiPageMenuSection extends YTNode { static type = 'MultiPageMenuSection'; - items: ObservedArray | null; + items: ObservedArray | null; constructor(data: RawNode) { super(); - this.items = Parser.parse(data.items, true, [ YTNodes.CompactLink ]); + this.items = Parser.parse(data.items, true, [ CompactLink ]); } } \ No newline at end of file diff --git a/src/parser/classes/ytmusic/ActiveAccountHeader.ts b/src/parser/classes/ytmusic/ActiveAccountHeader.ts index e18ef5240..0da0e3c28 100644 --- a/src/parser/classes/ytmusic/ActiveAccountHeader.ts +++ b/src/parser/classes/ytmusic/ActiveAccountHeader.ts @@ -1,7 +1,7 @@ -import type { RawNode } from '../../../platform/lib.js'; +import type { RawNode } from '../../index.js'; import { YTNode } from '../../helpers.js'; -import { Thumbnail } from '../../misc.js'; -import { NavigationEndpoint } from '../../nodes.js'; +import Thumbnail from '../misc/Thumbnail.js'; +import NavigationEndpoint from '../NavigationEndpoint.js'; import Text from '../misc/Text.js'; export default class ActiveAccountHeader extends YTNode { diff --git a/src/parser/ytmusic/Account.ts b/src/parser/ytmusic/Account.ts index 936fd7a4f..93cad1131 100644 --- a/src/parser/ytmusic/Account.ts +++ b/src/parser/ytmusic/Account.ts @@ -1,10 +1,13 @@ import { BrowseEndpoint } from '../../core/endpoints/index.js'; -import type { Actions, ApiResponse, IParsedResponse } from '../../platform/lib.js'; +import type Actions from '../../core/Actions.js'; +import { ApiResponse } from '../../core/Actions.js'; +import type { IParsedResponse } from '../types/ParsedResponse.js'; import { Parser } from '../../platform/lib.js'; import type { Memo, ObservedArray } from '../helpers.js'; -import type { Thumbnail } from '../misc.js'; -import { ActiveAccountHeader, CompactLink } from '../nodes.js'; -import { Artist } from './index.js'; +import type Thumbnail from '../classes/misc/Thumbnail.js'; +import ActiveAccountHeader from '../classes/ytmusic/ActiveAccountHeader.js'; +import CompactLink from '../classes/CompactLink.js'; +import Artist from './Artist.js'; export default class Account { #page: IParsedResponse; From c7d93bc5695bf3232d17e36c9174734f9acb307f Mon Sep 17 00:00:00 2001 From: rsandbach Date: Mon, 6 Nov 2023 21:55:09 -0500 Subject: [PATCH 5/7] Fix lint errors. --- src/parser/ytmusic/Account.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/ytmusic/Account.ts b/src/parser/ytmusic/Account.ts index 93cad1131..d8f292c48 100644 --- a/src/parser/ytmusic/Account.ts +++ b/src/parser/ytmusic/Account.ts @@ -1,12 +1,12 @@ import { BrowseEndpoint } from '../../core/endpoints/index.js'; import type Actions from '../../core/Actions.js'; -import { ApiResponse } from '../../core/Actions.js'; +import type { ApiResponse } from '../../core/Actions.js'; import type { IParsedResponse } from '../types/ParsedResponse.js'; import { Parser } from '../../platform/lib.js'; import type { Memo, ObservedArray } from '../helpers.js'; import type Thumbnail from '../classes/misc/Thumbnail.js'; import ActiveAccountHeader from '../classes/ytmusic/ActiveAccountHeader.js'; -import CompactLink from '../classes/CompactLink.js'; +import CompactLink from '../classes/CompactLink.js'; import Artist from './Artist.js'; export default class Account { From ee79dfe73c98a1c1bff67a169a44909e8493a86a Mon Sep 17 00:00:00 2001 From: rsandbach Date: Mon, 6 Nov 2023 22:07:04 -0500 Subject: [PATCH 6/7] Fixed failing test. --- src/parser/classes/StructuredDescriptionContent.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/parser/classes/StructuredDescriptionContent.ts b/src/parser/classes/StructuredDescriptionContent.ts index 3936e0679..15ac7ffdd 100644 --- a/src/parser/classes/StructuredDescriptionContent.ts +++ b/src/parser/classes/StructuredDescriptionContent.ts @@ -1,11 +1,11 @@ import { YTNode, type ObservedArray } from '../helpers.js'; -import Parser, { type RawNode } from '../index.js'; +import { Parser, type RawNode } from '../index.js'; import ExpandableVideoDescriptionBody from './ExpandableVideoDescriptionBody.js'; import HorizontalCardList from './HorizontalCardList.js'; import VideoDescriptionHeader from './VideoDescriptionHeader.js'; import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.js'; import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js'; -import type VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.js'; +import VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.js'; export default class StructuredDescriptionContent extends YTNode { static type = 'StructuredDescriptionContent'; @@ -17,6 +17,7 @@ export default class StructuredDescriptionContent extends YTNode { constructor(data: RawNode) { super(); - this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection, HorizontalCardList ]); + this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection + , VideoDescriptionInfocardsSection, VideoDescriptionTranscriptSection, HorizontalCardList ]); } } \ No newline at end of file From 55497a0a05d00352dfddf5bb50d4e53e0260dfe9 Mon Sep 17 00:00:00 2001 From: rsandbach Date: Mon, 6 Nov 2023 22:21:16 -0500 Subject: [PATCH 7/7] Revert "Fixed failing test." This reverts commit ee79dfe73c98a1c1bff67a169a44909e8493a86a. --- src/parser/classes/StructuredDescriptionContent.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/parser/classes/StructuredDescriptionContent.ts b/src/parser/classes/StructuredDescriptionContent.ts index 15ac7ffdd..3936e0679 100644 --- a/src/parser/classes/StructuredDescriptionContent.ts +++ b/src/parser/classes/StructuredDescriptionContent.ts @@ -1,11 +1,11 @@ import { YTNode, type ObservedArray } from '../helpers.js'; -import { Parser, type RawNode } from '../index.js'; +import Parser, { type RawNode } from '../index.js'; import ExpandableVideoDescriptionBody from './ExpandableVideoDescriptionBody.js'; import HorizontalCardList from './HorizontalCardList.js'; import VideoDescriptionHeader from './VideoDescriptionHeader.js'; import VideoDescriptionInfocardsSection from './VideoDescriptionInfocardsSection.js'; import VideoDescriptionMusicSection from './VideoDescriptionMusicSection.js'; -import VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.js'; +import type VideoDescriptionTranscriptSection from './VideoDescriptionTranscriptSection.js'; export default class StructuredDescriptionContent extends YTNode { static type = 'StructuredDescriptionContent'; @@ -17,7 +17,6 @@ export default class StructuredDescriptionContent extends YTNode { constructor(data: RawNode) { super(); - this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection - , VideoDescriptionInfocardsSection, VideoDescriptionTranscriptSection, HorizontalCardList ]); + this.items = Parser.parseArray(data.items, [ VideoDescriptionHeader, ExpandableVideoDescriptionBody, VideoDescriptionMusicSection, VideoDescriptionInfocardsSection, HorizontalCardList ]); } } \ No newline at end of file