Added more consistency
This commit is contained in:
parent
f5b5047c31
commit
cdbf3da1aa
|
|
@ -93,19 +93,6 @@ export default class YTMusic {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Asserts that the API has been initialized
|
|
||||||
*
|
|
||||||
* @returns Non-null config
|
|
||||||
*/
|
|
||||||
private assertInitialized() {
|
|
||||||
if (!this.config) {
|
|
||||||
throw new Error("API not initialized. Make sure to call the initialize() method first")
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.config
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a basic YouTube Music API request with all essential headers
|
* Constructs a basic YouTube Music API request with all essential headers
|
||||||
* and body parameters needed to make the API work
|
* and body parameters needed to make the API work
|
||||||
|
|
@ -120,17 +107,19 @@ export default class YTMusic {
|
||||||
body: Record<string, any> = {},
|
body: Record<string, any> = {},
|
||||||
query: Record<string, string> = {}
|
query: Record<string, string> = {}
|
||||||
) {
|
) {
|
||||||
const config = this.assertInitialized()
|
if (!this.config) {
|
||||||
|
throw new Error("API not initialized. Make sure to call the initialize() method first")
|
||||||
|
}
|
||||||
|
|
||||||
const headers: Record<string, any> = {
|
const headers: Record<string, any> = {
|
||||||
...this.client.defaults.headers,
|
...this.client.defaults.headers,
|
||||||
"x-origin": this.client.defaults.baseURL,
|
"x-origin": this.client.defaults.baseURL,
|
||||||
"X-Goog-Visitor-Id": config.VISITOR_DATA,
|
"X-Goog-Visitor-Id": this.config.VISITOR_DATA,
|
||||||
"X-YouTube-Client-Name": config.INNERTUBE_CONTEXT_CLIENT_NAME,
|
"X-YouTube-Client-Name": this.config.INNERTUBE_CONTEXT_CLIENT_NAME,
|
||||||
"X-YouTube-Client-Version": config.INNERTUBE_CLIENT_VERSION,
|
"X-YouTube-Client-Version": this.config.INNERTUBE_CLIENT_VERSION,
|
||||||
"X-YouTube-Device": config.DEVICE,
|
"X-YouTube-Device": this.config.DEVICE,
|
||||||
"X-YouTube-Page-CL": config.PAGE_CL,
|
"X-YouTube-Page-CL": this.config.PAGE_CL,
|
||||||
"X-YouTube-Page-Label": config.PAGE_BUILD_LABEL,
|
"X-YouTube-Page-Label": this.config.PAGE_BUILD_LABEL,
|
||||||
"X-YouTube-Utc-Offset": String(-new Date().getTimezoneOffset()),
|
"X-YouTube-Utc-Offset": String(-new Date().getTimezoneOffset()),
|
||||||
"X-YouTube-Time-Zone": new Intl.DateTimeFormat().resolvedOptions().timeZone
|
"X-YouTube-Time-Zone": new Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
}
|
}
|
||||||
|
|
@ -138,22 +127,21 @@ export default class YTMusic {
|
||||||
const searchParams = new URLSearchParams({
|
const searchParams = new URLSearchParams({
|
||||||
...query,
|
...query,
|
||||||
alt: "json",
|
alt: "json",
|
||||||
key: config.INNERTUBE_API_KEY
|
key: this.config.INNERTUBE_API_KEY
|
||||||
})
|
})
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const res = await this.client.post(
|
const res = await this.client.post(
|
||||||
`youtubei/${config.INNERTUBE_API_VERSION}/${endpoint}?${searchParams.toString()}`,
|
`youtubei/${this.config.INNERTUBE_API_VERSION}/${endpoint}?${searchParams.toString()}`,
|
||||||
{
|
{
|
||||||
context: {
|
context: {
|
||||||
capabilities: {},
|
capabilities: {},
|
||||||
client: {
|
client: {
|
||||||
clientName: config.INNERTUBE_CLIENT_NAME,
|
clientName: this.config.INNERTUBE_CLIENT_NAME,
|
||||||
clientVersion: config.INNERTUBE_CLIENT_VERSION,
|
clientVersion: this.config.INNERTUBE_CLIENT_VERSION,
|
||||||
experimentIds: [],
|
experimentIds: [],
|
||||||
experimentsToken: "",
|
experimentsToken: "",
|
||||||
gl: config.GL,
|
gl: this.config.GL,
|
||||||
hl: config.HL,
|
hl: this.config.HL,
|
||||||
locationInfo: {
|
locationInfo: {
|
||||||
locationPermissionAuthorizationStatus:
|
locationPermissionAuthorizationStatus:
|
||||||
"LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED"
|
"LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED"
|
||||||
|
|
@ -270,7 +258,7 @@ export default class YTMusic {
|
||||||
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 ArtistParser.parse(data, artistId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -291,7 +279,10 @@ export default class YTMusic {
|
||||||
{ continuation: continueToken }
|
{ continuation: continueToken }
|
||||||
)
|
)
|
||||||
|
|
||||||
return SongParser.parseArtistSongs(songsData, moreSongsData)
|
return [
|
||||||
|
...traverse(songsData, "musicResponsiveListItemRenderer"),
|
||||||
|
...traverse(moreSongsData, "musicResponsiveListItemRenderer")
|
||||||
|
].map(SongParser.parseArtistSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -307,7 +298,12 @@ export default class YTMusic {
|
||||||
|
|
||||||
const albumsData = await this.constructRequest("browse", browseBody)
|
const albumsData = await this.constructRequest("browse", browseBody)
|
||||||
|
|
||||||
return AlbumParser.parseArtistAlbums(artistId, albumsData)
|
return traverse(albumsData, "musicTwoRowItemRenderer").map((item: any) =>
|
||||||
|
AlbumParser.parseArtistAlbum(item, {
|
||||||
|
artistId,
|
||||||
|
name: traverse(albumsData, "header", "text").at(0)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAlbum(albumId: string) {
|
public async getAlbum(albumId: string) {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import YTMusic from "./YTMusic"
|
||||||
|
|
||||||
const ytmusic = new YTMusic()
|
const ytmusic = new YTMusic()
|
||||||
ytmusic.initialize().then(() => {
|
ytmusic.initialize().then(() => {
|
||||||
ytmusic.search("Yours Raiden", "ARTIST").then(res => {
|
ytmusic.search("Lilac").then(res => {
|
||||||
ytmusic.getArtistAlbums(res[0].artistId).then(res => {
|
// ytmusic.getPlaylist(res[0].playlistId).then(res => {
|
||||||
|
|
||||||
})
|
// })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,19 @@ export default class AlbumParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistAlbums(artistId: string, albumsData: any): YTMusic.AlbumDetailed[] {
|
public static parseArtistAlbum(
|
||||||
return traverse(albumsData, "musicTwoRowItemRenderer").map((item: any) => ({
|
item: any,
|
||||||
|
artistBasic: YTMusic.ArtistBasic
|
||||||
|
): YTMusic.AlbumDetailed {
|
||||||
|
return {
|
||||||
type: "ALBUM",
|
type: "ALBUM",
|
||||||
albumId: [traverse(item, "browseId")].flat().at(-1),
|
albumId: [traverse(item, "browseId")].flat().at(-1),
|
||||||
playlistId: traverse(item, "thumbnailOverlay", "playlistId"),
|
playlistId: traverse(item, "thumbnailOverlay", "playlistId"),
|
||||||
name: traverse(item, "title", "text").at(0),
|
name: traverse(item, "title", "text").at(0),
|
||||||
artists: [
|
artists: [artistBasic],
|
||||||
{
|
|
||||||
artistId,
|
|
||||||
name: traverse(albumsData, "header", "text").at(0)
|
|
||||||
}
|
|
||||||
],
|
|
||||||
year: +traverse(item, "subtitle", "text").at(-1),
|
year: +traverse(item, "subtitle", "text").at(-1),
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||||
}))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistTopAlbums(
|
public static parseArtistTopAlbums(
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,29 @@ import SongParser from "./SongParser"
|
||||||
import traverse from "../utils/traverse"
|
import traverse from "../utils/traverse"
|
||||||
|
|
||||||
export default class ArtistParser {
|
export default class ArtistParser {
|
||||||
private data: any
|
public static parse(data: any, artistId: string): YTMusic.ArtistFull {
|
||||||
|
const artistBasic = {
|
||||||
|
artistId,
|
||||||
|
name: traverse(data, "header", "title", "text").at(0)
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(data: any) {
|
const description = traverse(data, "header", "description", "text")
|
||||||
this.data = data
|
|
||||||
|
return {
|
||||||
|
type: "ARTIST",
|
||||||
|
...artistBasic,
|
||||||
|
thumbnails: traverse(data, "header", "thumbnails"),
|
||||||
|
description: description instanceof Array ? null : description,
|
||||||
|
subscribers: Parse.parseNumber(traverse(data, "subscriberCountText", "text")),
|
||||||
|
topSongs: traverse(data, "musicShelfRenderer", "contents").map((item: any) =>
|
||||||
|
SongParser.parseArtistTopSong(item, artistBasic)
|
||||||
|
),
|
||||||
|
topAlbums: [traverse(data, "musicCarouselShelfRenderer")]
|
||||||
|
.flat()
|
||||||
|
.at(0)
|
||||||
|
.contents
|
||||||
|
.map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): YTMusic.ArtistDetailed {
|
public static parseSearchResult(item: any): YTMusic.ArtistDetailed {
|
||||||
|
|
@ -21,28 +40,4 @@ export default class ArtistParser {
|
||||||
thumbnails: [thumbnails].flat()
|
thumbnails: [thumbnails].flat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public parse(artistId: string): YTMusic.ArtistFull {
|
|
||||||
const artistBasic = {
|
|
||||||
artistId,
|
|
||||||
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: description instanceof Array ? null : description,
|
|
||||||
subscribers: Parse.parseNumber(traverse(this.data, "subscriberCountText", "text")),
|
|
||||||
topSongs: traverse(this.data, "musicShelfRenderer", "contents").map((item: any) =>
|
|
||||||
SongParser.parseArtistTopSong(item, artistBasic)
|
|
||||||
),
|
|
||||||
topAlbums: [traverse(this.data, "musicCarouselShelfRenderer", "contents")]
|
|
||||||
.flat()
|
|
||||||
.at(0)
|
|
||||||
.map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import traverse from "../utils/traverse"
|
import traverse from "../utils/traverse"
|
||||||
|
|
||||||
export default class PlaylistParser {
|
export default class PlaylistParser {
|
||||||
public static parseSearchResult(item: any, specific: boolean): YTMusic.PlaylistDetailed {
|
public static parseSearchResult(item: any): 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")
|
||||||
|
|
@ -12,7 +12,7 @@ export default class PlaylistParser {
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
name: traverse(flexColumns[0], "runs", "text"),
|
||||||
artist: {
|
artist: {
|
||||||
artistId: artistId instanceof Array ? null : artistId,
|
artistId: artistId instanceof Array ? null : artistId,
|
||||||
name: traverse(flexColumns[1], "runs", "text").at(specific ? 0 : 2)
|
name: traverse(flexColumns[1], "runs", "text").at(-2)
|
||||||
},
|
},
|
||||||
trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0),
|
trackCount: +traverse(flexColumns[1], "runs", "text").at(-1).split(" ").at(0),
|
||||||
thumbnails: [thumbnails].flat()
|
thumbnails: [thumbnails].flat()
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@ export default class SearchParser {
|
||||||
| "Playlist"
|
| "Playlist"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Song: () => SongParser.parseSearchResult(item),
|
Song: SongParser.parseSearchResult,
|
||||||
Video: () => VideoParser.parseSearchResult(item, true),
|
Video: VideoParser.parseSearchResult,
|
||||||
Artist: () => ArtistParser.parseSearchResult(item),
|
Artist: ArtistParser.parseSearchResult,
|
||||||
EP: () => AlbumParser.parseSearchResult(item),
|
EP: AlbumParser.parseSearchResult,
|
||||||
Single: () => AlbumParser.parseSearchResult(item),
|
Single: AlbumParser.parseSearchResult,
|
||||||
Album: () => AlbumParser.parseSearchResult(item),
|
Album: AlbumParser.parseSearchResult,
|
||||||
Playlist: () => PlaylistParser.parseSearchResult(item, true)
|
Playlist: PlaylistParser.parseSearchResult
|
||||||
}[type]()
|
}[type](item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,32 +27,27 @@ export default class SongParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistSongs(songsData: any, moreSongsData: any): YTMusic.SongDetailed[] {
|
public static parseArtistSong(item: any): YTMusic.SongDetailed {
|
||||||
return [
|
const flexColumns = traverse(item, "flexColumns")
|
||||||
...traverse(songsData, "musicResponsiveListItemRenderer"),
|
|
||||||
...traverse(moreSongsData, "musicResponsiveListItemRenderer")
|
|
||||||
].map((item: any) => {
|
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "SONG",
|
type: "SONG",
|
||||||
videoId: traverse(item, "playlistItemData", "videoId"),
|
videoId: traverse(item, "playlistItemData", "videoId"),
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
name: traverse(flexColumns[0], "runs", "text"),
|
||||||
artists: [traverse(flexColumns[1], "runs")]
|
artists: [traverse(flexColumns[1], "runs")]
|
||||||
.flat()
|
.flat()
|
||||||
.filter((item: any) => "navigationEndpoint" in item)
|
.filter((item: any) => "navigationEndpoint" in item)
|
||||||
.map((run: any) => ({
|
.map((run: any) => ({
|
||||||
name: run.text,
|
name: run.text,
|
||||||
artistId: traverse(run, "browseId")
|
artistId: traverse(run, "browseId")
|
||||||
})),
|
})),
|
||||||
album: {
|
album: {
|
||||||
albumId: traverse(flexColumns[2], "browseId"),
|
albumId: traverse(flexColumns[2], "browseId"),
|
||||||
name: traverse(flexColumns[2], "runs", "text")
|
name: traverse(flexColumns[2], "runs", "text")
|
||||||
},
|
},
|
||||||
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
|
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistTopSong(
|
public static parseArtistTopSong(
|
||||||
|
|
|
||||||
|
|
@ -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 parseSearchResult(item: any, specific: boolean): YTMusic.VideoDetailed {
|
public static parseSearchResult(item: any): YTMusic.VideoDetailed {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverse(item, "flexColumns")
|
||||||
const thumbnails = traverse(item, "thumbnails")
|
const thumbnails = traverse(item, "thumbnails")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ const traverse = (data: any, keys: string[], single: boolean = false) => {
|
||||||
let res = []
|
let res = []
|
||||||
|
|
||||||
if (data instanceof Object && key in data) {
|
if (data instanceof Object && key in data) {
|
||||||
if (single) return data[key]
|
|
||||||
res.push(data[key])
|
res.push(data[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -17,7 +16,7 @@ const traverse = (data: any, keys: string[], single: boolean = false) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res.length === 1 ? res[0] : res
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = data
|
let value = data
|
||||||
Loading…
Reference in New Issue