From b19d687eed38636a487e6d72b166b9066416d77f Mon Sep 17 00:00:00 2001 From: LuanRT Date: Thu, 13 Apr 2023 10:53:42 +0000 Subject: [PATCH] chore: v4.3.0 release --- README.md | 2 +- deno/package.json | 2 +- deno/src/parser/classes/Button.ts | 9 +- deno/src/parser/classes/GridVideo.ts | 25 ++- .../parser/classes/GuideCollapsibleEntry.ts | 36 +-- .../classes/GuideCollapsibleSectionEntry.ts | 13 +- .../src/parser/classes/GuideDownloadsEntry.ts | 9 +- deno/src/parser/classes/GuideEntry.ts | 25 ++- deno/src/parser/classes/GuideSection.ts | 14 +- .../classes/GuideSubscriptionsSection.ts | 6 +- .../parser/classes/MusicResponsiveListItem.ts | 212 +++++++++++------- .../parser/classes/MusicTastebuilderShelf.ts | 26 +++ .../MusicTastebuilderShelfThumbnail.ts | 14 ++ .../parser/classes/ToggleMenuServiceItem.ts | 15 +- .../classes/menus/MenuNavigationItem.ts | 1 + deno/src/parser/nodes.ts | 2 + deno/src/parser/ytmusic/HomeFeed.ts | 9 +- deno/src/utils/Utils.ts | 5 + 18 files changed, 257 insertions(+), 168 deletions(-) create mode 100644 deno/src/parser/classes/MusicTastebuilderShelf.ts create mode 100644 deno/src/parser/classes/MusicTastebuilderShelfThumbnail.ts diff --git a/README.md b/README.md index 52897504..634ffc07 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Special thanks to:

- + SerpApi
diff --git a/deno/package.json b/deno/package.json index de758ec0..215a0805 100644 --- a/deno/package.json +++ b/deno/package.json @@ -1,6 +1,6 @@ { "name": "youtubei.js", - "version": "4.2.0", + "version": "4.3.0", "description": "A wrapper around YouTube's private API. Supports YouTube, YouTube Music, YouTube Kids and YouTube Studio (WIP).", "type": "module", "types": "./dist/src/platform/lib.d.ts", diff --git a/deno/src/parser/classes/Button.ts b/deno/src/parser/classes/Button.ts index aed8b2aa..8c3fc848 100644 --- a/deno/src/parser/classes/Button.ts +++ b/deno/src/parser/classes/Button.ts @@ -2,8 +2,9 @@ import Text from './misc/Text.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; -class Button extends YTNode { +export default class Button extends YTNode { static type = 'Button'; text?: string; @@ -15,7 +16,7 @@ class Button extends YTNode { endpoint: NavigationEndpoint; - constructor(data: any) { + constructor(data: RawNode) { super(); if (data.text) { @@ -40,6 +41,4 @@ class Button extends YTNode { this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint || data.command); } -} - -export default Button; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/GridVideo.ts b/deno/src/parser/classes/GridVideo.ts index 0e8d1b7f..d6d1f04f 100644 --- a/deno/src/parser/classes/GridVideo.ts +++ b/deno/src/parser/classes/GridVideo.ts @@ -8,7 +8,7 @@ import Menu from './menus/Menu.ts'; import { YTNode } from '../helpers.ts'; -class GridVideo extends YTNode { +export default class GridVideo extends YTNode { static type = 'GridVideo'; id: string; @@ -23,10 +23,15 @@ class GridVideo extends YTNode { short_view_count: Text; endpoint: NavigationEndpoint; menu: Menu | null; + buttons?; + upcoming?: Date; + upcoming_text?: Text; + is_reminder_set?: boolean; constructor(data: RawNode) { super(); const length_alt = data.thumbnailOverlays.find((overlay: any) => overlay.hasOwnProperty('thumbnailOverlayTimeStatusRenderer'))?.thumbnailOverlayTimeStatusRenderer; + this.id = data.videoId; this.title = new Text(data.title); this.thumbnails = Thumbnail.fromResponse(data.thumbnail); @@ -39,7 +44,19 @@ class GridVideo extends YTNode { this.short_view_count = new Text(data.shortViewCountText); this.endpoint = new NavigationEndpoint(data.navigationEndpoint); this.menu = Parser.parseItem(data.menu, Menu); - } -} -export default GridVideo; \ No newline at end of file + if (Reflect.has(data, 'buttons')) { + this.buttons = Parser.parseArray(data.buttons); + } + + if (Reflect.has(data, 'upcomingEventData')) { + this.upcoming = new Date(Number(`${data.upcomingEventData.startTime}000`)); + this.upcoming_text = new Text(data.upcomingEventData.upcomingEventText); + this.is_reminder_set = !!data.upcomingEventData?.isReminderSet; + } + } + + get is_upcoming(): boolean { + return Boolean(this.upcoming && this.upcoming > new Date()); + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/GuideCollapsibleEntry.ts b/deno/src/parser/classes/GuideCollapsibleEntry.ts index 28cd98ac..921fa40c 100644 --- a/deno/src/parser/classes/GuideCollapsibleEntry.ts +++ b/deno/src/parser/classes/GuideCollapsibleEntry.ts @@ -1,35 +1,19 @@ -import Text from './misc/Text.ts'; -import { YTNode } from '../helpers.ts'; import Parser from '../parser.ts'; +import GuideEntry from './GuideEntry.ts'; +import type { RawNode } from '../index.ts'; +import { YTNode } from '../helpers.ts'; -class GuideCollapsibleEntry extends YTNode { +export default class GuideCollapsibleEntry extends YTNode { static type = 'GuideCollapsibleEntry'; - expander_item: { - title: string, - icon_type: string - }; - collapser_item: { - title: string, - icon_type: string - }; + expander_item: GuideEntry | null; + collapser_item: GuideEntry | null; expandable_items; - constructor(data: any) { + constructor(data: RawNode) { super(); - - this.expander_item = { - title: new Text(data.expanderItem.guideEntryRenderer.formattedTitle).toString(), - icon_type: data.expanderItem.guideEntryRenderer.icon.iconType - }; - - this.collapser_item = { - title: new Text(data.collapserItem.guideEntryRenderer.formattedTitle).toString(), - icon_type: data.collapserItem.guideEntryRenderer.icon.iconType - }; - + this.expander_item = Parser.parseItem(data.expanderItem, GuideEntry); + this.collapser_item = Parser.parseItem(data.collapserItem, GuideEntry); this.expandable_items = Parser.parseArray(data.expandableItems); } -} - -export default GuideCollapsibleEntry; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/GuideCollapsibleSectionEntry.ts b/deno/src/parser/classes/GuideCollapsibleSectionEntry.ts index 75eb0d73..767eea3f 100644 --- a/deno/src/parser/classes/GuideCollapsibleSectionEntry.ts +++ b/deno/src/parser/classes/GuideCollapsibleSectionEntry.ts @@ -1,7 +1,8 @@ -import { YTNode } from '../helpers.ts'; import Parser from '../parser.ts'; +import type { RawNode } from '../index.ts'; +import { YTNode } from '../helpers.ts'; -class GuideCollapsibleSectionEntry extends YTNode { +export default class GuideCollapsibleSectionEntry extends YTNode { static type = 'GuideCollapsibleSectionEntry'; header_entry; @@ -9,15 +10,11 @@ class GuideCollapsibleSectionEntry extends YTNode { collapser_icon: string; section_items; - constructor(data: any) { + constructor(data: RawNode) { super(); - this.header_entry = Parser.parseItem(data.headerEntry); this.expander_icon = data.expanderIcon.iconType; this.collapser_icon = data.collapserIcon.iconType; this.section_items = Parser.parseArray(data.sectionItems); - } -} - -export default GuideCollapsibleSectionEntry; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/GuideDownloadsEntry.ts b/deno/src/parser/classes/GuideDownloadsEntry.ts index 2c27f921..07884970 100644 --- a/deno/src/parser/classes/GuideDownloadsEntry.ts +++ b/deno/src/parser/classes/GuideDownloadsEntry.ts @@ -1,14 +1,13 @@ import GuideEntry from './GuideEntry.ts'; +import type { RawNode } from '../index.ts'; -class GuideDownloadsEntry extends GuideEntry { +export default class GuideDownloadsEntry extends GuideEntry { static type = 'GuideDownloadsEntry'; always_show: boolean; - constructor(data: any) { + constructor(data: RawNode) { super(data.entryRenderer.guideEntryRenderer); this.always_show = !!data.alwaysShow; } -} - -export default GuideDownloadsEntry; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/GuideEntry.ts b/deno/src/parser/classes/GuideEntry.ts index 76a232fc..529a4d5f 100644 --- a/deno/src/parser/classes/GuideEntry.ts +++ b/deno/src/parser/classes/GuideEntry.ts @@ -1,9 +1,11 @@ -import Text from './misc/Text.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; -import { YTNode } from '../helpers.ts'; +import Text from './misc/Text.ts'; import Thumbnail from './misc/Thumbnail.ts'; -class GuideEntry extends YTNode { +import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; + +export default class GuideEntry extends YTNode { static type = 'GuideEntry'; title: Text; @@ -13,21 +15,24 @@ class GuideEntry extends YTNode { badges?: any; is_primary: boolean; - constructor(data: any) { + constructor(data: RawNode) { super(); this.title = new Text(data.formattedTitle); this.endpoint = new NavigationEndpoint(data.navigationEndpoint || data.serviceEndpoint); - if (data.icon?.iconType) { + + if (Reflect.has(data, 'icon') && Reflect.has(data.icon, 'iconType')) { this.icon_type = data.icon.iconType; } - if (data.thumbnail) { + + if (Reflect.has(data, 'thumbnail')) { this.thumbnails = Thumbnail.fromResponse(data.thumbnail); } - if (data.badges) { + + // (LuanRT) XXX: Check this property's data and parse it. + if (Reflect.has(data, 'badges')) { this.badges = data.badges; } + this.is_primary = !!data.isPrimary; } -} - -export default GuideEntry; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/GuideSection.ts b/deno/src/parser/classes/GuideSection.ts index 0361b5fd..ef1fca23 100644 --- a/deno/src/parser/classes/GuideSection.ts +++ b/deno/src/parser/classes/GuideSection.ts @@ -1,20 +1,20 @@ import Text from './misc/Text.ts'; -import { YTNode } from '../helpers.ts'; import Parser from '../parser.ts'; +import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; -class GuideSection extends YTNode { +export default class GuideSection extends YTNode { static type = 'GuideSection'; title?: Text; items; - constructor(data: any) { + constructor(data: RawNode) { super(); - if (data.formattedTitle) { + if (Reflect.has(data, 'formattedTitle')) { this.title = new Text(data.formattedTitle); } + this.items = Parser.parseArray(data.items); } -} - -export default GuideSection; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/GuideSubscriptionsSection.ts b/deno/src/parser/classes/GuideSubscriptionsSection.ts index e6a2864f..6af8e45a 100644 --- a/deno/src/parser/classes/GuideSubscriptionsSection.ts +++ b/deno/src/parser/classes/GuideSubscriptionsSection.ts @@ -1,7 +1,5 @@ import GuideSection from './GuideSection.ts'; -class GuideSubscriptionsSection extends GuideSection { +export default class GuideSubscriptionsSection extends GuideSection { static type = 'GuideSubscriptionsSection'; -} - -export default GuideSubscriptionsSection; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/MusicResponsiveListItem.ts b/deno/src/parser/classes/MusicResponsiveListItem.ts index fc48ad02..80bf0f4e 100644 --- a/deno/src/parser/classes/MusicResponsiveListItem.ts +++ b/deno/src/parser/classes/MusicResponsiveListItem.ts @@ -1,33 +1,33 @@ -// TODO: this needs a refactor -// Seems like a mess to use +// TODO: Clean up and refactor this. import Parser from '../index.ts'; -import Text from './misc/Text.ts'; -import TextRun from './misc/TextRun.ts'; -import Thumbnail from './misc/Thumbnail.ts'; -import NavigationEndpoint from './NavigationEndpoint.ts'; import MusicItemThumbnailOverlay from './MusicItemThumbnailOverlay.ts'; -import MusicResponsiveListItemFlexColumn from './MusicResponsiveListItemFlexColumn.ts'; import MusicResponsiveListItemFixedColumn from './MusicResponsiveListItemFixedColumn.ts'; +import MusicResponsiveListItemFlexColumn from './MusicResponsiveListItemFlexColumn.ts'; +import MusicThumbnail from './MusicThumbnail.ts'; +import NavigationEndpoint from './NavigationEndpoint.ts'; import Menu from './menus/Menu.ts'; +import Text from './misc/Text.ts'; -import { timeToSeconds } from '../../utils/Utils.ts'; +import { isTextRun, timeToSeconds } from '../../utils/Utils.ts'; +import type { ObservedArray } from '../helpers.ts'; import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; -class MusicResponsiveListItem extends YTNode { +export default class MusicResponsiveListItem extends YTNode { static type = 'MusicResponsiveListItem'; - #flex_columns; - #fixed_columns; + flex_columns: ObservedArray; + fixed_columns: ObservedArray; #playlist_item_data; - endpoint; - item_type; - index; - thumbnails; + endpoint: NavigationEndpoint | null; + item_type: 'album' | 'playlist' | 'artist' | 'library_artist' | 'video' | 'song' | 'endpoint' | 'unknown' | undefined; + index?: Text; + thumbnail?: MusicThumbnail | null; badges; - menu; - overlay; + menu?: Menu | null; + overlay?: MusicItemThumbnailOverlay | null; id?: string; title?: string; @@ -59,19 +59,20 @@ class MusicResponsiveListItem extends YTNode { subtitle?: Text; subscribers?: string; song_count?: string; + // TODO: these might be replaceable with Author class author?: { name: string, channel_id?: string endpoint?: NavigationEndpoint }; - item_count?: string | undefined; + item_count?: string; year?: string; - constructor(data: any) { + constructor(data: RawNode) { super(); - this.#flex_columns = Parser.parseArray(data.flexColumns, MusicResponsiveListItemFlexColumn); - this.#fixed_columns = Parser.parseArray(data.fixedColumns, MusicResponsiveListItemFixedColumn); + this.flex_columns = Parser.parseArray(data.flexColumns, MusicResponsiveListItemFlexColumn); + this.fixed_columns = Parser.parseArray(data.fixedColumns, MusicResponsiveListItemFixedColumn); this.#playlist_item_data = { video_id: data?.playlistItemData?.videoId || null, @@ -101,7 +102,7 @@ class MusicResponsiveListItem extends YTNode { this.#parseLibraryArtist(); break; default: - if (this.#flex_columns[1]) { + if (this.flex_columns[1]) { this.#parseVideoOrSong(); } else { this.#parseOther(); @@ -113,14 +114,14 @@ class MusicResponsiveListItem extends YTNode { this.index = new Text(data.index); } - this.thumbnails = data.thumbnail ? Thumbnail.fromResponse(data.thumbnail.musicThumbnailRenderer?.thumbnail) : []; + this.thumbnail = Parser.parseItem(data.thumbnail, MusicThumbnail); this.badges = Parser.parseArray(data.badges); - this.menu = Parser.parseItem(data.menu, Menu); - this.overlay = Parser.parseItem(data.overlay, MusicItemThumbnailOverlay); + this.menu = Parser.parseItem(data.menu, Menu); + this.overlay = Parser.parseItem(data.overlay, MusicItemThumbnailOverlay); } #parseOther() { - this.title = this.#flex_columns[0].key('title').instanceof(Text).toString(); + this.title = this.flex_columns.first().key('title').instanceof(Text).toString(); if (this.endpoint) { this.item_type = 'endpoint'; @@ -130,7 +131,7 @@ class MusicResponsiveListItem extends YTNode { } #parseVideoOrSong() { - const is_video = this.#flex_columns[1].key('title').instanceof(Text).runs?.some((run) => run.text.match(/(.*?) views/)); + const is_video = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.some((run) => run.text.match(/(.*?) views/)); if (is_video) { this.item_type = 'video'; this.#parseVideo(); @@ -142,105 +143,144 @@ class MusicResponsiveListItem extends YTNode { #parseSong() { this.id = this.#playlist_item_data.video_id || this.endpoint?.payload?.videoId; - this.title = this.#flex_columns[0].key('title').instanceof(Text).toString(); + this.title = this.flex_columns.first().key('title').instanceof(Text).toString(); - const duration_text = - this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || - this.#fixed_columns?.[0]?.key('title').instanceof(Text)?.toString(); + const duration_text = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find( + (run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.key('title').instanceof(Text)?.toString(); - duration_text && (this.duration = { - text: duration_text, - seconds: timeToSeconds(duration_text) - }); - - const album = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('MPR')) as TextRun || - this.#flex_columns[2]?.key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('MPR')) as TextRun; - if (album) { - this.album = { - id: album.endpoint?.payload?.browseId, - name: album.text, - endpoint: album.endpoint + if (duration_text) { + this.duration = { + text: duration_text, + seconds: timeToSeconds(duration_text) }; } - const artists = this.#flex_columns[1].key('title').instanceof(Text).runs?.filter((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun[]; - if (artists) { - this.artists = artists.map((artist) => ({ - name: artist.text, - channel_id: artist.endpoint?.payload?.browseId, - endpoint: artist.endpoint + const album_run = + this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find( + (run) => + (isTextRun(run) && run.endpoint) && + run.endpoint.payload.browseId.startsWith('MPR') + ) || + this.flex_columns.at(2)?.key('title').instanceof(Text).runs?.find( + (run) => + (isTextRun(run) && run.endpoint) && + run.endpoint.payload.browseId.startsWith('MPR') + ); + + if (album_run && isTextRun(album_run)) { + this.album = { + id: album_run.endpoint?.payload?.browseId, + name: album_run.text, + endpoint: album_run.endpoint + }; + } + + const artist_runs = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.filter( + (run) => (isTextRun(run) && run.endpoint) && run.endpoint.payload.browseId.startsWith('UC') + ); + + if (artist_runs) { + this.artists = artist_runs.map((run) => ({ + name: run.text, + channel_id: isTextRun(run) ? run.endpoint?.payload?.browseId : undefined, + endpoint: isTextRun(run) ? run.endpoint : undefined })); } } #parseVideo() { this.id = this.#playlist_item_data.video_id; - this.title = this.#flex_columns[0].key('title').instanceof(Text).toString(); - this.views = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => run.text.match(/(.*?) views/))?.text; + this.title = this.flex_columns.first().key('title').instanceof(Text).toString(); + this.views = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find((run) => run.text.match(/(.*?) views/))?.toString(); - const authors = this.#flex_columns[1].key('title').instanceof(Text).runs?.filter((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun[]; - if (authors) { - this.authors = authors.map((author) => ({ - name: author.text, - channel_id: author.endpoint?.payload?.browseId, - endpoint: author.endpoint - })); + const author_runs = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.filter( + (run) => + (isTextRun(run) && run.endpoint) && + run.endpoint.payload.browseId.startsWith('UC') + ); + + if (author_runs) { + this.authors = author_runs.map((run) => { + return { + name: run.text, + channel_id: isTextRun(run) ? run.endpoint?.payload?.browseId : undefined, + endpoint: isTextRun(run) ? run.endpoint : undefined + }; + }); } - const duration_text = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || - this.#fixed_columns[0]?.key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text; - duration_text && (this.duration = { - text: duration_text, - seconds: timeToSeconds(duration_text) - }); + const duration_text = this.flex_columns[1].key('title').instanceof(Text).runs?.find( + (run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text || this.fixed_columns.first()?.key('title').instanceof(Text).runs?.find((run) => (/^\d+$/).test(run.text.replace(/:/g, '')))?.text; + + if (duration_text) { + this.duration = { + text: duration_text, + seconds: timeToSeconds(duration_text) + }; + } } #parseArtist() { this.id = this.endpoint?.payload?.browseId; - this.name = this.#flex_columns[0].key('title').instanceof(Text).toString(); - this.subtitle = this.#flex_columns[1].key('title').instanceof(Text); - this.subscribers = this.subtitle.runs?.find((run) => (/^(\d*\.)?\d+[M|K]? subscribers?$/i).test(run.text))?.text || ''; + this.name = this.flex_columns.first().key('title').instanceof(Text).toString(); + this.subtitle = this.flex_columns.at(1)?.key('title').instanceof(Text); + this.subscribers = this.subtitle?.runs?.find((run) => (/^(\d*\.)?\d+[M|K]? subscribers?$/i).test(run.text))?.text || ''; } #parseLibraryArtist() { - this.name = this.#flex_columns[0].key('title').instanceof(Text).toString(); - this.subtitle = this.#flex_columns[1].key('title').instanceof(Text); + this.name = this.flex_columns.first().key('title').instanceof(Text).toString(); + this.subtitle = this.flex_columns.at(1)?.key('title').instanceof(Text); this.song_count = this.subtitle?.runs?.find((run) => (/^\d+(,\d+)? songs?$/i).test(run.text))?.text || ''; } #parseAlbum() { this.id = this.endpoint?.payload?.browseId; - this.title = this.#flex_columns[0].key('title').instanceof(Text).toString(); + this.title = this.flex_columns.first().title.toString(); - const author = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun; - author && (this.author = { - name: author.text, - channel_id: author.endpoint?.payload?.browseId, - endpoint: author.endpoint - }); + const author_run = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find( + (run) => + (isTextRun(run) && run.endpoint) && + run.endpoint.payload.browseId.startsWith('UC') + ); - this.year = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => (/^[12][0-9]{3}$/).test(run.text))?.text; + if (author_run && isTextRun(author_run)) { + this.author = { + name: author_run.text, + channel_id: author_run.endpoint?.payload?.browseId, + endpoint: author_run.endpoint + }; + } + + this.year = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find( + (run) => (/^[12][0-9]{3}$/).test(run.text) + )?.text; } #parsePlaylist() { this.id = this.endpoint?.payload?.browseId; - this.title = this.#flex_columns[0].key('title').instanceof(Text).toString(); + this.title = this.flex_columns.first().title.toString(); - const item_count_run = this.#flex_columns[1].key('title') + const item_count_run = this.flex_columns.at(1)?.key('title') .instanceof(Text).runs?.find((run) => run.text.match(/\d+ (song|songs)/)); this.item_count = item_count_run ? item_count_run.text : undefined; - const author = this.#flex_columns[1].key('title').instanceof(Text).runs?.find((run) => Reflect.get(run, 'endpoint')?.payload?.browseId.startsWith('UC')) as TextRun; + const author_run = this.flex_columns.at(1)?.key('title').instanceof(Text).runs?.find( + (run) => + (isTextRun(run) && run.endpoint) && + run.endpoint.payload.browseId.startsWith('UC') + ); - if (author) { + if (author_run && isTextRun(author_run)) { this.author = { - name: author.text, - channel_id: author.endpoint?.payload?.browseId, - endpoint: author.endpoint + name: author_run.text, + channel_id: author_run.endpoint?.payload?.browseId, + endpoint: author_run.endpoint }; } } -} -export default MusicResponsiveListItem; \ No newline at end of file + get thumbnails() { + return this.thumbnail?.contents || []; + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/MusicTastebuilderShelf.ts b/deno/src/parser/classes/MusicTastebuilderShelf.ts new file mode 100644 index 00000000..90cd5e70 --- /dev/null +++ b/deno/src/parser/classes/MusicTastebuilderShelf.ts @@ -0,0 +1,26 @@ +import Parser from '../index.ts'; +import Button from './Button.ts'; +import Text from './misc/Text.ts'; +import MusicTastebuilderShelfThumbnail from './MusicTastebuilderShelfThumbnail.ts'; + +import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; + +export default class MusicTasteBuilderShelf extends YTNode { + static type = 'MusicTasteBuilderShelf'; + + thumbnail: MusicTastebuilderShelfThumbnail | null; + primary_text: Text; + secondary_text: Text; + action_button: Button | null; + is_visible: boolean; + + constructor(data: RawNode) { + super(); + this.thumbnail = Parser.parseItem(data.thumbnail, MusicTastebuilderShelfThumbnail); + this.primary_text = new Text(data.primaryText); + this.secondary_text = new Text(data.secondaryText); + this.action_button = Parser.parseItem(data.actionButton, Button); + this.is_visible = data.isVisible; + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/MusicTastebuilderShelfThumbnail.ts b/deno/src/parser/classes/MusicTastebuilderShelfThumbnail.ts new file mode 100644 index 00000000..db0e9b95 --- /dev/null +++ b/deno/src/parser/classes/MusicTastebuilderShelfThumbnail.ts @@ -0,0 +1,14 @@ +import { YTNode } from '../helpers.ts'; +import { Thumbnail } from '../misc.ts'; +import type { RawNode } from '../index.ts'; + +export default class MusicTastebuilderShelfThumbnail extends YTNode { + static type = 'MusicTastebuilderShelfThumbnail'; + + thumbnail: Thumbnail[]; + + constructor(data: RawNode) { + super(); + this.thumbnail = Thumbnail.fromResponse(data.thumbnail); + } +} \ No newline at end of file diff --git a/deno/src/parser/classes/ToggleMenuServiceItem.ts b/deno/src/parser/classes/ToggleMenuServiceItem.ts index a067bdf1..7a819ed0 100644 --- a/deno/src/parser/classes/ToggleMenuServiceItem.ts +++ b/deno/src/parser/classes/ToggleMenuServiceItem.ts @@ -1,24 +1,25 @@ import Text from './misc/Text.ts'; import NavigationEndpoint from './NavigationEndpoint.ts'; import { YTNode } from '../helpers.ts'; +import type { RawNode } from '../index.ts'; -class ToggleMenuServiceItem extends YTNode { +export default class ToggleMenuServiceItem extends YTNode { static type = 'ToggleMenuServiceItem'; text: Text; toggled_text: Text; icon_type: string; toggled_icon_type: string; - endpoint: NavigationEndpoint; + default_endpoint: NavigationEndpoint; + toggled_endpoint: NavigationEndpoint; - constructor(data: any) { + constructor(data: RawNode) { super(); this.text = new Text(data.defaultText); this.toggled_text = new Text(data.toggledText); this.icon_type = data.defaultIcon.iconType; this.toggled_icon_type = data.toggledIcon.iconType; - this.endpoint = new NavigationEndpoint(data.toggledServiceEndpoint); + this.default_endpoint = new NavigationEndpoint(data.defaultServiceEndpoint); + this.toggled_endpoint = new NavigationEndpoint(data.toggledServiceEndpoint); } -} - -export default ToggleMenuServiceItem; \ No newline at end of file +} \ No newline at end of file diff --git a/deno/src/parser/classes/menus/MenuNavigationItem.ts b/deno/src/parser/classes/menus/MenuNavigationItem.ts index 99540ebe..4c9cd635 100644 --- a/deno/src/parser/classes/menus/MenuNavigationItem.ts +++ b/deno/src/parser/classes/menus/MenuNavigationItem.ts @@ -1,5 +1,6 @@ import Button from '../Button.ts'; import type { RawNode } from '../../index.ts'; + class MenuNavigationItem extends Button { static type = 'MenuNavigationItem'; diff --git a/deno/src/parser/nodes.ts b/deno/src/parser/nodes.ts index 0102ad5b..e9e3f91e 100644 --- a/deno/src/parser/nodes.ts +++ b/deno/src/parser/nodes.ts @@ -219,6 +219,8 @@ export { default as MusicResponsiveListItemFlexColumn } from './classes/MusicRes export { default as MusicShelf } from './classes/MusicShelf.ts'; export { default as MusicSideAlignedItem } from './classes/MusicSideAlignedItem.ts'; export { default as MusicSortFilterButton } from './classes/MusicSortFilterButton.ts'; +export { default as MusicTastebuilderShelf } from './classes/MusicTastebuilderShelf.ts'; +export { default as MusicTastebuilderShelfThumbnail } from './classes/MusicTastebuilderShelfThumbnail.ts'; export { default as MusicThumbnail } from './classes/MusicThumbnail.ts'; export { default as MusicTwoRowItem } from './classes/MusicTwoRowItem.ts'; export { default as MusicVisualHeader } from './classes/MusicVisualHeader.ts'; diff --git a/deno/src/parser/ytmusic/HomeFeed.ts b/deno/src/parser/ytmusic/HomeFeed.ts index 39519c69..51c34e84 100644 --- a/deno/src/parser/ytmusic/HomeFeed.ts +++ b/deno/src/parser/ytmusic/HomeFeed.ts @@ -1,20 +1,21 @@ -import { InnertubeError } from '../../utils/Utils.ts'; +import Parser, { SectionListContinuation } from '../index.ts'; import MusicCarouselShelf from '../classes/MusicCarouselShelf.ts'; import SectionList from '../classes/SectionList.ts'; import SingleColumnBrowseResults from '../classes/SingleColumnBrowseResults.ts'; -import Parser, { SectionListContinuation } from '../index.ts'; +import MusicTastebuilderShelf from '../classes/MusicTastebuilderShelf.ts'; import type Actions from '../../core/Actions.ts'; import type { ApiResponse } from '../../core/Actions.ts'; import type { ObservedArray } from '../helpers.ts'; import type { IBrowseResponse } from '../types/ParsedResponse.ts'; +import { InnertubeError } from '../../utils/Utils.ts'; class HomeFeed { #page: IBrowseResponse; #actions: Actions; #continuation?: string; - sections?: ObservedArray; + sections?: ObservedArray; constructor(response: ApiResponse, actions: Actions) { this.#actions = actions; @@ -36,7 +37,7 @@ class HomeFeed { } this.#continuation = tab.content?.as(SectionList).continuation; - this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf); + this.sections = tab.content?.as(SectionList).contents.as(MusicCarouselShelf, MusicTastebuilderShelf); } /** diff --git a/deno/src/utils/Utils.ts b/deno/src/utils/Utils.ts index 99ca3ade..b08a205d 100644 --- a/deno/src/utils/Utils.ts +++ b/deno/src/utils/Utils.ts @@ -1,4 +1,5 @@ import { Memo } from '../parser/helpers.ts'; +import { EmojiRun, TextRun } from '../parser/misc.ts'; import PlatformShim, { FetchFunction } from '../types/PlatformShim.ts'; import userAgents from './user-agents.ts'; @@ -221,4 +222,8 @@ export function u8ToBase64(u8: Uint8Array): string { export function base64ToU8(base64: string): Uint8Array { return new Uint8Array(atob(base64).split('').map((char) => char.charCodeAt(0))); +} + +export function isTextRun(run: TextRun | EmojiRun): run is TextRun { + return !('emoji' in run); } \ No newline at end of file