Fetching album by id works

This commit is contained in:
Zechariah 2021-12-25 12:15:53 +08:00
parent cdbf3da1aa
commit e3579a90b5
12 changed files with 129 additions and 69 deletions

10
.vscode/launch.json vendored
View File

@ -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": ["<node_internals>/**"],
"runtimeExecutable": "node",
"runtimeArgs": ["--require", "ts-node/register/files"]
},
{
"type": "node",
"request": "launch",

View File

@ -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<YTMusic.AlbumFull> {
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) {

View File

@ -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)
})
})
})

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
}
}
}

View File

@ -82,8 +82,8 @@ export const ALBUM_FULL: ObjectValidator<YTMusic.AlbumFull> = 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<YTMusic.PlaylistDetailed> = OBJECT({
@ -91,6 +91,6 @@ export const PLAYLIST_DETAILED: ObjectValidator<YTMusic.PlaylistDetailed> = OBJE
playlistId: STRING(),
name: STRING(),
artist: ARTIST_BASIC,
trackCount: NUMBER(),
songCount: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})

View File

@ -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<any>, Validator<any>][] = 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.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)
])
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<any>][] = [
[songs, LIST(SONG_DETAILED)],
[videos, LIST(VIDEO_DETAILED)],
[artists, LIST(ARTIST_DETAILED)],
[albums, LIST(ALBUM_DETAILED)],
[playlists, LIST(PLAYLIST_DETAILED)],
[
() => ytmusic.search(query),
LIST(ALBUM_DETAILED, ARTIST_DETAILED, PLAYLIST_DETAILED, SONG_DETAILED, VIDEO_DETAILED)
results,
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)]
]
// [song, SONG_DETAILED],
// [video, VIDEO_DETAILED],
[artist, ARTIST_FULL],
[artistSongs, LIST(SONG_DETAILED)],
[artistAlbums, LIST(ALBUM_DETAILED)],
[album, ALBUM_FULL]
// [playlist, PLAYLIST_DETAILED]
]
const queries = ["Lilac", "Weekend", "Yours Raiden", "Eminem"]
ytmusic.initialize().then(async () => {
queries.forEach(query => {
tests(query).forEach(async ([fetch, validator]) => {
const value = await fetch()
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}`)
})
})
})
)

6
src/types.d.ts vendored
View File

@ -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[]
}