Fetching playlist by id works

This commit is contained in:
Zechariah 2021-12-25 17:06:16 +08:00
parent 10c15a85af
commit 7471f1e70c
6 changed files with 112 additions and 22 deletions

View File

@ -318,9 +318,45 @@ export default class YTMusic {
return AlbumParser.parse(data, albumId) 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<YTMusic.PlaylistDetailed> {
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
const data = await this.constructRequest("browse", { browseId: 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<Omit<YTMusic.VideoDetailed, "views">[]> {
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)
} }
} }

View File

@ -1,6 +1,24 @@
import traverse from "../utils/traverse" import traverse from "../utils/traverse"
export default class PlaylistParser { 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 { public static parseSearchResult(item: any): YTMusic.PlaylistDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const artistId = traverse(flexColumns[1], "browseId") const artistId = traverse(flexColumns[1], "browseId")
@ -13,7 +31,11 @@ export default class PlaylistParser {
artistId: artistId instanceof Array ? null : artistId, artistId: artistId instanceof Array ? null : artistId,
name: traverse(flexColumns[1], "runs", "text").at(-2) 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() thumbnails: [traverse(item, "thumbnails")].flat()
} }
} }

View File

@ -4,12 +4,14 @@ import traverse from "../utils/traverse"
export default class VideoParser { export default class VideoParser {
public static parseSearchResult(item: any): YTMusic.VideoDetailed { public static parseSearchResult(item: any): YTMusic.VideoDetailed {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
const videoId = traverse(item, "playNavigationEndpoint", "videoId")
return { return {
type: "VIDEO", type: "VIDEO",
videoId: traverse(item, "playNavigationEndpoint", "videoId"), videoId: videoId instanceof Array ? null : videoId,
name: traverse(flexColumns[0], "runs", "text"), name: traverse(flexColumns[0], "runs", "text"),
artists: traverse(flexColumns[1], "runs") artists: [traverse(flexColumns[1], "runs")]
.flat()
.filter((run: any) => "navigationEndpoint" in run) .filter((run: any) => "navigationEndpoint" in run)
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })), .map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
views: Parser.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)), 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() thumbnails: [traverse(item, "thumbnails")].flat()
} }
} }
public static parsePlaylistVideo(item: any): Omit<YTMusic.VideoDetailed, "views"> {
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()
}
}
} }

View File

@ -29,7 +29,7 @@ export const SONG_DETAILED: ObjectValidator<YTMusic.SongDetailed> = OBJECT({
export const VIDEO_DETAILED: ObjectValidator<YTMusic.VideoDetailed> = OBJECT({ export const VIDEO_DETAILED: ObjectValidator<YTMusic.VideoDetailed> = OBJECT({
type: STRING("VIDEO"), type: STRING("VIDEO"),
videoId: STRING(), videoId: OR(STRING(), NULL()),
name: STRING(), name: STRING(),
artists: LIST(ARTIST_BASIC), artists: LIST(ARTIST_BASIC),
views: NUMBER(), views: NUMBER(),
@ -91,6 +91,15 @@ export const PLAYLIST_DETAILED: ObjectValidator<YTMusic.PlaylistDetailed> = OBJE
playlistId: STRING(), playlistId: STRING(),
name: STRING(), name: STRING(),
artist: ARTIST_BASIC, artist: ARTIST_BASIC,
songCount: NUMBER(), videoCount: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL)
})
export const PLAYLIST_VIDEO: ObjectValidator<Omit<YTMusic.VideoDetailed, "views">> = OBJECT({
type: STRING("VIDEO"),
videoId: OR(STRING(), NULL()),
name: STRING(),
artists: LIST(ARTIST_BASIC),
duration: NUMBER(),
thumbnails: LIST(THUMBNAIL_FULL) thumbnails: LIST(THUMBNAIL_FULL)
}) })

View File

@ -6,12 +6,13 @@ import {
ARTIST_DETAILED, ARTIST_DETAILED,
ARTIST_FULL, ARTIST_FULL,
PLAYLIST_DETAILED, PLAYLIST_DETAILED,
PLAYLIST_VIDEO,
SONG_DETAILED, SONG_DETAILED,
VIDEO_DETAILED VIDEO_DETAILED
} from "./interfaces" } from "./interfaces"
import { LIST, validate } from "validate-any" 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() const ytmusic = new YTMusic()
ytmusic.initialize().then(() => ytmusic.initialize().then(() =>
@ -25,14 +26,16 @@ ytmusic.initialize().then(() =>
ytmusic.search(query) ytmusic.search(query)
]) ])
const [artist, artistSongs, artistAlbums, album] = await Promise.all([ const [artist, artistSongs, artistAlbums, album, playlist, playlistVideos] =
await Promise.all([
// ytmusic.getSong(songs[0].videoId), // ytmusic.getSong(songs[0].videoId),
// ytmusic.getVideo(videos[0].videoId), // ytmusic.getVideo(videos[0].videoId),
ytmusic.getArtist(artists[0].artistId), ytmusic.getArtist(artists[0].artistId),
ytmusic.getArtistSongs(artists[0].artistId), ytmusic.getArtistSongs(artists[0].artistId),
ytmusic.getArtistAlbums(artists[0].artistId), ytmusic.getArtistAlbums(artists[0].artistId),
ytmusic.getAlbum(albums[0].albumId) ytmusic.getAlbum(albums[0].albumId),
// ytmusic.getPlaylist(playlists[0].playlistId) ytmusic.getPlaylist(playlists[0].playlistId),
ytmusic.getPlaylistVideos(playlists[0].playlistId)
]) ])
const tests: [any, Validator<any>][] = [ const tests: [any, Validator<any>][] = [
@ -56,8 +59,9 @@ ytmusic.initialize().then(() =>
[artist, ARTIST_FULL], [artist, ARTIST_FULL],
[artistSongs, LIST(SONG_DETAILED)], [artistSongs, LIST(SONG_DETAILED)],
[artistAlbums, LIST(ALBUM_DETAILED)], [artistAlbums, LIST(ALBUM_DETAILED)],
[album, ALBUM_FULL] [album, ALBUM_FULL],
// [playlist, PLAYLIST_DETAILED] [playlist, PLAYLIST_DETAILED],
[playlistVideos, LIST(PLAYLIST_VIDEO)]
] ]
for (const [value, validator] of tests) { for (const [value, validator] of tests) {

4
src/types.d.ts vendored
View File

@ -17,7 +17,7 @@ declare namespace YTMusic {
interface VideoDetailed { interface VideoDetailed {
type: "VIDEO" type: "VIDEO"
videoId: string videoId: string | null
name: string name: string
artists: ArtistBasic[] artists: ArtistBasic[]
views: number views: number
@ -66,7 +66,7 @@ declare namespace YTMusic {
playlistId: string playlistId: string
name: string name: string
artist: ArtistBasic artist: ArtistBasic
songCount: number videoCount: number
thumbnails: ThumbnailFull[] thumbnails: ThumbnailFull[]
} }