From e3579a90b5336056bedf7e57786f45866d34c4b5 Mon Sep 17 00:00:00 2001 From: Zechariah Date: Sat, 25 Dec 2021 12:15:53 +0800 Subject: [PATCH] Fetching album by id works --- .vscode/launch.json | 10 ----- src/YTMusic.ts | 12 ++++-- src/index.ts | 8 ++-- src/parsers/AlbumParser.ts | 31 +++++++++++++- src/parsers/ArtistParser.ts | 8 ++-- src/parsers/PlaylistParser.ts | 5 +-- src/parsers/SongParser.ts | 31 +++++++++----- src/parsers/VideoParser.ts | 3 +- src/tests/interfaces.ts | 6 +-- src/tests/testing.ts | 76 ++++++++++++++++++++++++----------- src/tests/traverse/mine.ts | 2 +- src/types.d.ts | 6 +-- 12 files changed, 129 insertions(+), 69 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 9246a37..09f4896 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,16 +14,6 @@ "runtimeExecutable": "node", "runtimeArgs": ["--require", "ts-node/register/files"] }, - { - "type": "node", - "request": "launch", - "name": "YTMusic.ts", - "program": "${workspaceFolder}/src/YTMusic.ts", - "sourceMaps": true, - "skipFiles": ["/**"], - "runtimeExecutable": "node", - "runtimeArgs": ["--require", "ts-node/register/files"] - }, { "type": "node", "request": "launch", diff --git a/src/YTMusic.ts b/src/YTMusic.ts index 83036bc..23285f6 100644 --- a/src/YTMusic.ts +++ b/src/YTMusic.ts @@ -226,7 +226,7 @@ export default class YTMusic { }[category!] || null }) - return traverse(searchData, "musicResponsiveListItemRenderer").map( + return [traverse(searchData, "musicResponsiveListItemRenderer")].flat().map( { SONG: SongParser.parseSearchResult, VIDEO: VideoParser.parseSearchResult, @@ -306,10 +306,16 @@ export default class YTMusic { ) } - public async getAlbum(albumId: string) { + /** + * Get all possible information of an Album + * + * @param albumId Album ID + * @returns Album Data + */ + public async getAlbum(albumId: string): Promise { const data = await this.constructRequest("browse", { browseId: albumId }) - fs.writeFileSync("data.json", JSON.stringify(data)) + return AlbumParser.parse(data, albumId) } public async getPlaylist(playlistId: string) { diff --git a/src/index.ts b/src/index.ts index 1f7b32d..5666d0a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,9 @@ import YTMusic from "./YTMusic" const ytmusic = new YTMusic() ytmusic.initialize().then(() => { - ytmusic.search("Lilac").then(res => { - // ytmusic.getPlaylist(res[0].playlistId).then(res => { - - // }) + ytmusic.search("Lilac", "ALBUM").then(res => { + ytmusic.getAlbum(res[0].albumId).then(res => { + console.log(res) + }) }) }) diff --git a/src/parsers/AlbumParser.ts b/src/parsers/AlbumParser.ts index 03afa6c..12a51d9 100644 --- a/src/parsers/AlbumParser.ts +++ b/src/parsers/AlbumParser.ts @@ -1,9 +1,36 @@ +import SongParser from "./SongParser" import traverse from "../utils/traverse" export default class AlbumParser { + public static parse(data: any, albumId: string): YTMusic.AlbumFull { + const albumBasic = { + albumId, + name: traverse(data, "header", "title", "text").at(0) + } + const artists = traverse(data, "header", "subtitle", "runs") + .filter((run: any) => "navigationEndpoint" in run) + .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })) + const thumbnails = [traverse(data, "header", "thumbnails")].flat() + const description = traverse(data, "description", "text") + + return { + type: "ALBUM", + ...albumBasic, + playlistId: traverse(data, "buttonRenderer", "playlistId"), + artists, + year: +traverse(data, "header", "subtitle", "text").at(-1), + thumbnails, + description: description instanceof Array ? null : description, + songs: [traverse(data, "musicResponsiveListItemRenderer")] + .flat() + .map((item: any) => + SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails) + ) + } + } + public static parseSearchResult(item: any): YTMusic.AlbumDetailed { const flexColumns = traverse(item, "flexColumns") - const thumbnails = traverse(item, "thumbnails") return { type: "ALBUM", @@ -14,7 +41,7 @@ export default class AlbumParser { .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), name: traverse(flexColumns[0], "runs", "text"), year: +traverse(flexColumns[1], "runs", "text").at(-1), - thumbnails: [thumbnails].flat() + thumbnails: [traverse(item, "thumbnails")].flat() } } diff --git a/src/parsers/ArtistParser.ts b/src/parsers/ArtistParser.ts index 61af5a1..b320cc6 100644 --- a/src/parsers/ArtistParser.ts +++ b/src/parsers/ArtistParser.ts @@ -15,7 +15,7 @@ export default class ArtistParser { return { type: "ARTIST", ...artistBasic, - thumbnails: traverse(data, "header", "thumbnails"), + thumbnails: [traverse(data, "header", "thumbnails")].flat(), description: description instanceof Array ? null : description, subscribers: Parse.parseNumber(traverse(data, "subscriberCountText", "text")), topSongs: traverse(data, "musicShelfRenderer", "contents").map((item: any) => @@ -24,20 +24,18 @@ export default class ArtistParser { topAlbums: [traverse(data, "musicCarouselShelfRenderer")] .flat() .at(0) - .contents - .map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic)) + .contents.map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic)) } } public static parseSearchResult(item: any): YTMusic.ArtistDetailed { const flexColumns = traverse(item, "flexColumns") - const thumbnails = traverse(item, "thumbnails") return { type: "ARTIST", artistId: traverse(item, "browseId"), name: traverse(flexColumns[0], "runs", "text"), - thumbnails: [thumbnails].flat() + thumbnails: [traverse(item, "thumbnails")].flat() } } } diff --git a/src/parsers/PlaylistParser.ts b/src/parsers/PlaylistParser.ts index e07a5fc..ad648fe 100644 --- a/src/parsers/PlaylistParser.ts +++ b/src/parsers/PlaylistParser.ts @@ -3,7 +3,6 @@ import traverse from "../utils/traverse" export default class PlaylistParser { public static parseSearchResult(item: any): YTMusic.PlaylistDetailed { const flexColumns = traverse(item, "flexColumns") - const thumbnails = traverse(item, "thumbnails") const artistId = traverse(flexColumns[1], "browseId") return { @@ -14,8 +13,8 @@ export default class PlaylistParser { artistId: artistId instanceof Array ? null : artistId, name: traverse(flexColumns[1], "runs", "text").at(-2) }, - trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0), - thumbnails: [thumbnails].flat() + songCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0), + thumbnails: [traverse(item, "thumbnails")].flat() } } } diff --git a/src/parsers/SongParser.ts b/src/parsers/SongParser.ts index 197eca5..57f5570 100644 --- a/src/parsers/SongParser.ts +++ b/src/parsers/SongParser.ts @@ -4,26 +4,20 @@ import traverse from "../utils/traverse" export default class SongParser { public static parseSearchResult(item: any): YTMusic.SongDetailed { const flexColumns = traverse(item, "flexColumns") - const thumbnails = traverse(item, "thumbnails") return { type: "SONG", videoId: traverse(item, "playlistItemData", "videoId"), name: traverse(flexColumns[0], "runs", "text"), artists: traverse(flexColumns[1], "runs") - .map((run: any) => - "navigationEndpoint" in run - ? { name: run.text, artistId: traverse(run, "browseId") } - : null - ) - .slice(0, -3) - .filter(Boolean), + .filter((run: any) => "navigationEndpoint" in run) + .map((run: any) => ({ name: run.text, artistId: traverse(run, "browseId") })), album: { albumId: traverse(item, "browseId").at(-1), name: traverse(flexColumns[1], "runs", "text").at(-3) }, duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)), - thumbnails: thumbnails + thumbnails: [traverse(item, "thumbnails")].flat() } } @@ -68,4 +62,23 @@ export default class SongParser { thumbnails: [traverse(item, "thumbnails")].flat() } } + + public static parseAlbumSong( + item: any, + artists: YTMusic.ArtistBasic[], + albumBasic: YTMusic.AlbumBasic, + thumbnails: YTMusic.ThumbnailFull[] + ): YTMusic.SongDetailed { + const flexColumns = traverse(item, "flexColumns") + + return { + type: "SONG", + videoId: traverse(item, "playlistItemData", "videoId"), + name: traverse(flexColumns[0], "runs", "text"), + artists, + album: albumBasic, + duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), + thumbnails + } + } } diff --git a/src/parsers/VideoParser.ts b/src/parsers/VideoParser.ts index b6ddd02..6ff29a4 100644 --- a/src/parsers/VideoParser.ts +++ b/src/parsers/VideoParser.ts @@ -4,7 +4,6 @@ import traverse from "../utils/traverse" export default class VideoParser { public static parseSearchResult(item: any): YTMusic.VideoDetailed { const flexColumns = traverse(item, "flexColumns") - const thumbnails = traverse(item, "thumbnails") return { type: "VIDEO", @@ -15,7 +14,7 @@ export default class VideoParser { .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), views: Parser.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)), duration: Parser.parseDuration(traverse(flexColumns[1], "text").at(-1)), - thumbnails: [thumbnails].flat() + thumbnails: [traverse(item, "thumbnails")].flat() } } } diff --git a/src/tests/interfaces.ts b/src/tests/interfaces.ts index 8aab5bc..8e26a2c 100644 --- a/src/tests/interfaces.ts +++ b/src/tests/interfaces.ts @@ -82,8 +82,8 @@ export const ALBUM_FULL: ObjectValidator = OBJECT({ artists: LIST(ARTIST_BASIC), year: NUMBER(), thumbnails: LIST(THUMBNAIL_FULL), - description: STRING(), - tracks: LIST(SONG_DETAILED) + description: OR(STRING(), NULL()), + songs: LIST(SONG_DETAILED) }) export const PLAYLIST_DETAILED: ObjectValidator = OBJECT({ @@ -91,6 +91,6 @@ export const PLAYLIST_DETAILED: ObjectValidator = OBJE playlistId: STRING(), name: STRING(), artist: ARTIST_BASIC, - trackCount: NUMBER(), + songCount: NUMBER(), thumbnails: LIST(THUMBNAIL_FULL) }) diff --git a/src/tests/testing.ts b/src/tests/testing.ts index 4c74d9f..a8bb734 100644 --- a/src/tests/testing.ts +++ b/src/tests/testing.ts @@ -2,6 +2,7 @@ import Validator from "validate-any/build/classes/Validator" import YTMusic from "../YTMusic" import { ALBUM_DETAILED, + ALBUM_FULL, ARTIST_DETAILED, ARTIST_FULL, PLAYLIST_DETAILED, @@ -10,31 +11,56 @@ import { } from "./interfaces" import { LIST, validate } from "validate-any" +const queries = ["Lilac", "Weekend", "Yours Raiden", "Eminem", "IU"] const ytmusic = new YTMusic() -const tests: (query: string) => [() => Promise, Validator][] = query => [ - [() => ytmusic.search(query, "SONG"), LIST(SONG_DETAILED)], - [() => ytmusic.search(query, "VIDEO"), LIST(VIDEO_DETAILED)], - [() => ytmusic.search(query, "ARTIST"), LIST(ARTIST_DETAILED)], - [() => ytmusic.search(query, "ALBUM"), LIST(ALBUM_DETAILED)], - [() => ytmusic.search(query, "PLAYLIST"), LIST(PLAYLIST_DETAILED)], - [ - () => ytmusic.search(query), - LIST(ALBUM_DETAILED, ARTIST_DETAILED, PLAYLIST_DETAILED, SONG_DETAILED, VIDEO_DETAILED) - ], - [() => ytmusic.getArtist("UCUCF7BJBzLcu_6qvgSBk7dA"), ARTIST_FULL], - [() => ytmusic.getArtist("UCTUR0sVEkD8T5MlSHqgaI_Q"), ARTIST_FULL], - [() => ytmusic.getArtistSongs("UCUCF7BJBzLcu_6qvgSBk7dA"), LIST(SONG_DETAILED)], - [() => ytmusic.getArtistSongs("UCTUR0sVEkD8T5MlSHqgaI_Q"), LIST(SONG_DETAILED)], - [() => ytmusic.getArtistAlbums("UCUCF7BJBzLcu_6qvgSBk7dA"), LIST(ALBUM_DETAILED)], - [() => ytmusic.getArtistAlbums("UCTUR0sVEkD8T5MlSHqgaI_Q"), LIST(ALBUM_DETAILED)] -] -const queries = ["Lilac", "Weekend", "Yours Raiden", "Eminem"] +ytmusic.initialize().then(() => + queries.forEach(async query => { + const [songs, videos, artists, albums, playlists, results] = await Promise.all([ + ytmusic.search(query, "SONG"), + ytmusic.search(query, "VIDEO"), + ytmusic.search(query, "ARTIST"), + ytmusic.search(query, "ALBUM"), + ytmusic.search(query, "PLAYLIST"), + ytmusic.search(query) + ]) -ytmusic.initialize().then(async () => { - queries.forEach(query => { - tests(query).forEach(async ([fetch, validator]) => { - const value = await fetch() + 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 tests: [any, Validator][] = [ + [songs, LIST(SONG_DETAILED)], + [videos, LIST(VIDEO_DETAILED)], + [artists, LIST(ARTIST_DETAILED)], + [albums, LIST(ALBUM_DETAILED)], + [playlists, LIST(PLAYLIST_DETAILED)], + [ + results, + LIST( + ALBUM_DETAILED, + ARTIST_DETAILED, + PLAYLIST_DETAILED, + SONG_DETAILED, + VIDEO_DETAILED + ) + ], + // [song, SONG_DETAILED], + // [video, VIDEO_DETAILED], + [artist, ARTIST_FULL], + [artistSongs, LIST(SONG_DETAILED)], + [artistAlbums, LIST(ALBUM_DETAILED)], + [album, ALBUM_FULL] + // [playlist, PLAYLIST_DETAILED] + ] + + for (const [value, validator] of tests) { const result = validate(value, validator) if (!result.success) { console.log(JSON.stringify(value)) @@ -42,6 +68,8 @@ ytmusic.initialize().then(async () => { console.log(result.errors) process.exit(0) } - }) + } + + console.log(`Valid 🎉 - ${query}`) }) -}) +) diff --git a/src/tests/traverse/mine.ts b/src/tests/traverse/mine.ts index ef186f4..aa1c993 100644 --- a/src/tests/traverse/mine.ts +++ b/src/tests/traverse/mine.ts @@ -23,7 +23,7 @@ const traverse = (data: any, keys: string[], single: boolean = false) => { for (const key of keys) { value = again(value, key) } - + return value } diff --git a/src/types.d.ts b/src/types.d.ts index 35b0085..01bea28 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -57,8 +57,8 @@ declare namespace YTMusic { } interface AlbumFull extends AlbumDetailed { - description: string - tracks: SongDetailed[] + description: string | null + songs: SongDetailed[] } interface PlaylistDetailed { @@ -66,7 +66,7 @@ declare namespace YTMusic { playlistId: string name: string artist: ArtistBasic - trackCount: number + songCount: number thumbnails: ThumbnailFull[] }