From f5b5047c314d0d14a86ee34c09b219eb7007a736 Mon Sep 17 00:00:00 2001 From: Zechariah Date: Fri, 24 Dec 2021 18:11:38 +0800 Subject: [PATCH] Moved methods all over, all artist methods work --- src/YTMusic.ts | 34 +++++++++++---- src/index.ts | 6 ++- src/parsers/AlbumParser.ts | 35 ++++++++++++++- src/parsers/ArtistParser.ts | 80 +++++------------------------------ src/parsers/PlaylistParser.ts | 4 +- src/parsers/SearchParser.ts | 14 +++--- src/parsers/SongParser.ts | 49 ++++++++++++++++++++- src/parsers/VideoParser.ts | 2 +- src/tests/interfaces.ts | 8 ++-- src/tests/testing.ts | 9 +++- src/types.d.ts | 6 +-- 11 files changed, 147 insertions(+), 100 deletions(-) diff --git a/src/YTMusic.ts b/src/YTMusic.ts index 9d078c0..c43ccc5 100644 --- a/src/YTMusic.ts +++ b/src/YTMusic.ts @@ -240,11 +240,11 @@ export default class YTMusic { return traverse(searchData, "musicResponsiveListItemRenderer").map( { - SONG: SongParser.parseSearch, - VIDEO: VideoParser.parseSearch, - ARTIST: ArtistParser.parseSearch, - ALBUM: AlbumParser.parseSearch, - PLAYLIST: PlaylistParser.parseSearch + SONG: SongParser.parseSearchResult, + VIDEO: VideoParser.parseSearchResult, + ARTIST: ArtistParser.parseSearchResult, + ALBUM: AlbumParser.parseSearchResult, + PLAYLIST: PlaylistParser.parseSearchResult }[category!] || SearchParser.parse ) } @@ -261,12 +261,24 @@ export default class YTMusic { fs.writeFileSync("data.json", JSON.stringify(data)) } + /** + * Get all possible information of an Artist + * + * @param artistId Artist ID + * @returns Artist Data + */ public async getArtist(artistId: string): Promise { const data = await this.constructRequest("browse", { browseId: artistId }) return new ArtistParser(data).parse(artistId) } + /** + * Get all of Artist's Songs + * + * @param artistId Artist ID + * @returns Artist's Songs + */ public async getArtistSongs(artistId: string): Promise { const artistData = await this.constructRequest("browse", { browseId: artistId }) const browseToken = traverse(artistData, "musicShelfRenderer", "title", "browseId") @@ -279,9 +291,15 @@ export default class YTMusic { { continuation: continueToken } ) - return ArtistParser.parseSongs(songsData, moreSongsData) + return SongParser.parseArtistSongs(songsData, moreSongsData) } + /** + * Get all of Artist's Albums + * + * @param artistId Artist ID + * @returns Artist's Albums + */ public async getArtistAlbums(artistId: string): Promise { const artistData = await this.constructRequest("browse", { browseId: artistId }) const artistAlbumsData = traverse(artistData, "musicCarouselShelfRenderer")[0] @@ -289,7 +307,7 @@ export default class YTMusic { const albumsData = await this.constructRequest("browse", browseBody) - return ArtistParser.parseAlbums(artistId, albumsData) + return AlbumParser.parseArtistAlbums(artistId, albumsData) } public async getAlbum(albumId: string) { @@ -303,4 +321,4 @@ export default class YTMusic { fs.writeFileSync("data.json", JSON.stringify(data)) } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index db9aa9b..24756d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,9 @@ import YTMusic from "./YTMusic" const ytmusic = new YTMusic() ytmusic.initialize().then(() => { - ytmusic.search("Yours Raiden", "ALBUM").then(res => { - console.log(JSON.stringify(res, null, 4)) + ytmusic.search("Yours Raiden", "ARTIST").then(res => { + ytmusic.getArtistAlbums(res[0].artistId).then(res => { + + }) }) }) diff --git a/src/parsers/AlbumParser.ts b/src/parsers/AlbumParser.ts index 149ec67..817100a 100644 --- a/src/parsers/AlbumParser.ts +++ b/src/parsers/AlbumParser.ts @@ -1,8 +1,7 @@ import traverse from "../utils/traverse" -import fs from "fs" export default class AlbumParser { - public static parseSearch(item: any): YTMusic.AlbumDetailed { + public static parseSearchResult(item: any): YTMusic.AlbumDetailed { const flexColumns = traverse(item, "flexColumns") const thumbnails = traverse(item, "thumbnails") @@ -18,4 +17,36 @@ export default class AlbumParser { thumbnails: [thumbnails].flat() } } + + public static parseArtistAlbums(artistId: string, albumsData: any): YTMusic.AlbumDetailed[] { + return traverse(albumsData, "musicTwoRowItemRenderer").map((item: any) => ({ + type: "ALBUM", + albumId: [traverse(item, "browseId")].flat().at(-1), + playlistId: traverse(item, "thumbnailOverlay", "playlistId"), + name: traverse(item, "title", "text").at(0), + artists: [ + { + artistId, + name: traverse(albumsData, "header", "text").at(0) + } + ], + year: +traverse(item, "subtitle", "text").at(-1), + thumbnails: [traverse(item, "thumbnails")].flat() + })) + } + + public static parseArtistTopAlbums( + item: any, + artistBasic: YTMusic.ArtistBasic + ): YTMusic.AlbumDetailed { + return { + type: "ALBUM", + albumId: traverse(item, "browseId").at(-1), + playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"), + name: traverse(item, "title", "text").at(0), + artists: [artistBasic], + year: +traverse(item, "subtitle", "text").at(-1), + thumbnails: [traverse(item, "thumbnails")].flat() + } + } } diff --git a/src/parsers/ArtistParser.ts b/src/parsers/ArtistParser.ts index 5c065b4..2fa9fb5 100644 --- a/src/parsers/ArtistParser.ts +++ b/src/parsers/ArtistParser.ts @@ -1,4 +1,6 @@ +import AlbumParser from "./AlbumParser" import Parse from "./Parser" +import SongParser from "./SongParser" import traverse from "../utils/traverse" export default class ArtistParser { @@ -8,7 +10,7 @@ export default class ArtistParser { this.data = data } - public static parseSearch(item: any): YTMusic.ArtistDetailed { + public static parseSearchResult(item: any): YTMusic.ArtistDetailed { const flexColumns = traverse(item, "flexColumns") const thumbnails = traverse(item, "thumbnails") @@ -26,81 +28,21 @@ export default class ArtistParser { name: traverse(this.data, "header", "title", "text").at(0) } + const description = traverse(this.data, "header", "description", "text") + return { type: "ARTIST", ...artistBasic, thumbnails: traverse(this.data, "header", "thumbnails"), - description: traverse(this.data, "header", "description", "text"), + description: description instanceof Array ? null : description, subscribers: Parse.parseNumber(traverse(this.data, "subscriberCountText", "text")), - topTracks: traverse(this.data, "musicShelfRenderer", "contents").map((item: any) => { - const flexColumns = traverse(item, "flexColumns") - - return { - type: "SONG", - videoId: traverse(item, "playlistItemData", "videoId"), - name: traverse(flexColumns[0], "runs", "text"), - artists: [artistBasic], - album: { - albumId: traverse(flexColumns[2], "runs", "text"), - name: traverse(flexColumns[2], "browseId") - }, - thumbnails: [traverse(item, "thumbnails")].flat() - } - }), - topAlbums: [traverse(this.data, "musicCarouselShelfRenderer")] + topSongs: traverse(this.data, "musicShelfRenderer", "contents").map((item: any) => + SongParser.parseArtistTopSong(item, artistBasic) + ), + topAlbums: [traverse(this.data, "musicCarouselShelfRenderer", "contents")] .flat() .at(0) - .contents.map((item: any) => ({ - type: "ALBUM", - albumId: traverse(item, "browseId").at(-1), - playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"), - name: traverse(item, "title", "text").at(0), - artists: [artistBasic], - year: +traverse(item, "subtitle", "text").at(-1), - thumbnails: [traverse(item, "thumbnails")].flat() - })) + .map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic)) } } - - public static parseSongs(songsData: any, moreSongsData: any): YTMusic.SongDetailed[] { - return [ - ...traverse(songsData, "musicResponsiveListItemRenderer"), - ...traverse(moreSongsData, "musicResponsiveListItemRenderer") - ].map((item: any) => { - const flexColumns = traverse(item, "flexColumns") - - return { - type: "SONG", - videoId: traverse(item, "playlistItemData", "videoId"), - name: traverse(flexColumns[0], "runs", "text"), - artists: [traverse(flexColumns[1], "runs")].flat().map((run: any) => ({ - name: run.text, - artistId: traverse(run, "browseId") - })), - album: { - albumId: traverse(flexColumns[2], "browseId"), - name: traverse(flexColumns[2], "runs", "text") - }, - duration: Parse.parseDuration(traverse(item, "fixedColumns", "runs", "text")), - thumbnails: [traverse(item, "thumbnails")].flat() - } - }) - } - - public static parseAlbums(artistId: string, albumsData: any): YTMusic.AlbumDetailed[] { - return traverse(albumsData, "musicTwoRowItemRenderer").map((item: any) => ({ - type: "ALBUM", - albumId: [traverse(item, "browseId")].flat().at(-1), - playlistId: traverse(item, "thumbnailOverlay", "playlistId"), - name: traverse(item, "title", "text").at(0), - artists: [ - { - artistId, - name: traverse(albumsData, "header", "text").at(0) - } - ], - year: +traverse(item, "subtitle", "text").at(-1), - thumbnails: [traverse(item, "thumbnails")].flat() - })) - } } diff --git a/src/parsers/PlaylistParser.ts b/src/parsers/PlaylistParser.ts index 1af89fe..b734f69 100644 --- a/src/parsers/PlaylistParser.ts +++ b/src/parsers/PlaylistParser.ts @@ -1,7 +1,7 @@ import traverse from "../utils/traverse" export default class PlaylistParser { - public static parseSearch(item: any, specific: boolean): YTMusic.PlaylistDetailed { + public static parseSearchResult(item: any, specific: boolean): YTMusic.PlaylistDetailed { const flexColumns = traverse(item, "flexColumns") const thumbnails = traverse(item, "thumbnails") const artistId = traverse(flexColumns[1], "browseId") @@ -11,7 +11,7 @@ export default class PlaylistParser { playlistId: traverse(item, "overlay", "playlistId"), name: traverse(flexColumns[0], "runs", "text"), artist: { - artistId: artistId instanceof Array ? undefined : artistId, + artistId: artistId instanceof Array ? null : artistId, name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2) }, trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0), diff --git a/src/parsers/SearchParser.ts b/src/parsers/SearchParser.ts index a715dea..245f7cf 100644 --- a/src/parsers/SearchParser.ts +++ b/src/parsers/SearchParser.ts @@ -18,13 +18,13 @@ export default class SearchParser { | "Playlist" return { - Song: () => SongParser.parseSearch(item), - Video: () => VideoParser.parseSearch(item, true), - Artist: () => ArtistParser.parseSearch(item), - EP: () => AlbumParser.parseSearch(item), - Single: () => AlbumParser.parseSearch(item), - Album: () => AlbumParser.parseSearch(item), - Playlist: () => PlaylistParser.parseSearch(item, true) + Song: () => SongParser.parseSearchResult(item), + Video: () => VideoParser.parseSearchResult(item, true), + Artist: () => ArtistParser.parseSearchResult(item), + EP: () => AlbumParser.parseSearchResult(item), + Single: () => AlbumParser.parseSearchResult(item), + Album: () => AlbumParser.parseSearchResult(item), + Playlist: () => PlaylistParser.parseSearchResult(item, true) }[type]() } } diff --git a/src/parsers/SongParser.ts b/src/parsers/SongParser.ts index 8ba2c1f..38e23a0 100644 --- a/src/parsers/SongParser.ts +++ b/src/parsers/SongParser.ts @@ -2,7 +2,7 @@ import Parser from "./Parser" import traverse from "../utils/traverse" export default class SongParser { - public static parseSearch(item: any): YTMusic.SongDetailed { + public static parseSearchResult(item: any): YTMusic.SongDetailed { const flexColumns = traverse(item, "flexColumns") const thumbnails = traverse(item, "thumbnails") @@ -26,4 +26,51 @@ export default class SongParser { thumbnails: thumbnails } } + + public static parseArtistSongs(songsData: any, moreSongsData: any): YTMusic.SongDetailed[] { + return [ + ...traverse(songsData, "musicResponsiveListItemRenderer"), + ...traverse(moreSongsData, "musicResponsiveListItemRenderer") + ].map((item: any) => { + const flexColumns = traverse(item, "flexColumns") + + return { + type: "SONG", + videoId: traverse(item, "playlistItemData", "videoId"), + name: traverse(flexColumns[0], "runs", "text"), + artists: [traverse(flexColumns[1], "runs")] + .flat() + .filter((item: any) => "navigationEndpoint" in item) + .map((run: any) => ({ + name: run.text, + artistId: traverse(run, "browseId") + })), + album: { + albumId: traverse(flexColumns[2], "browseId"), + name: traverse(flexColumns[2], "runs", "text") + }, + duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")), + thumbnails: [traverse(item, "thumbnails")].flat() + } + }) + } + + public static parseArtistTopSong( + item: any, + artistBasic: YTMusic.ArtistBasic + ): Omit { + const flexColumns = traverse(item, "flexColumns") + + return { + type: "SONG", + videoId: traverse(item, "playlistItemData", "videoId"), + name: traverse(flexColumns[0], "runs", "text"), + artists: [artistBasic], + album: { + albumId: traverse(flexColumns[2], "runs", "text"), + name: traverse(flexColumns[2], "browseId") + }, + thumbnails: [traverse(item, "thumbnails")].flat() + } + } } diff --git a/src/parsers/VideoParser.ts b/src/parsers/VideoParser.ts index c2ac1d9..4780742 100644 --- a/src/parsers/VideoParser.ts +++ b/src/parsers/VideoParser.ts @@ -2,7 +2,7 @@ import Parser from "./Parser" import traverse from "../utils/traverse" export default class VideoParser { - public static parseSearch(item: any, specific: boolean): YTMusic.VideoDetailed { + public static parseSearchResult(item: any, specific: boolean): YTMusic.VideoDetailed { const flexColumns = traverse(item, "flexColumns") const thumbnails = traverse(item, "thumbnails") diff --git a/src/tests/interfaces.ts b/src/tests/interfaces.ts index a6e59f1..8aab5bc 100644 --- a/src/tests/interfaces.ts +++ b/src/tests/interfaces.ts @@ -1,5 +1,5 @@ import ObjectValidator from "validate-any/build/validators/ObjectValidator" -import { LIST, NUMBER, OBJECT, OR, STRING, UNDEFINED } from "validate-any" +import { LIST, NULL, NUMBER, OBJECT, OR, STRING } from "validate-any" export const THUMBNAIL_FULL: ObjectValidator = OBJECT({ url: STRING(), @@ -8,7 +8,7 @@ export const THUMBNAIL_FULL: ObjectValidator = OBJECT({ }) export const ARTIST_BASIC: ObjectValidator = OBJECT({ - artistId: OR(STRING(), UNDEFINED()), + artistId: OR(STRING(), NULL()), name: STRING() }) @@ -59,9 +59,9 @@ export const ARTIST_FULL: ObjectValidator = OBJECT({ name: STRING(), type: STRING("ARTIST"), thumbnails: LIST(THUMBNAIL_FULL), - description: STRING(), + description: OR(STRING(), NULL()), subscribers: NUMBER(), - topTracks: LIST( + topSongs: LIST( OBJECT({ type: STRING("SONG"), videoId: STRING(), diff --git a/src/tests/testing.ts b/src/tests/testing.ts index 66c3ec1..4c74d9f 100644 --- a/src/tests/testing.ts +++ b/src/tests/testing.ts @@ -3,6 +3,7 @@ import YTMusic from "../YTMusic" import { ALBUM_DETAILED, ARTIST_DETAILED, + ARTIST_FULL, PLAYLIST_DETAILED, SONG_DETAILED, VIDEO_DETAILED @@ -19,7 +20,13 @@ const tests: (query: string) => [() => Promise, Validator][] = query = [ () => 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"] diff --git a/src/types.d.ts b/src/types.d.ts index 21adb4a..35b0085 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -26,7 +26,7 @@ declare namespace YTMusic { } interface ArtistBasic { - artistId?: string + artistId: string | null name: string } @@ -37,9 +37,9 @@ declare namespace YTMusic { } interface ArtistFull extends ArtistDetailed { - description: string + description: string | null subscribers: number - topTracks: Omit[] + topSongs: Omit[] topAlbums: AlbumDetailed[] }