Added more consistency

This commit is contained in:
Zechariah 2021-12-24 22:30:17 +08:00
parent f5b5047c31
commit cdbf3da1aa
11 changed files with 90 additions and 107 deletions

View File

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

View File

@ -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 => {
}) // })
}) })
}) })

View File

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

View File

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

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

View File

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

View File

@ -27,11 +27,7 @@ export default class SongParser {
} }
} }
public static parseArtistSongs(songsData: any, moreSongsData: any): YTMusic.SongDetailed[] { public static parseArtistSong(item: any): YTMusic.SongDetailed {
return [
...traverse(songsData, "musicResponsiveListItemRenderer"),
...traverse(moreSongsData, "musicResponsiveListItemRenderer")
].map((item: any) => {
const flexColumns = traverse(item, "flexColumns") const flexColumns = traverse(item, "flexColumns")
return { return {
@ -52,7 +48,6 @@ export default class SongParser {
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(

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 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")

View File

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