From a3fafe2f7979313906dbaf1a7f9779f411266d6b Mon Sep 17 00:00:00 2001 From: Luan Date: Fri, 21 Feb 2025 18:52:34 -0300 Subject: [PATCH] fix(music#getPlaylist): Handle `ContinuationItem` nodes Closes #904 --- src/parser/classes/MusicPlaylistShelf.ts | 13 +++---- src/parser/ytmusic/Playlist.ts | 46 +++++++++++++++--------- src/utils/Constants.ts | 2 +- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/parser/classes/MusicPlaylistShelf.ts b/src/parser/classes/MusicPlaylistShelf.ts index a4b1444e..8ace60b7 100644 --- a/src/parser/classes/MusicPlaylistShelf.ts +++ b/src/parser/classes/MusicPlaylistShelf.ts @@ -1,19 +1,20 @@ -import { YTNode, type ObservedArray } from '../helpers.js'; +import { type ObservedArray, YTNode } from '../helpers.js'; import { Parser, type RawNode } from '../index.js'; import MusicResponsiveListItem from './MusicResponsiveListItem.js'; +import ContinuationItem from './ContinuationItem.js'; export default class MusicPlaylistShelf extends YTNode { static type = 'MusicPlaylistShelf'; - playlist_id: string; - contents: ObservedArray; - collapsed_item_count: number; - continuation: string | null; + public playlist_id: string; + public contents: ObservedArray; + public collapsed_item_count: number; + public continuation: string | null; constructor(data: RawNode) { super(); this.playlist_id = data.playlistId; - this.contents = Parser.parseArray(data.contents, MusicResponsiveListItem); + this.contents = Parser.parseArray(data.contents, [ MusicResponsiveListItem, ContinuationItem ]); this.collapsed_item_count = data.collapsedItemCount; this.continuation = data.continuations?.[0]?.nextContinuationData?.continuation || null; } diff --git a/src/parser/ytmusic/Playlist.ts b/src/parser/ytmusic/Playlist.ts index 36c7d79d..9ae03118 100644 --- a/src/parser/ytmusic/Playlist.ts +++ b/src/parser/ytmusic/Playlist.ts @@ -1,6 +1,6 @@ // noinspection ES6MissingAwait -import { Parser, MusicPlaylistShelfContinuation, SectionListContinuation } from '../index.js'; +import { MusicPlaylistShelfContinuation, Parser, SectionListContinuation } from '../index.js'; import MusicCarouselShelf from '../classes/MusicCarouselShelf.js'; import MusicDetailHeader from '../classes/MusicDetailHeader.js'; @@ -13,17 +13,19 @@ import MusicResponsiveHeader from '../classes/MusicResponsiveHeader.js'; import { InnertubeError } from '../../utils/Utils.js'; import { observe, type ObservedArray } from '../helpers.js'; -import type { ApiResponse, Actions } from '../../core/index.js'; +import type { Actions, ApiResponse } from '../../core/index.js'; import type { IBrowseResponse } from '../types/index.js'; import type MusicThumbnail from '../classes/MusicThumbnail.js'; +import ContinuationItem from '../classes/ContinuationItem.js'; +import AppendContinuationItemsAction from '../classes/actions/AppendContinuationItemsAction.js'; export default class Playlist { readonly #page: IBrowseResponse; readonly #actions: Actions; - readonly #continuation: string | null; + readonly #continuation?: string | ContinuationItem; public header?: MusicResponsiveHeader | MusicDetailHeader | MusicEditablePlaylistDetailHeader; - public contents?: ObservedArray; + public contents?: ObservedArray; public background?: MusicThumbnail; #last_fetched_suggestions: ObservedArray | null; @@ -40,15 +42,19 @@ export default class Playlist { const data = this.#page.continuation_contents?.as(MusicPlaylistShelfContinuation); if (!data.contents) throw new InnertubeError('No contents found in the response'); - this.contents = data.contents.as(MusicResponsiveListItem); - this.#continuation = data.continuation; - } else { - if (!this.#page.contents_memo) - throw new InnertubeError('No contents found in the response'); + this.contents = data.contents.as(MusicResponsiveListItem, ContinuationItem); + const continuation_item = this.contents.firstOfType(ContinuationItem); + this.#continuation = data.continuation || continuation_item; + } else if (this.#page.contents_memo) { this.header = this.#page.contents_memo.getType(MusicResponsiveHeader, MusicEditablePlaylistDetailHeader, MusicDetailHeader)?.[0]; - this.contents = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0]?.contents || observe([]); + this.contents = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0]?.contents.as(MusicResponsiveListItem, ContinuationItem) || observe([]); this.background = this.#page.background; - this.#continuation = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0]?.continuation || null; + const continuation_item = this.contents.firstOfType(ContinuationItem); + this.#continuation = this.#page.contents_memo.getType(MusicPlaylistShelf)?.[0]?.continuation || continuation_item; + } else if (this.#page.on_response_received_actions) { + const append_continuation_action = this.#page.on_response_received_actions.firstOfType(AppendContinuationItemsAction); + this.contents = append_continuation_action?.contents?.as(MusicResponsiveListItem, ContinuationItem); + this.#continuation = this.contents?.firstOfType(ContinuationItem); } } @@ -59,11 +65,17 @@ export default class Playlist { if (!this.#continuation) throw new InnertubeError('Continuation not found.'); - const response = await this.#actions.execute('/browse', { - client: 'YTMUSIC', - continuation: this.#continuation - }); - + let response: ApiResponse; + + if (typeof this.#continuation === 'string') { + response = await this.#actions.execute('/browse', { + client: 'YTMUSIC', + continuation: this.#continuation + }); + } else { + response = await this.#continuation.endpoint.call(this.#actions, { client: 'YTMUSIC' }); + } + return new Playlist(response, this.#actions); } @@ -144,7 +156,7 @@ export default class Playlist { return this.#page; } - get items(): ObservedArray { + get items(): ObservedArray { return this.contents || observe([]); } diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 2033f965..577344b3 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -48,7 +48,7 @@ export const CLIENTS = { }, YTMUSIC: { NAME: 'WEB_REMIX', - VERSION: '1.20211213.00.00' + VERSION: '1.20250219.01.00' }, ANDROID: { NAME: 'ANDROID',