diff --git a/src/parser/classes/DropdownItem.ts b/src/parser/classes/DropdownItem.ts index 80e221c4..72992eb4 100644 --- a/src/parser/classes/DropdownItem.ts +++ b/src/parser/classes/DropdownItem.ts @@ -7,8 +7,9 @@ class DropdownItem extends YTNode { label: string; selected: boolean; - value?: number; + value?: number | string; iconType?: string; + description?: string; endpoint?: NavigationEndpoint; constructor(data: any) { @@ -19,6 +20,8 @@ class DropdownItem extends YTNode { if (data.int32Value) { this.value = data.int32Value; + } else if (data.stringValue) { + this.value = data.stringValue; } if (data.onSelectCommand?.browseEndpoint) { @@ -28,6 +31,10 @@ class DropdownItem extends YTNode { if (data.icon?.iconType) { this.iconType = data.icon?.iconType; } + + if (data.descriptionText) { + this.description = new Text(data.descriptionText).toString(); + } } } diff --git a/src/parser/classes/MusicDetailHeader.js b/src/parser/classes/MusicDetailHeader.js index 1820c619..191c558f 100644 --- a/src/parser/classes/MusicDetailHeader.js +++ b/src/parser/classes/MusicDetailHeader.js @@ -12,7 +12,7 @@ class MusicDetailHeader extends YTNode { this.description = new Text(data.description); this.subtitle = new Text(data.subtitle); this.second_subtitle = new Text(data.secondSubtitle); - this.year = this.subtitle.runs.find((run) => (/^[12][0-9]{3}$/).test(run.text)).text; + this.year = this.subtitle.runs.find((run) => (/^[12][0-9]{3}$/).test(run.text))?.text || null; this.song_count = this.second_subtitle.runs[0].text; this.total_duration = this.second_subtitle.runs[2].text; this.thumbnails = Thumbnail.fromResponse(data.thumbnail.croppedSquareThumbnailRenderer.thumbnail); diff --git a/src/parser/classes/MusicEditablePlaylistDetailHeader.ts b/src/parser/classes/MusicEditablePlaylistDetailHeader.ts new file mode 100644 index 00000000..ffdc155f --- /dev/null +++ b/src/parser/classes/MusicEditablePlaylistDetailHeader.ts @@ -0,0 +1,18 @@ +import Parser from '../index'; +import { YTNode } from '../helpers'; + +class MusicEditablePlaylistDetailHeader extends YTNode { + static type = 'MusicEditablePlaylistDetailHeader'; + + header; + + constructor(data: any) { + super(); + this.header = Parser.parse(data.header); + + // TODO: Should we also parse data.editHeader.musicPlaylistEditHeaderRenderer? + // It doesn't seem practical to do so... + } +} + +export default MusicEditablePlaylistDetailHeader; \ No newline at end of file diff --git a/src/parser/classes/MusicShelf.ts b/src/parser/classes/MusicShelf.ts index 159622c3..4f72f996 100644 --- a/src/parser/classes/MusicShelf.ts +++ b/src/parser/classes/MusicShelf.ts @@ -20,7 +20,9 @@ class MusicShelf extends YTNode { this.title = new Text(data.title); this.contents = Parser.parseArray(data.contents, MusicResponsiveListItem); this.endpoint = Reflect.has(data, 'bottomEndpoint') ? new NavigationEndpoint(data.bottomEndpoint) : null; - this.continuation = data.continuations?.[0]?.nextContinuationData?.continuation || null; + this.continuation = + data.continuations?.[0].nextContinuationData?.continuation || + data.continuations?.[0].reloadContinuationData?.continuation || null; this.bottom_text = Reflect.has(data, 'bottomText') ? new Text(data.bottomText) : null; } } diff --git a/src/parser/index.ts b/src/parser/index.ts index a96a7809..9a58ce7d 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -69,7 +69,9 @@ export class MusicShelfContinuation extends YTNode { constructor(data: any) { super(); this.contents = Parser.parse(data.contents, true); - this.continuation = data.continuations?.[0].nextContinuationData.continuation || null; + this.continuation = + data.continuations?.[0].nextContinuationData?.continuation || + data.continuations?.[0].reloadContinuationData?.continuation || null; } } diff --git a/src/parser/map.ts b/src/parser/map.ts index 5b242983..5b11a1c3 100644 --- a/src/parser/map.ts +++ b/src/parser/map.ts @@ -133,6 +133,7 @@ import { default as MusicCarouselShelf } from './classes/MusicCarouselShelf'; import { default as MusicCarouselShelfBasicHeader } from './classes/MusicCarouselShelfBasicHeader'; import { default as MusicDescriptionShelf } from './classes/MusicDescriptionShelf'; import { default as MusicDetailHeader } from './classes/MusicDetailHeader'; +import { default as MusicEditablePlaylistDetailHeader } from './classes/MusicEditablePlaylistDetailHeader'; import { default as MusicHeader } from './classes/MusicHeader'; import { default as MusicImmersiveHeader } from './classes/MusicImmersiveHeader'; import { default as MusicInlineBadge } from './classes/MusicInlineBadge'; @@ -371,6 +372,7 @@ const map: Record = { MusicCarouselShelfBasicHeader, MusicDescriptionShelf, MusicDetailHeader, + MusicEditablePlaylistDetailHeader, MusicHeader, MusicImmersiveHeader, MusicInlineBadge, diff --git a/src/parser/ytmusic/Playlist.ts b/src/parser/ytmusic/Playlist.ts index 1d970e51..6e62c746 100644 --- a/src/parser/ytmusic/Playlist.ts +++ b/src/parser/ytmusic/Playlist.ts @@ -1,16 +1,19 @@ -import Parser, { MusicPlaylistShelfContinuation, ParsedResponse, SectionListContinuation } from '../index'; +import Parser, { MusicPlaylistShelfContinuation, MusicShelfContinuation, ParsedResponse, SectionListContinuation } from '../index'; import Actions, { AxioslikeResponse } from '../../core/Actions'; -import MusicDetailHeader from '../classes/MusicDetailHeader'; import MusicCarouselShelf from '../classes/MusicCarouselShelf'; import MusicPlaylistShelf from '../classes/MusicPlaylistShelf'; import SectionList from '../classes/SectionList'; import { InnertubeError } from '../../utils/Utils'; +import MusicEditablePlaylistDetailHeader from '../classes/MusicEditablePlaylistDetailHeader'; +import MusicShelf from '../classes/MusicShelf'; class Playlist { #page; #actions; #continuation; + #suggestions_continuation; + #last_fetched_suggestions: any; header; items; @@ -19,15 +22,22 @@ class Playlist { this.#actions = actions; this.#page = Parser.parseResponse(response.data); this.#actions = actions; + this.#suggestions_continuation = this.#page.contents_memo.getType(MusicShelf)?.find( + (shelf) => shelf.title.toString() === 'Suggestions')?.continuation || null; + this.#last_fetched_suggestions = null; if (this.#page.continuation_contents) { const data = this.#page.continuation_contents?.as(MusicPlaylistShelfContinuation); this.items = data.contents; this.#continuation = data.continuation; } else { - this.header = this.#page.header.item().as(MusicDetailHeader); - this.items = this.#page.contents_memo.get('MusicPlaylistShelf')?.[0].as(MusicPlaylistShelf).contents; - this.#continuation = this.#page.contents_memo.get('MusicPlaylistShelf')?.[0].as(MusicPlaylistShelf).continuation || null; + if (this.#page.header?.item().type === 'MusicEditablePlaylistDetailHeader') { + this.header = this.#page.header?.item().as(MusicEditablePlaylistDetailHeader).header.item(); + } else { + this.header = this.#page.header?.item() || null; + } + this.items = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0].contents; + this.#continuation = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0].continuation || null; } } @@ -75,6 +85,36 @@ class Playlist { return []; } + async getSuggestions(refresh = true) { + const require_fetch = refresh || !this.#last_fetched_suggestions; + const fetch_promise = require_fetch ? this.#fetchSuggestions(this.#suggestions_continuation) : Promise.resolve(null); + const fetch_result = await fetch_promise; + + if (fetch_result) { + this.#last_fetched_suggestions = fetch_result.items; + this.#suggestions_continuation = fetch_result.continuation; + } + + return fetch_result?.items || this.#last_fetched_suggestions; + } + + async #fetchSuggestions(continuation: string | null) { + if (continuation) { + const response = await this.#actions.browse(continuation, { is_ctoken: true, client: 'YTMUSIC' }); + const page = Parser.parseResponse(response.data); + const data = page.continuation_contents?.as(MusicShelfContinuation); + return { + items: data?.contents || [], + continuation: data?.continuation || null + }; + } + + return { + items: [], + continuation: null + }; + } + } export default Playlist; \ No newline at end of file