This code is a mess

This commit is contained in:
Zechariah 2021-12-24 02:32:27 +08:00
parent 14ae240638
commit fe73e1d5ab
7 changed files with 361 additions and 186 deletions

View File

@ -11,7 +11,7 @@
"**/*.cs.meta": true,
"**/android": true,
"**/ios": true,
"**/node_modules": false,
"**/node_modules": true,
"**/__pycache__": true,
"**/babel.config.js": true,
"**/metro.config.js": true,

1
data.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,7 @@
import ArtistParser from "./utils/ArtistParser"
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 { Cookie, CookieJar } from "tough-cookie"
@ -198,11 +200,12 @@ export default class YTMusic {
* @returns Search suggestions
*/
public async getSearchSuggestions(query: string): Promise<string[]> {
const res = await this.constructRequest("music/get_search_suggestions", {
return traverse(
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): Promise<YTMusic.SearchResult[]>
public async search(query: string, category?: string) {
const data = await this.constructRequest("search", {
const searchData = await this.constructRequest("search", {
query: query,
params:
{
@ -231,20 +234,84 @@ export default class YTMusic {
}[category!] || null
})
const parser = new Parser(data)
const searchParser = new SearchParser(searchData)
return (
{
SONG: parser.parseSongsSearchResults,
VIDEO: parser.parseVideosSearchResults,
ARTIST: parser.parseArtistsSearchResults,
ALBUM: parser.parseAlbumsSearchResults,
PLAYLIST: parser.parsePlaylistsSearchResults
}[category!] || parser.parseSearchResult
).call(parser)
SONG: searchParser.parseSongs,
VIDEO: searchParser.parseVideos,
ARTIST: searchParser.parseArtists,
ALBUM: searchParser.parseAlbums,
PLAYLIST: searchParser.parsePlaylists
}[category!] || searchParser.parse
).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()
ytmusicapi.initialize().then(async () => {
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))
})

12
src/types.d.ts vendored
View File

@ -35,6 +35,13 @@ declare namespace YTMusic {
thumbnails: ThumbnailFull[]
}
interface ArtistFull extends ArtistDetailed {
description: string
subscribers: number
topTracks: (Omit<SongDetailed, "duration">)[]
topAlbums: AlbumDetailed[]
}
interface AlbumBasic {
albumId: string
name: string
@ -48,6 +55,11 @@ declare namespace YTMusic {
thumbnails: ThumbnailFull[]
}
interface AlbumFull extends AlbumDetailed {
description: string
tracks: []
}
interface PlaylistDetailed {
type: "PLAYLIST"
playlistId: string

94
src/utils/ArtistParser.ts Normal file
View File

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

View File

@ -1,175 +1,28 @@
import traverse from "./traverse"
const parseDuration = (time: string) => {
export default class Parse {
public static parseDuration(time: string) {
const [seconds, minutes, hours] = time
.split(":")
.reverse()
.map(n => +n) as (number | undefined)[]
return (seconds || 0) + (minutes || 0) * 60 + (hours || 0) * 60 * 60
}
}
const parseViews = (views: string): number => {
views = views.slice(0, -6)
if (views.at(-1)!.match(/^[A-Z]+$/)) {
const number = +views.slice(0, -1)
const multiplier = views.at(-1)
public static parseNumber(string: string): number {
if (string.at(-1)!.match(/^[A-Z]+$/)) {
const number = +string.slice(0, -1)
const multiplier = string.at(-1)
return (
{
K: number * 1000,
M: number * 1000 * 1000,
B: number * 1000 * 1000 * 1000
B: number * 1000 * 1000 * 1000,
T: number * 1000 * 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]()
})
return +string
}
}
}

148
src/utils/SearchParser.ts Normal file
View File

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