Moved methods all over, all artist methods work

This commit is contained in:
Zechariah 2021-12-24 18:11:38 +08:00
parent 779035391c
commit f5b5047c31
11 changed files with 147 additions and 100 deletions

View File

@ -240,11 +240,11 @@ export default class YTMusic {
return traverse(searchData, "musicResponsiveListItemRenderer").map( return traverse(searchData, "musicResponsiveListItemRenderer").map(
{ {
SONG: SongParser.parseSearch, SONG: SongParser.parseSearchResult,
VIDEO: VideoParser.parseSearch, VIDEO: VideoParser.parseSearchResult,
ARTIST: ArtistParser.parseSearch, ARTIST: ArtistParser.parseSearchResult,
ALBUM: AlbumParser.parseSearch, ALBUM: AlbumParser.parseSearchResult,
PLAYLIST: PlaylistParser.parseSearch PLAYLIST: PlaylistParser.parseSearchResult
}[category!] || SearchParser.parse }[category!] || SearchParser.parse
) )
} }
@ -261,12 +261,24 @@ export default class YTMusic {
fs.writeFileSync("data.json", JSON.stringify(data)) 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<YTMusic.ArtistFull> { public async getArtist(artistId: string): Promise<YTMusic.ArtistFull> {
const data = await this.constructRequest("browse", { browseId: artistId }) const data = await this.constructRequest("browse", { browseId: artistId })
return new ArtistParser(data).parse(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<YTMusic.SongDetailed[]> { public async getArtistSongs(artistId: string): Promise<YTMusic.SongDetailed[]> {
const artistData = await this.constructRequest("browse", { browseId: artistId }) const artistData = await this.constructRequest("browse", { browseId: artistId })
const browseToken = traverse(artistData, "musicShelfRenderer", "title", "browseId") const browseToken = traverse(artistData, "musicShelfRenderer", "title", "browseId")
@ -279,9 +291,15 @@ export default class YTMusic {
{ continuation: continueToken } { 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<YTMusic.AlbumDetailed[]> { public async getArtistAlbums(artistId: string): Promise<YTMusic.AlbumDetailed[]> {
const artistData = await this.constructRequest("browse", { browseId: artistId }) const artistData = await this.constructRequest("browse", { browseId: artistId })
const artistAlbumsData = traverse(artistData, "musicCarouselShelfRenderer")[0] const artistAlbumsData = traverse(artistData, "musicCarouselShelfRenderer")[0]
@ -289,7 +307,7 @@ export default class YTMusic {
const albumsData = await this.constructRequest("browse", browseBody) const albumsData = await this.constructRequest("browse", browseBody)
return ArtistParser.parseAlbums(artistId, albumsData) return AlbumParser.parseArtistAlbums(artistId, albumsData)
} }
public async getAlbum(albumId: string) { public async getAlbum(albumId: string) {
@ -303,4 +321,4 @@ export default class YTMusic {
fs.writeFileSync("data.json", JSON.stringify(data)) fs.writeFileSync("data.json", JSON.stringify(data))
} }
} }

View File

@ -2,7 +2,9 @@ import YTMusic from "./YTMusic"
const ytmusic = new YTMusic() const ytmusic = new YTMusic()
ytmusic.initialize().then(() => { ytmusic.initialize().then(() => {
ytmusic.search("Yours Raiden", "ALBUM").then(res => { ytmusic.search("Yours Raiden", "ARTIST").then(res => {
console.log(JSON.stringify(res, null, 4)) ytmusic.getArtistAlbums(res[0].artistId).then(res => {
})
}) })
}) })

View File

@ -1,8 +1,7 @@
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
import fs from "fs"
export default class AlbumParser { export default class AlbumParser {
public static parseSearch(item: any): YTMusic.AlbumDetailed { public static parseSearchResult(item: any): YTMusic.AlbumDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails") const thumbnails = traverse(item, "thumbnails")
@ -18,4 +17,36 @@ export default class AlbumParser {
thumbnails: [thumbnails].flat() 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()
}
}
} }

View File

@ -1,4 +1,6 @@
import AlbumParser from "./AlbumParser"
import Parse from "./Parser" import Parse from "./Parser"
import SongParser from "./SongParser"
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
export default class ArtistParser { export default class ArtistParser {
@ -8,7 +10,7 @@ export default class ArtistParser {
this.data = data this.data = data
} }
public static parseSearch(item: any): YTMusic.ArtistDetailed { public static parseSearchResult(item: any): YTMusic.ArtistDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails") const thumbnails = traverse(item, "thumbnails")
@ -26,81 +28,21 @@ export default class ArtistParser {
name: traverse(this.data, "header", "title", "text").at(0) name: traverse(this.data, "header", "title", "text").at(0)
} }
const description = traverse(this.data, "header", "description", "text")
return { return {
type: "ARTIST", type: "ARTIST",
...artistBasic, ...artistBasic,
thumbnails: traverse(this.data, "header", "thumbnails"), 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")), subscribers: Parse.parseNumber(traverse(this.data, "subscriberCountText", "text")),
topTracks: traverse(this.data, "musicShelfRenderer", "contents").map((item: any) => { topSongs: traverse(this.data, "musicShelfRenderer", "contents").map((item: any) =>
const flexColumns = traverse(item, "flexColumns") SongParser.parseArtistTopSong(item, artistBasic)
),
return { topAlbums: [traverse(this.data, "musicCarouselShelfRenderer", "contents")]
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")]
.flat() .flat()
.at(0) .at(0)
.contents.map((item: any) => ({ .map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic))
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()
}))
} }
} }
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()
}))
}
} }

View File

@ -1,7 +1,7 @@
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
export default class PlaylistParser { 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 flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails") const thumbnails = traverse(item, "thumbnails")
const artistId = traverse(flexColumns[1], "browseId") const artistId = traverse(flexColumns[1], "browseId")
@ -11,7 +11,7 @@ export default class PlaylistParser {
playlistId: traverse(item, "overlay", "playlistId"), playlistId: traverse(item, "overlay", "playlistId"),
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
artist: { artist: {
artistId: artistId instanceof Array ? undefined : artistId, artistId: artistId instanceof Array ? null : artistId,
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2) name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
}, },
trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0), trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0),

View File

@ -18,13 +18,13 @@ export default class SearchParser {
| "Playlist" | "Playlist"
return { return {
Song: () => SongParser.parseSearch(item), Song: () => SongParser.parseSearchResult(item),
Video: () => VideoParser.parseSearch(item, true), Video: () => VideoParser.parseSearchResult(item, true),
Artist: () => ArtistParser.parseSearch(item), Artist: () => ArtistParser.parseSearchResult(item),
EP: () => AlbumParser.parseSearch(item), EP: () => AlbumParser.parseSearchResult(item),
Single: () => AlbumParser.parseSearch(item), Single: () => AlbumParser.parseSearchResult(item),
Album: () => AlbumParser.parseSearch(item), Album: () => AlbumParser.parseSearchResult(item),
Playlist: () => PlaylistParser.parseSearch(item, true) Playlist: () => PlaylistParser.parseSearchResult(item, true)
}[type]() }[type]()
} }
} }

View File

@ -2,7 +2,7 @@ import Parser from "./Parser"
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
export default class SongParser { export default class SongParser {
public static parseSearch(item: any): YTMusic.SongDetailed { public static parseSearchResult(item: any): YTMusic.SongDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails") const thumbnails = traverse(item, "thumbnails")
@ -26,4 +26,51 @@ export default class SongParser {
thumbnails: thumbnails 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<YTMusic.SongDetailed, "duration"> {
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()
}
}
} }

View File

@ -2,7 +2,7 @@ import Parser from "./Parser"
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
export default class VideoParser { 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 flexColumns = traverse(item, "flexColumns")
const thumbnails = traverse(item, "thumbnails") const thumbnails = traverse(item, "thumbnails")

View File

@ -1,5 +1,5 @@
import ObjectValidator from "validate-any/build/validators/ObjectValidator" 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<YTMusic.ThumbnailFull> = OBJECT({ export const THUMBNAIL_FULL: ObjectValidator<YTMusic.ThumbnailFull> = OBJECT({
url: STRING(), url: STRING(),
@ -8,7 +8,7 @@ export const THUMBNAIL_FULL: ObjectValidator<YTMusic.ThumbnailFull> = OBJECT({
}) })
export const ARTIST_BASIC: ObjectValidator<YTMusic.ArtistBasic> = OBJECT({ export const ARTIST_BASIC: ObjectValidator<YTMusic.ArtistBasic> = OBJECT({
artistId: OR(STRING(), UNDEFINED()), artistId: OR(STRING(), NULL()),
name: STRING() name: STRING()
}) })
@ -59,9 +59,9 @@ export const ARTIST_FULL: ObjectValidator<YTMusic.ArtistFull> = OBJECT({
name: STRING(), name: STRING(),
type: STRING("ARTIST"), type: STRING("ARTIST"),
thumbnails: LIST(THUMBNAIL_FULL), thumbnails: LIST(THUMBNAIL_FULL),
description: STRING(), description: OR(STRING(), NULL()),
subscribers: NUMBER(), subscribers: NUMBER(),
topTracks: LIST( topSongs: LIST(
OBJECT({ OBJECT({
type: STRING("SONG"), type: STRING("SONG"),
videoId: STRING(), videoId: STRING(),

View File

@ -3,6 +3,7 @@ import YTMusic from "../YTMusic"
import { import {
ALBUM_DETAILED, ALBUM_DETAILED,
ARTIST_DETAILED, ARTIST_DETAILED,
ARTIST_FULL,
PLAYLIST_DETAILED, PLAYLIST_DETAILED,
SONG_DETAILED, SONG_DETAILED,
VIDEO_DETAILED VIDEO_DETAILED
@ -19,7 +20,13 @@ const tests: (query: string) => [() => Promise<any>, Validator<any>][] = query =
[ [
() => ytmusic.search(query), () => ytmusic.search(query),
LIST(ALBUM_DETAILED, ARTIST_DETAILED, PLAYLIST_DETAILED, SONG_DETAILED, VIDEO_DETAILED) 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"] const queries = ["Lilac", "Weekend", "Yours Raiden", "Eminem"]

6
src/types.d.ts vendored
View File

@ -26,7 +26,7 @@ declare namespace YTMusic {
} }
interface ArtistBasic { interface ArtistBasic {
artistId?: string artistId: string | null
name: string name: string
} }
@ -37,9 +37,9 @@ declare namespace YTMusic {
} }
interface ArtistFull extends ArtistDetailed { interface ArtistFull extends ArtistDetailed {
description: string description: string | null
subscribers: number subscribers: number
topTracks: Omit<SongDetailed, "duration">[] topSongs: Omit<SongDetailed, "duration">[]
topAlbums: AlbumDetailed[] topAlbums: AlbumDetailed[]
} }