This code is a mess
This commit is contained in:
parent
14ae240638
commit
fe73e1d5ab
|
|
@ -11,7 +11,7 @@
|
||||||
"**/*.cs.meta": true,
|
"**/*.cs.meta": true,
|
||||||
"**/android": true,
|
"**/android": true,
|
||||||
"**/ios": true,
|
"**/ios": true,
|
||||||
"**/node_modules": false,
|
"**/node_modules": true,
|
||||||
"**/__pycache__": true,
|
"**/__pycache__": true,
|
||||||
"**/babel.config.js": true,
|
"**/babel.config.js": true,
|
||||||
"**/metro.config.js": true,
|
"**/metro.config.js": true,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import ArtistParser from "./utils/ArtistParser"
|
||||||
import axios, { AxiosInstance } from "axios"
|
import axios, { AxiosInstance } from "axios"
|
||||||
import Parser from "./utils/Parser"
|
import fs from "fs"
|
||||||
|
import SearchParser from "./utils/SearchParser"
|
||||||
import traverse from "./utils/traverse"
|
import traverse from "./utils/traverse"
|
||||||
import { Cookie, CookieJar } from "tough-cookie"
|
import { Cookie, CookieJar } from "tough-cookie"
|
||||||
|
|
||||||
|
|
@ -198,11 +200,12 @@ export default class YTMusic {
|
||||||
* @returns Search suggestions
|
* @returns Search suggestions
|
||||||
*/
|
*/
|
||||||
public async getSearchSuggestions(query: string): Promise<string[]> {
|
public async getSearchSuggestions(query: string): Promise<string[]> {
|
||||||
const res = await this.constructRequest("music/get_search_suggestions", {
|
return traverse(
|
||||||
input: query
|
await this.constructRequest("music/get_search_suggestions", {
|
||||||
})
|
input: query
|
||||||
|
}),
|
||||||
return traverse(res, "query")
|
"query"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -219,7 +222,7 @@ export default class YTMusic {
|
||||||
public async search(query: string, category: "PLAYLIST"): Promise<YTMusic.PlaylistDetailed[]>
|
public async search(query: string, category: "PLAYLIST"): Promise<YTMusic.PlaylistDetailed[]>
|
||||||
public async search(query: string): Promise<YTMusic.SearchResult[]>
|
public async search(query: string): Promise<YTMusic.SearchResult[]>
|
||||||
public async search(query: string, category?: string) {
|
public async search(query: string, category?: string) {
|
||||||
const data = await this.constructRequest("search", {
|
const searchData = await this.constructRequest("search", {
|
||||||
query: query,
|
query: query,
|
||||||
params:
|
params:
|
||||||
{
|
{
|
||||||
|
|
@ -231,20 +234,84 @@ export default class YTMusic {
|
||||||
}[category!] || null
|
}[category!] || null
|
||||||
})
|
})
|
||||||
|
|
||||||
const parser = new Parser(data)
|
const searchParser = new SearchParser(searchData)
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
SONG: parser.parseSongsSearchResults,
|
SONG: searchParser.parseSongs,
|
||||||
VIDEO: parser.parseVideosSearchResults,
|
VIDEO: searchParser.parseVideos,
|
||||||
ARTIST: parser.parseArtistsSearchResults,
|
ARTIST: searchParser.parseArtists,
|
||||||
ALBUM: parser.parseAlbumsSearchResults,
|
ALBUM: searchParser.parseAlbums,
|
||||||
PLAYLIST: parser.parsePlaylistsSearchResults
|
PLAYLIST: searchParser.parsePlaylists
|
||||||
}[category!] || parser.parseSearchResult
|
}[category!] || searchParser.parse
|
||||||
).call(parser)
|
).call(searchParser)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getSong(videoId: string) {
|
||||||
|
const data = await this.constructRequest("player", { videoId })
|
||||||
|
|
||||||
|
fs.writeFileSync("data.json", JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getVideo(videoId: string) {
|
||||||
|
const data = await this.constructRequest("player", { videoId })
|
||||||
|
|
||||||
|
fs.writeFileSync("data.json", JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getArtist(artistId: string): Promise<YTMusic.ArtistFull> {
|
||||||
|
const data = await this.constructRequest("browse", { browseId: artistId })
|
||||||
|
|
||||||
|
return new ArtistParser(data).parse(artistId)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getArtistSongs(artistId: string): Promise<YTMusic.SongDetailed[]> {
|
||||||
|
const artistData = await this.constructRequest("browse", { browseId: artistId })
|
||||||
|
const browseToken = traverse(artistData, "musicShelfRenderer", "title", "browseId")
|
||||||
|
|
||||||
|
const songsData = await this.constructRequest("browse", { browseId: browseToken })
|
||||||
|
const continueToken = traverse(songsData, "continuation")
|
||||||
|
const moreSongsData = await this.constructRequest(
|
||||||
|
"browse",
|
||||||
|
{},
|
||||||
|
{ continuation: continueToken }
|
||||||
|
)
|
||||||
|
|
||||||
|
return ArtistParser.parseSongs(songsData, moreSongsData)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getArtistAlbums(artistId: string): Promise<YTMusic.AlbumDetailed[]> {
|
||||||
|
const artistData = await this.constructRequest("browse", { browseId: artistId })
|
||||||
|
const artistAlbumsData = traverse(artistData, "musicCarouselShelfRenderer")[0]
|
||||||
|
const browseBody = traverse(artistAlbumsData, "moreContentButton", "browseEndpoint")
|
||||||
|
|
||||||
|
const albumsData = await this.constructRequest("browse", browseBody)
|
||||||
|
|
||||||
|
return ArtistParser.parseAlbums(artistId, albumsData)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAlbum(albumId: string) {
|
||||||
|
const data = await this.constructRequest("browse", { browseId: albumId })
|
||||||
|
|
||||||
|
fs.writeFileSync("data.json", JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPlaylist(playlistId: string) {
|
||||||
|
const data = await this.constructRequest("browse", { browseId: playlistId })
|
||||||
|
|
||||||
|
fs.writeFileSync("data.json", JSON.stringify(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ytmusicapi = new YTMusic()
|
const ytmusicapi = new YTMusic()
|
||||||
ytmusicapi.initialize().then(async () => {
|
ytmusicapi.initialize().then(async () => {
|
||||||
console.log("Initialized")
|
console.log("Initialized")
|
||||||
|
|
||||||
|
const artistDetailed = (await ytmusicapi.search("Roundworm", "ARTIST"))[0]
|
||||||
|
const artistFull = await ytmusicapi.getArtist(artistDetailed.artistId)
|
||||||
|
console.log(JSON.stringify(artistFull, null, 4))
|
||||||
|
|
||||||
|
// const artistDetailed = (await ytmusicapi.search("IU Lilac", "ARTIST"))[0]
|
||||||
|
// const albumDetailed = (await ytmusicapi.getArtistAlbums(artistDetailed.artistId))[0]
|
||||||
|
// const albumFull = await ytmusicapi.getAlbum(albumDetailed.albumId)
|
||||||
|
// console.log(JSON.stringify(albumFull))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ declare namespace YTMusic {
|
||||||
thumbnails: ThumbnailFull[]
|
thumbnails: ThumbnailFull[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ArtistFull extends ArtistDetailed {
|
||||||
|
description: string
|
||||||
|
subscribers: number
|
||||||
|
topTracks: (Omit<SongDetailed, "duration">)[]
|
||||||
|
topAlbums: AlbumDetailed[]
|
||||||
|
}
|
||||||
|
|
||||||
interface AlbumBasic {
|
interface AlbumBasic {
|
||||||
albumId: string
|
albumId: string
|
||||||
name: string
|
name: string
|
||||||
|
|
@ -48,6 +55,11 @@ declare namespace YTMusic {
|
||||||
thumbnails: ThumbnailFull[]
|
thumbnails: ThumbnailFull[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AlbumFull extends AlbumDetailed {
|
||||||
|
description: string
|
||||||
|
tracks: []
|
||||||
|
}
|
||||||
|
|
||||||
interface PlaylistDetailed {
|
interface PlaylistDetailed {
|
||||||
type: "PLAYLIST"
|
type: "PLAYLIST"
|
||||||
playlistId: string
|
playlistId: string
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
import Parse from "./Parser"
|
||||||
|
import traverse from "./traverse"
|
||||||
|
|
||||||
|
export default class ArtistParser {
|
||||||
|
private data: any
|
||||||
|
|
||||||
|
public constructor(data: any) {
|
||||||
|
this.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
public parse(artistId: string): YTMusic.ArtistFull {
|
||||||
|
const artistBasic: YTMusic.ArtistBasic = {
|
||||||
|
artistId,
|
||||||
|
name: traverse(this.data, "header", "title", "text").at(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "ARTIST",
|
||||||
|
...artistBasic,
|
||||||
|
thumbnails: traverse(this.data, "header", "thumbnails"),
|
||||||
|
description: traverse(this.data, "header", "description", "text"),
|
||||||
|
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")]
|
||||||
|
.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()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,175 +1,28 @@
|
||||||
import traverse from "./traverse"
|
export default class Parse {
|
||||||
|
public static parseDuration(time: string) {
|
||||||
|
const [seconds, minutes, hours] = time
|
||||||
|
.split(":")
|
||||||
|
.reverse()
|
||||||
|
.map(n => +n) as (number | undefined)[]
|
||||||
|
|
||||||
const parseDuration = (time: string) => {
|
return (seconds || 0) + (minutes || 0) * 60 + (hours || 0) * 60 * 60
|
||||||
const [seconds, minutes, hours] = time
|
}
|
||||||
.split(":")
|
|
||||||
.reverse()
|
|
||||||
.map(n => +n) as (number | undefined)[]
|
|
||||||
|
|
||||||
return (seconds || 0) + (minutes || 0) * 60 + (hours || 0) * 60 * 60
|
public static parseNumber(string: string): number {
|
||||||
}
|
if (string.at(-1)!.match(/^[A-Z]+$/)) {
|
||||||
|
const number = +string.slice(0, -1)
|
||||||
|
const multiplier = string.at(-1)
|
||||||
|
|
||||||
const parseViews = (views: string): number => {
|
return (
|
||||||
views = views.slice(0, -6)
|
{
|
||||||
|
K: number * 1000,
|
||||||
if (views.at(-1)!.match(/^[A-Z]+$/)) {
|
M: number * 1000 * 1000,
|
||||||
const number = +views.slice(0, -1)
|
B: number * 1000 * 1000 * 1000,
|
||||||
const multiplier = views.at(-1)
|
T: number * 1000 * 1000 * 1000 * 1000
|
||||||
|
}[multiplier!] || NaN
|
||||||
return (
|
)
|
||||||
{
|
} else {
|
||||||
K: number * 1000,
|
return +string
|
||||||
M: number * 1000 * 1000,
|
}
|
||||||
B: number * 1000 * 1000 * 1000
|
|
||||||
}[multiplier!] || NaN
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return +views
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Parser {
|
|
||||||
private items: any[]
|
|
||||||
|
|
||||||
constructor(data: any) {
|
|
||||||
this.items = [traverse(data, "musicResponsiveListItemRenderer")].flat()
|
|
||||||
}
|
|
||||||
|
|
||||||
public parseSongsSearchResults(): YTMusic.SongDetailed[] {
|
|
||||||
return this.items.map(item => this.parseSongSearchResult(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseSongSearchResult(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),
|
|
||||||
album: {
|
|
||||||
albumId: traverse(item, "browseId").at(-1),
|
|
||||||
name: traverse(flexColumns[1], "runs", "text").at(-3)
|
|
||||||
},
|
|
||||||
duration: parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
|
|
||||||
thumbnails: [thumbnails].flat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public parseVideosSearchResults(): YTMusic.VideoDetailed[] {
|
|
||||||
return this.items.map(item => this.parseVideoSearchResult(item, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseVideoSearchResult(item: any, specific: boolean): YTMusic.VideoDetailed {
|
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
|
||||||
const thumbnails = traverse(item, "thumbnails")
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "VIDEO",
|
|
||||||
videoId: traverse(item, "playNavigationEndpoint", "videoId"),
|
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
|
||||||
artist: {
|
|
||||||
artistId: traverse(flexColumns[1], "browseId"),
|
|
||||||
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
|
|
||||||
},
|
|
||||||
views: parseViews(traverse(flexColumns[1], "runs", "text").at(-3)),
|
|
||||||
duration: parseDuration(traverse(flexColumns[1], "text").at(-1)),
|
|
||||||
thumbnails: [thumbnails].flat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public parseArtistsSearchResults(): YTMusic.ArtistDetailed[] {
|
|
||||||
return this.items.map(item => this.parseArtistSearchResult(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseArtistSearchResult(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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public parseAlbumsSearchResults(): YTMusic.AlbumDetailed[] {
|
|
||||||
return this.items.map(item => this.parseAlbumSearchResult(item))
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseAlbumSearchResult(item: any): YTMusic.AlbumDetailed {
|
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
|
||||||
const thumbnails = traverse(item, "thumbnails")
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "ALBUM",
|
|
||||||
albumId: traverse(item, "browseId").at(-1),
|
|
||||||
playlistId: traverse(item, "overlay", "playlistId"),
|
|
||||||
artists: traverse(flexColumns[1], "runs")
|
|
||||||
.map((run: any) =>
|
|
||||||
"navigationEndpoint" in run
|
|
||||||
? { name: run.text, artistId: traverse(run, "browseId") }
|
|
||||||
: null
|
|
||||||
)
|
|
||||||
.slice(0, -1)
|
|
||||||
.filter(Boolean),
|
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
|
||||||
year: +traverse(flexColumns[1], "runs", "text").at(-1),
|
|
||||||
thumbnails: [thumbnails].flat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public parsePlaylistsSearchResults(): YTMusic.PlaylistDetailed[] {
|
|
||||||
return this.items.map(item => this.parsePlaylistSearchResult(item, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
private parsePlaylistSearchResult(item: any, specific: boolean): YTMusic.PlaylistDetailed {
|
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
|
||||||
const thumbnails = traverse(item, "thumbnails")
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "PLAYLIST",
|
|
||||||
playlistId: traverse(item, "overlay", "playlistId"),
|
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
|
||||||
artist: {
|
|
||||||
artistId: traverse(flexColumns[1], "browseId"),
|
|
||||||
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
|
|
||||||
},
|
|
||||||
trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0),
|
|
||||||
thumbnails: [thumbnails].flat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public parseSearchResult(): YTMusic.SearchResult[] {
|
|
||||||
return this.items.map(item => {
|
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
|
||||||
const type = traverse(flexColumns[1], "runs", "text").at(0) as
|
|
||||||
| "Song"
|
|
||||||
| "Video"
|
|
||||||
| "Artist"
|
|
||||||
| "EP"
|
|
||||||
| "Single"
|
|
||||||
| "Album"
|
|
||||||
| "Playlist"
|
|
||||||
|
|
||||||
return {
|
|
||||||
Song: () => this.parseSongSearchResult(item),
|
|
||||||
Video: () => this.parseVideoSearchResult(item, true),
|
|
||||||
Artist: () => this.parseArtistSearchResult(item),
|
|
||||||
EP: () => this.parseAlbumSearchResult(item),
|
|
||||||
Single: () => this.parseAlbumSearchResult(item),
|
|
||||||
Album: () => this.parseAlbumSearchResult(item),
|
|
||||||
Playlist: () => this.parsePlaylistSearchResult(item, true)
|
|
||||||
}[type]()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
import Parse from "./Parser"
|
||||||
|
import traverse from "./traverse"
|
||||||
|
|
||||||
|
export default class SearchParser {
|
||||||
|
private items: any[]
|
||||||
|
|
||||||
|
public constructor(data: any) {
|
||||||
|
this.items = [traverse(data, "musicResponsiveListItemRenderer")].flat()
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseSongs(): YTMusic.SongDetailed[] {
|
||||||
|
return this.items.map(item => this.parseSong(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSong(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),
|
||||||
|
album: {
|
||||||
|
albumId: traverse(item, "browseId").at(-1),
|
||||||
|
name: traverse(flexColumns[1], "runs", "text").at(-3)
|
||||||
|
},
|
||||||
|
duration: Parse.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
|
||||||
|
thumbnails: [thumbnails].flat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseVideos(): YTMusic.VideoDetailed[] {
|
||||||
|
return this.items.map(item => this.parseVideo(item, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseVideo(item: any, specific: boolean): YTMusic.VideoDetailed {
|
||||||
|
const flexColumns = traverse(item, "flexColumns")
|
||||||
|
const thumbnails = traverse(item, "thumbnails")
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "VIDEO",
|
||||||
|
videoId: traverse(item, "playNavigationEndpoint", "videoId"),
|
||||||
|
name: traverse(flexColumns[0], "runs", "text"),
|
||||||
|
artist: {
|
||||||
|
artistId: traverse(flexColumns[1], "browseId"),
|
||||||
|
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
|
||||||
|
},
|
||||||
|
views: Parse.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)),
|
||||||
|
duration: Parse.parseDuration(traverse(flexColumns[1], "text").at(-1)),
|
||||||
|
thumbnails: [thumbnails].flat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseArtists(): YTMusic.ArtistDetailed[] {
|
||||||
|
return this.items.map(item => this.parseArtist(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseArtist(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseAlbums(): YTMusic.AlbumDetailed[] {
|
||||||
|
return this.items.map(item => this.parseAlbum(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseAlbum(item: any): YTMusic.AlbumDetailed {
|
||||||
|
const flexColumns = traverse(item, "flexColumns")
|
||||||
|
const thumbnails = traverse(item, "thumbnails")
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "ALBUM",
|
||||||
|
albumId: traverse(item, "browseId").at(-1),
|
||||||
|
playlistId: traverse(item, "overlay", "playlistId"),
|
||||||
|
artists: traverse(flexColumns[1], "runs")
|
||||||
|
.map((run: any) =>
|
||||||
|
"navigationEndpoint" in run
|
||||||
|
? { name: run.text, artistId: traverse(run, "browseId") }
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
.slice(0, -1)
|
||||||
|
.filter(Boolean),
|
||||||
|
name: traverse(flexColumns[0], "runs", "text"),
|
||||||
|
year: +traverse(flexColumns[1], "runs", "text").at(-1),
|
||||||
|
thumbnails: [thumbnails].flat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public parsePlaylists(): YTMusic.PlaylistDetailed[] {
|
||||||
|
return this.items.map(item => this.parsePlaylist(item, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePlaylist(item: any, specific: boolean): YTMusic.PlaylistDetailed {
|
||||||
|
const flexColumns = traverse(item, "flexColumns")
|
||||||
|
const thumbnails = traverse(item, "thumbnails")
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "PLAYLIST",
|
||||||
|
playlistId: traverse(item, "overlay", "playlistId"),
|
||||||
|
name: traverse(flexColumns[0], "runs", "text"),
|
||||||
|
artist: {
|
||||||
|
artistId: traverse(flexColumns[1], "browseId"),
|
||||||
|
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
|
||||||
|
},
|
||||||
|
trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0),
|
||||||
|
thumbnails: [thumbnails].flat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public parse(): YTMusic.SearchResult[] {
|
||||||
|
return this.items.map(item => {
|
||||||
|
const flexColumns = traverse(item, "flexColumns")
|
||||||
|
const type = traverse(flexColumns[1], "runs", "text").at(0) as
|
||||||
|
| "Song"
|
||||||
|
| "Video"
|
||||||
|
| "Artist"
|
||||||
|
| "EP"
|
||||||
|
| "Single"
|
||||||
|
| "Album"
|
||||||
|
| "Playlist"
|
||||||
|
|
||||||
|
return {
|
||||||
|
Song: () => this.parseSong(item),
|
||||||
|
Video: () => this.parseVideo(item, true),
|
||||||
|
Artist: () => this.parseArtist(item),
|
||||||
|
EP: () => this.parseAlbum(item),
|
||||||
|
Single: () => this.parseAlbum(item),
|
||||||
|
Album: () => this.parseAlbum(item),
|
||||||
|
Playlist: () => this.parsePlaylist(item, true)
|
||||||
|
}[type]()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue