From 9b4d86b81f137ec89b8adecf49d265f3de5c8392 Mon Sep 17 00:00:00 2001 From: patrickkfkan <55383971+patrickkfkan@users.noreply.github.com> Date: Thu, 11 Aug 2022 01:11:31 +0800 Subject: [PATCH] feat(ytmusic): add `music#getPlaylist()` (#131) * add music#getPlaylist() * fix: lint errors --- src/core/Music.ts | 11 ++++ src/parser/classes/MusicPlaylistShelf.js | 22 ------- src/parser/classes/MusicPlaylistShelf.ts | 24 +++++++ src/parser/index.ts | 17 ++++- src/parser/ytmusic/Playlist.ts | 80 ++++++++++++++++++++++++ test/main.test.js | 5 ++ 6 files changed, 136 insertions(+), 23 deletions(-) delete mode 100644 src/parser/classes/MusicPlaylistShelf.js create mode 100644 src/parser/classes/MusicPlaylistShelf.ts create mode 100644 src/parser/ytmusic/Playlist.ts diff --git a/src/core/Music.ts b/src/core/Music.ts index 81a20cad..c060196c 100644 --- a/src/core/Music.ts +++ b/src/core/Music.ts @@ -8,6 +8,7 @@ import Explore from '../parser/ytmusic/Explore'; import Library from '../parser/ytmusic/Library'; import Artist from '../parser/ytmusic/Artist'; import Album from '../parser/ytmusic/Album'; +import Playlist from '../parser/ytmusic/Playlist'; import Parser from '../parser/index'; import { observe, YTNode } from '../parser/helpers'; @@ -108,6 +109,16 @@ class Music { return new Album(response, this.#actions); } + /** + * Retrieves playlist. + */ + async getPlaylist(playlist_id: string) { + throwIfMissing({ playlist_id }); + + const response = await this.#actions.browse(`VL${playlist_id.replace(/VL/g, '')}`, { client: 'YTMUSIC' }); + return new Playlist(response, this.#actions); + } + /** * Retrieves song lyrics. */ diff --git a/src/parser/classes/MusicPlaylistShelf.js b/src/parser/classes/MusicPlaylistShelf.js deleted file mode 100644 index 92a2c7a4..00000000 --- a/src/parser/classes/MusicPlaylistShelf.js +++ /dev/null @@ -1,22 +0,0 @@ -import Parser from '../index'; -import { YTNode } from '../helpers'; - -class MusicPlaylistShelf extends YTNode { - static type = 'MusicPlaylistShelf'; - - #continuations; - - constructor(data) { - super(); - this.playlist_id = data.playlistId; - this.contents = Parser.parse(data.contents); - this.collapsed_item_count = data.collapsedItemCount; - this.#continuations = data.continuations; - } - - get continuation() { - return this.#continuations?.[0]?.nextContinuationData; - } -} - -export default MusicPlaylistShelf; \ No newline at end of file diff --git a/src/parser/classes/MusicPlaylistShelf.ts b/src/parser/classes/MusicPlaylistShelf.ts new file mode 100644 index 00000000..0b084003 --- /dev/null +++ b/src/parser/classes/MusicPlaylistShelf.ts @@ -0,0 +1,24 @@ +import Parser from '../index'; +import MusicResponsiveListItem from './MusicResponsiveListItem'; + +import { YTNode } from '../helpers'; + +class MusicPlaylistShelf extends YTNode { + static type = 'MusicPlaylistShelf'; + + playlist_id: string; + contents; + collapsed_item_count: number; + continuation: string | null; + + constructor(data: any) { + super(); + + this.playlist_id = data.playlistId; + this.contents = Parser.parseArray(data.contents, MusicResponsiveListItem); + this.collapsed_item_count = data.collapsedItemCount; + this.continuation = data.continuations?.[0]?.nextContinuationData?.continuation || null; + } +} + +export default MusicPlaylistShelf; \ No newline at end of file diff --git a/src/parser/index.ts b/src/parser/index.ts index b355bb85..8f2ecc27 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -43,7 +43,20 @@ export class SectionListContinuation extends YTNode { constructor(data: any) { super(); this.contents = Parser.parse(data.contents, true); - this.continuation = data.continuations[0].nextContinuationData.continuation; + this.continuation = data.continuations?.[0].nextContinuationData.continuation || null; + } +} + +export class MusicPlaylistShelfContinuation extends YTNode { + static readonly type = 'musicPlaylistShelfContinuation'; + + continuation: string; + contents: ObservedArray | null; + + constructor(data: any) { + super(); + this.contents = Parser.parse(data.contents, true); + this.continuation = data.continuations?.[0].nextContinuationData.continuation || null; } } @@ -220,6 +233,8 @@ export default class Parser { return new SectionListContinuation(data.sectionListContinuation); if (data.liveChatContinuation) return new LiveChatContinuation(data.liveChatContinuation); + if (data.musicPlaylistShelfContinuation) + return new MusicPlaylistShelfContinuation(data.musicPlaylistShelfContinuation); } static parseRR(actions: any[]) { diff --git a/src/parser/ytmusic/Playlist.ts b/src/parser/ytmusic/Playlist.ts new file mode 100644 index 00000000..6b7951b2 --- /dev/null +++ b/src/parser/ytmusic/Playlist.ts @@ -0,0 +1,80 @@ +import Parser, { MusicPlaylistShelfContinuation, 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'; + +class Playlist { + #page; + #actions; + #continuation; + + header; + items; + + constructor(response: AxioslikeResponse, actions: Actions) { + this.#actions = actions; + this.#page = Parser.parseResponse(response.data); + this.#actions = actions; + + 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; + } + } + + get page(): ParsedResponse { + return this.#page; + } + + get has_continuation() { + return !!this.#continuation; + } + + /** + * Retrieves playlist item continuation. + */ + async getContinuation() { + if (this.#continuation) { + const response = await this.#actions.browse(this.#continuation, { is_ctoken: true, client: 'YTMUSIC' }); + return new Playlist(response, this.#actions); + } + + throw new InnertubeError('Continuation not found.'); + + } + + /** + * Retrieves related playlists + */ + async getRelated() { + let section_continuation = this.#page.contents_memo.get('SectionList')?.[0].as(SectionList).continuation; + + while (section_continuation) { + const response = await this.#actions.browse(section_continuation, { is_ctoken: true, client: 'YTMUSIC' }); + const data = Parser.parseResponse(response.data); + const section_list = data.continuation_contents?.as(SectionListContinuation); + const sections = section_list?.contents?.as(MusicCarouselShelf); + const related = sections?.filter((section) => section.header?.title === 'Related playlists')[0]; + if (related) { + return related.contents || []; + } + + section_continuation = section_list?.continuation; + + } + + return []; + } + +} + +export default Playlist; \ No newline at end of file diff --git a/test/main.test.js b/test/main.test.js index b6891908..3737a22a 100644 --- a/test/main.test.js +++ b/test/main.test.js @@ -62,6 +62,11 @@ describe('YouTube.js Tests', () => { const playlist = await this.session.getPlaylist('PLLw0AzOz95FU7w2juhPECP9NyGhbZmz_t', { client: 'YOUTUBE' }); expect(playlist.items.length).toBeLessThanOrEqual(100); }); + + it('Should retrieve playlist with YouTube Music', async () => { + const playlist = await this.session.music.getPlaylist('PLVbEymL-83SyVXXqT7fYX5sEvELvyGjL7'); + expect(playlist.items.length).toBeLessThanOrEqual(100); + }); it('Should retrieve home feed', async () => { const homefeed = await this.session.getHomeFeed();