From 7471f1e70ca811b2552b81f8a8e091c4f20e0435 Mon Sep 17 00:00:00 2001 From: Zechariah Date: Sat, 25 Dec 2021 17:06:16 +0800 Subject: [PATCH] Fetching playlist by id works --- src/YTMusic.ts | 42 ++++++++++++++++++++++++++++++++--- src/parsers/PlaylistParser.ts | 24 +++++++++++++++++++- src/parsers/VideoParser.ts | 23 +++++++++++++++++-- src/tests/interfaces.ts | 13 +++++++++-- src/tests/testing.ts | 28 +++++++++++++---------- src/types.d.ts | 4 ++-- 6 files changed, 112 insertions(+), 22 deletions(-) diff --git a/src/YTMusic.ts b/src/YTMusic.ts index 23285f6..f919a8a 100644 --- a/src/YTMusic.ts +++ b/src/YTMusic.ts @@ -308,7 +308,7 @@ export default class YTMusic { /** * Get all possible information of an Album - * + * * @param albumId Album ID * @returns Album Data */ @@ -318,9 +318,45 @@ export default class YTMusic { return AlbumParser.parse(data, albumId) } - public async getPlaylist(playlistId: string) { + /** + * Get all possible information of a Playlist except the tracks + * + * @param playlistId Playlist ID + * @returns Playlist Data + */ + public async getPlaylist(playlistId: string): Promise { + if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId const data = await this.constructRequest("browse", { browseId: playlistId }) - fs.writeFileSync("data.json", JSON.stringify(data)) + return PlaylistParser.parse(data, playlistId) + } + + /** + * Get all videos in a Playlist + * + * @param playlistId Playlist ID + * @returns Playlist's Videos + */ + public async getPlaylistVideos( + playlistId: string + ): Promise[]> { + if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId + const playlistData = await this.constructRequest("browse", { browseId: playlistId }) + + const songs = traverse( + playlistData, + "musicPlaylistShelfRenderer", + "musicResponsiveListItemRenderer" + ) + let continuation = traverse(playlistData, "musicPlaylistShelfRenderer", "continuation") + while (true) { + if (continuation instanceof Array) break + + const songsData = await this.constructRequest("browse", {}, { continuation }) + songs.push(...traverse(songsData, "musicResponsiveListItemRenderer")) + continuation = traverse(songsData, "continuation") + } + + return songs.map(VideoParser.parsePlaylistVideo) } } diff --git a/src/parsers/PlaylistParser.ts b/src/parsers/PlaylistParser.ts index ad648fe..3b4606b 100644 --- a/src/parsers/PlaylistParser.ts +++ b/src/parsers/PlaylistParser.ts @@ -1,6 +1,24 @@ import traverse from "../utils/traverse" export default class PlaylistParser { + public static parse(data: any, playlistId: string): YTMusic.PlaylistDetailed { + return { + type: "PLAYLIST", + playlistId, + name: traverse(data, "header", "title", "text").at(0), + artist: { + artistId: traverse(data, "header", "subtitle", "browseId"), + name: traverse(data, "header", "subtitle", "text").at(2) + }, + videoCount: +traverse(data, "header", "secondSubtitle", "text") + .at(0) + .split(" ") + .at(0) + .replaceAll(",", ""), + thumbnails: traverse(data, "header", "thumbnails") + } + } + public static parseSearchResult(item: any): YTMusic.PlaylistDetailed { const flexColumns = traverse(item, "flexColumns") const artistId = traverse(flexColumns[1], "browseId") @@ -13,7 +31,11 @@ export default class PlaylistParser { artistId: artistId instanceof Array ? null : artistId, name: traverse(flexColumns[1], "runs", "text").at(-2) }, - songCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0), + videoCount: +traverse(flexColumns[1], "runs", "text") + .at(-1) + .split(" ") + .at(0) + .replaceAll(",", ""), thumbnails: [traverse(item, "thumbnails")].flat() } } diff --git a/src/parsers/VideoParser.ts b/src/parsers/VideoParser.ts index 6ff29a4..a59d529 100644 --- a/src/parsers/VideoParser.ts +++ b/src/parsers/VideoParser.ts @@ -4,12 +4,14 @@ import traverse from "../utils/traverse" export default class VideoParser { public static parseSearchResult(item: any): YTMusic.VideoDetailed { const flexColumns = traverse(item, "flexColumns") + const videoId = traverse(item, "playNavigationEndpoint", "videoId") return { type: "VIDEO", - videoId: traverse(item, "playNavigationEndpoint", "videoId"), + videoId: videoId instanceof Array ? null : videoId, name: traverse(flexColumns[0], "runs", "text"), - artists: traverse(flexColumns[1], "runs") + artists: [traverse(flexColumns[1], "runs")] + .flat() .filter((run: any) => "navigationEndpoint" in run) .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), views: Parser.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)), @@ -17,4 +19,21 @@ export default class VideoParser { thumbnails: [traverse(item, "thumbnails")].flat() } } + + public static parsePlaylistVideo(item: any): Omit { + const flexColumns = traverse(item, "flexColumns") + const videoId = traverse(item, "playNavigationEndpoint", "videoId") + + return { + type: "VIDEO", + videoId: videoId instanceof Array ? null : videoId, + name: traverse(flexColumns[0], "runs", "text"), + artists: [traverse(flexColumns[1], "runs")] + .flat() + .filter((run: any) => "navigationEndpoint" in run) + .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), + duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), + thumbnails: [traverse(item, "thumbnails")].flat() + } + } } diff --git a/src/tests/interfaces.ts b/src/tests/interfaces.ts index 271b1b8..28563fd 100644 --- a/src/tests/interfaces.ts +++ b/src/tests/interfaces.ts @@ -29,7 +29,7 @@ export const SONG_DETAILED: ObjectValidator = OBJECT({ export const VIDEO_DETAILED: ObjectValidator = OBJECT({ type: STRING("VIDEO"), - videoId: STRING(), + videoId: OR(STRING(), NULL()), name: STRING(), artists: LIST(ARTIST_BASIC), views: NUMBER(), @@ -91,6 +91,15 @@ export const PLAYLIST_DETAILED: ObjectValidator = OBJE playlistId: STRING(), name: STRING(), artist: ARTIST_BASIC, - songCount: NUMBER(), + videoCount: NUMBER(), + thumbnails: LIST(THUMBNAIL_FULL) +}) + +export const PLAYLIST_VIDEO: ObjectValidator> = OBJECT({ + type: STRING("VIDEO"), + videoId: OR(STRING(), NULL()), + name: STRING(), + artists: LIST(ARTIST_BASIC), + duration: NUMBER(), thumbnails: LIST(THUMBNAIL_FULL) }) diff --git a/src/tests/testing.ts b/src/tests/testing.ts index a8bb734..3a4c0bd 100644 --- a/src/tests/testing.ts +++ b/src/tests/testing.ts @@ -6,12 +6,13 @@ import { ARTIST_DETAILED, ARTIST_FULL, PLAYLIST_DETAILED, + PLAYLIST_VIDEO, SONG_DETAILED, VIDEO_DETAILED } from "./interfaces" import { LIST, validate } from "validate-any" -const queries = ["Lilac", "Weekend", "Yours Raiden", "Eminem", "IU"] +const queries = ["Lilac", "Weekend", "Yours Raiden", "Eminem", "Lisa Hannigan"] const ytmusic = new YTMusic() ytmusic.initialize().then(() => @@ -25,15 +26,17 @@ ytmusic.initialize().then(() => ytmusic.search(query) ]) - const [artist, artistSongs, artistAlbums, album] = await Promise.all([ - // ytmusic.getSong(songs[0].videoId), - // ytmusic.getVideo(videos[0].videoId), - ytmusic.getArtist(artists[0].artistId), - ytmusic.getArtistSongs(artists[0].artistId), - ytmusic.getArtistAlbums(artists[0].artistId), - ytmusic.getAlbum(albums[0].albumId) - // ytmusic.getPlaylist(playlists[0].playlistId) - ]) + const [artist, artistSongs, artistAlbums, album, playlist, playlistVideos] = + await Promise.all([ + // ytmusic.getSong(songs[0].videoId), + // ytmusic.getVideo(videos[0].videoId), + ytmusic.getArtist(artists[0].artistId), + ytmusic.getArtistSongs(artists[0].artistId), + ytmusic.getArtistAlbums(artists[0].artistId), + ytmusic.getAlbum(albums[0].albumId), + ytmusic.getPlaylist(playlists[0].playlistId), + ytmusic.getPlaylistVideos(playlists[0].playlistId) + ]) const tests: [any, Validator][] = [ [songs, LIST(SONG_DETAILED)], @@ -56,8 +59,9 @@ ytmusic.initialize().then(() => [artist, ARTIST_FULL], [artistSongs, LIST(SONG_DETAILED)], [artistAlbums, LIST(ALBUM_DETAILED)], - [album, ALBUM_FULL] - // [playlist, PLAYLIST_DETAILED] + [album, ALBUM_FULL], + [playlist, PLAYLIST_DETAILED], + [playlistVideos, LIST(PLAYLIST_VIDEO)] ] for (const [value, validator] of tests) { diff --git a/src/types.d.ts b/src/types.d.ts index 1212514..b200096 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -17,7 +17,7 @@ declare namespace YTMusic { interface VideoDetailed { type: "VIDEO" - videoId: string + videoId: string | null name: string artists: ArtistBasic[] views: number @@ -66,7 +66,7 @@ declare namespace YTMusic { playlistId: string name: string artist: ArtistBasic - songCount: number + videoCount: number thumbnails: ThumbnailFull[] }