Initial Commit - Searching works
This commit is contained in:
		
						commit
						14ae240638
					
				|  | @ -0,0 +1,3 @@ | |||
| [*] | ||||
| indent_size = 4 | ||||
| indent_style = tab | ||||
|  | @ -0,0 +1,6 @@ | |||
| /node_modules | ||||
| /.idea | ||||
| /build | ||||
| config.json | ||||
| *.test.ts | ||||
| .rest | ||||
|  | @ -0,0 +1,8 @@ | |||
| { | ||||
| 	"semi": false, | ||||
| 	"arrowParens": "avoid", | ||||
| 	"trailingComma": "none", | ||||
| 	"jsxSingleQuote": false, | ||||
| 	"jsxBracketSameLine": true, | ||||
| 	"printWidth": 100 | ||||
| } | ||||
|  | @ -0,0 +1,28 @@ | |||
| { | ||||
|     // Use IntelliSense to learn about possible attributes. | ||||
|     // Hover to view descriptions of existing attributes. | ||||
|     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||
|     "version": "0.2.0", | ||||
|     "configurations": [ | ||||
|         { | ||||
| 			"type": "node", | ||||
| 			"request": "launch", | ||||
| 			"name": "index.ts", | ||||
| 			"program": "${workspaceFolder}/src/index.ts", | ||||
| 			"sourceMaps": true, | ||||
| 			"skipFiles": ["<node_internals>/**"], | ||||
| 			"runtimeExecutable": "node", | ||||
| 			"runtimeArgs": ["--require", "ts-node/register/files"] | ||||
| 		}, | ||||
| 		{ | ||||
| 			"type": "node", | ||||
| 			"request": "launch", | ||||
| 			"name": "YTMusic.ts", | ||||
| 			"program": "${workspaceFolder}/src/YTMusic.ts", | ||||
| 			"sourceMaps": true, | ||||
| 			"skipFiles": ["<node_internals>/**"], | ||||
| 			"runtimeExecutable": "node", | ||||
| 			"runtimeArgs": ["--require", "ts-node/register/files"] | ||||
| 		} | ||||
|     ] | ||||
| } | ||||
|  | @ -0,0 +1,22 @@ | |||
| { | ||||
|     "files.exclude": { | ||||
|         "**/.git": true, | ||||
|         "**/.svn": true, | ||||
|         "**/.hg": true, | ||||
|         "**/CVS": true, | ||||
|         "**/.DS_Store": true, | ||||
|         "**/Thumbs.db": true, | ||||
|         "**/.project": true, | ||||
|         "**/.vscode": true, | ||||
|         "**/*.cs.meta": true, | ||||
|         "**/android": true, | ||||
|         "**/ios": true, | ||||
|         "**/node_modules": false, | ||||
|         "**/__pycache__": true, | ||||
|         "**/babel.config.js": true, | ||||
|         "**/metro.config.js": true, | ||||
|         "**/.prettierrc": true, | ||||
|         "**/.editorconfig": true | ||||
|     }, | ||||
|     "explorerExclude.backup": null | ||||
| } | ||||
|  | @ -0,0 +1,30 @@ | |||
| { | ||||
| 	"name": "yt-music", | ||||
| 	"version": "1.0.0", | ||||
| 	"description": "YouTube Music API", | ||||
| 	"main": "build/YTMusic.js", | ||||
| 	"types": "build/types.d.ts", | ||||
| 	"author": "zS1L3NT <zechariahtan144@gmail.com> (http://www.zectan.com)", | ||||
| 	"license": "ISC", | ||||
| 	"repository": { | ||||
| 		"type": "git", | ||||
| 		"url": "https://github.com/zS1L3NT/ts-npm-yt-music" | ||||
| 	}, | ||||
| 	"dependencies": { | ||||
| 		"axios": "^0.24.0", | ||||
| 		"tough-cookie": "^4.0.0" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@types/node": "^16.9.2", | ||||
| 		"@types/tough-cookie": "^4.0.1", | ||||
| 		"http-debug": "^0.1.2", | ||||
| 		"ts-node": "^10.2.1", | ||||
| 		"typescript": "^4.4.3", | ||||
| 		"youtube-music-api": "^1.0.6" | ||||
| 	}, | ||||
| 	"keywords": [ | ||||
| 		"youtube", | ||||
| 		"music", | ||||
| 		"api" | ||||
| 	] | ||||
| } | ||||
|  | @ -0,0 +1,250 @@ | |||
| import axios, { AxiosInstance } from "axios" | ||||
| import Parser from "./utils/Parser" | ||||
| import traverse from "./utils/traverse" | ||||
| import { Cookie, CookieJar } from "tough-cookie" | ||||
| 
 | ||||
| export default class YTMusic { | ||||
| 	private cookiejar: CookieJar | ||||
| 	private config?: Record<string, string> | ||||
| 	private client: AxiosInstance | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Creates an instance of YTMusic | ||||
| 	 * Make sure to call initialize() | ||||
| 	 */ | ||||
| 	public constructor() { | ||||
| 		this.cookiejar = new CookieJar() | ||||
| 		this.config = {} | ||||
| 		this.client = axios.create({ | ||||
| 			baseURL: "https://music.youtube.com/", | ||||
| 			headers: { | ||||
| 				"User-Agent": | ||||
| 					"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36", | ||||
| 				"Accept-Language": "en-US,en;q=0.5" | ||||
| 			}, | ||||
| 			withCredentials: true | ||||
| 		}) | ||||
| 
 | ||||
| 		this.client.interceptors.request.use(req => { | ||||
| 			if (!req.baseURL) return | ||||
| 
 | ||||
| 			const cookieString = this.cookiejar.getCookieStringSync(req.baseURL) | ||||
| 			if (cookieString) { | ||||
| 				if (!req.headers) { | ||||
| 					req.headers = {} | ||||
| 				} | ||||
| 				req.headers["Cookie"] = cookieString | ||||
| 			} | ||||
| 
 | ||||
| 			return req | ||||
| 		}) | ||||
| 
 | ||||
| 		this.client.interceptors.response.use(res => { | ||||
| 			if ("set-cookie" in res.headers) { | ||||
| 				if (!res.config.baseURL) return | ||||
| 
 | ||||
| 				const setCookie = res.headers["set-cookie"] as Array<string> | string | ||||
| 				const cookieStrings: string[] = [] | ||||
| 
 | ||||
| 				if (setCookie instanceof Array) { | ||||
| 					cookieStrings.push(...setCookie) | ||||
| 				} else { | ||||
| 					cookieStrings.push(setCookie) | ||||
| 				} | ||||
| 
 | ||||
| 				for (const cookieString of cookieStrings) { | ||||
| 					const cookie = Cookie.parse(cookieString) | ||||
| 					if (!cookie) return | ||||
| 
 | ||||
| 					this.cookiejar.setCookieSync(cookie, res.config.baseURL) | ||||
| 				} | ||||
| 			} | ||||
| 			return res | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Initializes the API | ||||
| 	 */ | ||||
| 	public async initialize() { | ||||
| 		const html = (await this.client.get("/")).data as string | ||||
| 		const setConfigs = html.match(/ytcfg\.set\(.*\)/) || [] | ||||
| 
 | ||||
| 		const configs = setConfigs | ||||
| 			.map(c => c.slice(10, -1)) | ||||
| 			.map(s => { | ||||
| 				try { | ||||
| 					return JSON.parse(s) | ||||
| 				} catch {} | ||||
| 			}) | ||||
| 			.filter(j => !!j) | ||||
| 
 | ||||
| 		for (const config of configs) { | ||||
| 			this.config = { | ||||
| 				...this.config, | ||||
| 				...config | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 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 | ||||
| 	 * and body parameters needed to make the API work | ||||
| 	 * | ||||
| 	 * @param endpoint Endpoint for the request | ||||
| 	 * @param body Body | ||||
| 	 * @param query Search params | ||||
| 	 * @returns Raw response from YouTube Music API which needs to be parsed | ||||
| 	 */ | ||||
| 	private async constructRequest( | ||||
| 		endpoint: string, | ||||
| 		body: Record<string, any> = {}, | ||||
| 		query: Record<string, string> = {} | ||||
| 	) { | ||||
| 		const config = this.assertInitialized() | ||||
| 
 | ||||
| 		const headers: Record<string, any> = { | ||||
| 			...this.client.defaults.headers, | ||||
| 			"x-origin": this.client.defaults.baseURL, | ||||
| 			"X-Goog-Visitor-Id": config.VISITOR_DATA, | ||||
| 			"X-YouTube-Client-Name": config.INNERTUBE_CONTEXT_CLIENT_NAME, | ||||
| 			"X-YouTube-Client-Version": config.INNERTUBE_CLIENT_VERSION, | ||||
| 			"X-YouTube-Device": config.DEVICE, | ||||
| 			"X-YouTube-Page-CL": config.PAGE_CL, | ||||
| 			"X-YouTube-Page-Label": config.PAGE_BUILD_LABEL, | ||||
| 			"X-YouTube-Utc-Offset": String(-new Date().getTimezoneOffset()), | ||||
| 			"X-YouTube-Time-Zone": new Intl.DateTimeFormat().resolvedOptions().timeZone | ||||
| 		} | ||||
| 
 | ||||
| 		const searchParams = new URLSearchParams({ | ||||
| 			...query, | ||||
| 			alt: "json", | ||||
| 			key: config.INNERTUBE_API_KEY | ||||
| 		}) | ||||
| 
 | ||||
| 		// prettier-ignore
 | ||||
| 		const res = await this.client.post( | ||||
| 			`youtubei/${config.INNERTUBE_API_VERSION}/${endpoint}?${searchParams.toString()}`, | ||||
| 			{ | ||||
| 				context: { | ||||
| 					capabilities: {}, | ||||
| 					client: { | ||||
| 						clientName: config.INNERTUBE_CLIENT_NAME, | ||||
| 						clientVersion: config.INNERTUBE_CLIENT_VERSION, | ||||
| 						experimentIds: [], | ||||
| 						experimentsToken: "", | ||||
| 						gl: config.GL, | ||||
| 						hl: config.HL, | ||||
| 						locationInfo: { | ||||
| 							locationPermissionAuthorizationStatus: | ||||
| 								"LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED" | ||||
| 						}, | ||||
| 						musicAppInfo: { | ||||
| 							musicActivityMasterSwitch: "MUSIC_ACTIVITY_MASTER_SWITCH_INDETERMINATE", | ||||
| 							musicLocationMasterSwitch: "MUSIC_LOCATION_MASTER_SWITCH_INDETERMINATE", | ||||
| 							pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_UNKNOWN" | ||||
| 						}, | ||||
| 						utcOffsetMinutes: -new Date().getTimezoneOffset() | ||||
| 					}, | ||||
| 					request: { | ||||
| 						internalExperimentFlags: [ | ||||
| 							{ | ||||
| 								key: "force_music_enable_outertube_tastebuilder_browse", | ||||
| 								value: "true" | ||||
| 							}, | ||||
| 							{ | ||||
| 								key: "force_music_enable_outertube_playlist_detail_browse", | ||||
| 								value: "true" | ||||
| 							}, | ||||
| 							{ | ||||
| 								key: "force_music_enable_outertube_search_suggestions", | ||||
| 								value: "true" | ||||
| 							} | ||||
| 						], | ||||
| 						sessionIndex: {} | ||||
| 					}, | ||||
| 					user: { | ||||
| 						enableSafetyMode: false | ||||
| 					} | ||||
| 				}, | ||||
| 				...body | ||||
| 			}, | ||||
| 			{ | ||||
| 				responseType: "json", | ||||
| 				headers | ||||
| 			} | ||||
| 		) | ||||
| 
 | ||||
| 		return "responseContext" in res.data ? res.data : res | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Get a list of search suggestiong based on the query | ||||
| 	 * | ||||
| 	 * @param query Query string | ||||
| 	 * @returns Search suggestions | ||||
| 	 */ | ||||
| 	public async getSearchSuggestions(query: string): Promise<string[]> { | ||||
| 		const res = await this.constructRequest("music/get_search_suggestions", { | ||||
| 			input: query | ||||
| 		}) | ||||
| 
 | ||||
| 		return traverse(res, "query") | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * Searches YouTube Music API for content | ||||
| 	 * | ||||
| 	 * @param query Query string | ||||
| 	 * @param category Type of search results to receive | ||||
| 	 */ | ||||
| 	public async search(query: string, category: "SONG"): Promise<YTMusic.SongDetailed[]> | ||||
| 	public async search(query: string, category: "PLAYLIST"): Promise<YTMusic.PlaylistDetailed[]> | ||||
| 	public async search(query: string, category: "VIDEO"): Promise<YTMusic.VideoDetailed[]> | ||||
| 	public async search(query: string, category: "ARTIST"): Promise<YTMusic.ArtistDetailed[]> | ||||
| 	public async search(query: string, category: "ALBUM"): Promise<YTMusic.AlbumDetailed[]> | ||||
| 	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", { | ||||
| 			query: query, | ||||
| 			params: | ||||
| 				{ | ||||
| 					SONG: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D", | ||||
| 					VIDEO: "Eg-KAQwIABABGAAgACgAMABqChAEEAMQCRAFEAo%3D", | ||||
| 					ALBUM: "Eg-KAQwIABAAGAEgACgAMABqChAEEAMQCRAFEAo%3D", | ||||
| 					ARTIST: "Eg-KAQwIABAAGAAgASgAMABqChAEEAMQCRAFEAo%3D", | ||||
| 					PLAYLIST: "Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D" | ||||
| 				}[category!] || null | ||||
| 		}) | ||||
| 
 | ||||
| 		const parser = new Parser(data) | ||||
| 		return ( | ||||
| 			{ | ||||
| 				SONG: parser.parseSongsSearchResults, | ||||
| 				VIDEO: parser.parseVideosSearchResults, | ||||
| 				ARTIST: parser.parseArtistsSearchResults, | ||||
| 				ALBUM: parser.parseAlbumsSearchResults, | ||||
| 				PLAYLIST: parser.parsePlaylistsSearchResults | ||||
| 			}[category!] || parser.parseSearchResult | ||||
| 		).call(parser) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| const ytmusicapi = new YTMusic() | ||||
| ytmusicapi.initialize().then(async () => { | ||||
| 	console.log("Initialized") | ||||
| }) | ||||
|  | @ -0,0 +1,67 @@ | |||
| declare namespace YTMusic { | ||||
| 	interface ThumbnailFull { | ||||
| 		url: string | ||||
| 		width: number | ||||
| 		height: number | ||||
| 	} | ||||
| 
 | ||||
| 	interface SongDetailed { | ||||
| 		type: "SONG" | ||||
| 		videoId: string | ||||
| 		name: string | ||||
| 		artists: ArtistBasic[] | ||||
| 		album: AlbumBasic | ||||
| 		duration: number | ||||
| 		thumbnails: ThumbnailFull[] | ||||
| 	} | ||||
| 
 | ||||
| 	interface VideoDetailed { | ||||
| 		type: "VIDEO" | ||||
| 		videoId: string | ||||
| 		name: string | ||||
| 		artist: ArtistBasic | ||||
| 		views: number | ||||
| 		duration: number | ||||
| 		thumbnails: ThumbnailFull[] | ||||
| 	} | ||||
| 
 | ||||
| 	interface ArtistBasic { | ||||
| 		artistId: string | ||||
| 		name: string | ||||
| 	} | ||||
| 
 | ||||
| 	interface ArtistDetailed extends ArtistBasic { | ||||
| 		type: "ARTIST" | ||||
| 		thumbnails: ThumbnailFull[] | ||||
| 	} | ||||
| 
 | ||||
| 	interface AlbumBasic { | ||||
| 		albumId: string | ||||
| 		name: string | ||||
| 	} | ||||
| 
 | ||||
| 	interface AlbumDetailed extends AlbumBasic { | ||||
| 		type: "ALBUM" | ||||
| 		playlistId: string | ||||
| 		artists: ArtistBasic[] | ||||
| 		year: number | ||||
| 		thumbnails: ThumbnailFull[] | ||||
| 	} | ||||
| 
 | ||||
| 	interface PlaylistDetailed { | ||||
| 		type: "PLAYLIST" | ||||
| 		playlistId: string | ||||
| 		name: string | ||||
| 		artist: ArtistBasic | ||||
| 		trackCount: number | ||||
| 		thumbnails: ThumbnailFull[] | ||||
| 	} | ||||
| 
 | ||||
| 	type SearchResult = | ||||
| 		| SongDetailed | ||||
| 		| PlaylistDetailed | ||||
| 		| VideoDetailed | ||||
| 		| AlbumDetailed | ||||
| 		| ArtistDetailed | ||||
| 		| PlaylistDetailed | ||||
| } | ||||
|  | @ -0,0 +1,175 @@ | |||
| import traverse from "./traverse" | ||||
| 
 | ||||
| const 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) | ||||
| 
 | ||||
| 		return ( | ||||
| 			{ | ||||
| 				K: number * 1000, | ||||
| 				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,30 @@ | |||
| const traverse = (data: any, ...keys: string[]) => { | ||||
| 	const again = (data: any, key: string): any => { | ||||
| 		let res = [] | ||||
| 
 | ||||
| 		if (data instanceof Object && key in data) { | ||||
| 			res.push(data[key]) | ||||
| 		} | ||||
| 
 | ||||
| 		if (data instanceof Array) { | ||||
| 			res.push(...data.map(v => again(v, key)).flat()) | ||||
| 		} else if (data instanceof Object) { | ||||
| 			res.push( | ||||
| 				...Object.keys(data) | ||||
| 					.map(k => again(data[k], key)) | ||||
| 					.flat() | ||||
| 			) | ||||
| 		} | ||||
| 
 | ||||
| 		return res.length === 1 ? res[0] : res | ||||
| 	} | ||||
| 
 | ||||
| 	let value = data | ||||
| 	for (const key of keys) { | ||||
| 		value = again(value, key) | ||||
| 	} | ||||
| 
 | ||||
| 	return value | ||||
| } | ||||
| 
 | ||||
| export default traverse | ||||
|  | @ -0,0 +1,23 @@ | |||
| { | ||||
| 	"compilerOptions": { | ||||
| 		"target": "esnext", | ||||
| 		"allowSyntheticDefaultImports": true, | ||||
| 		"forceConsistentCasingInFileNames": true, | ||||
| 		"module": "commonjs", | ||||
| 		"moduleResolution": "node", | ||||
| 		"esModuleInterop": true, | ||||
| 		"strict": true, | ||||
| 		"alwaysStrict": true, | ||||
| 		"noImplicitAny": true, | ||||
| 		"noUnusedLocals": false, | ||||
| 		"noUnusedParameters": false, | ||||
| 		"resolveJsonModule": true, | ||||
| 		"strictNullChecks": true, | ||||
| 		"strictFunctionTypes": true, | ||||
| 		"outDir": "build", | ||||
| 		"rootDir": "src" | ||||
| 	}, | ||||
| 	"exclude": [ | ||||
| 		"**/*.test.*" | ||||
| 	] | ||||
| } | ||||
|  | @ -0,0 +1,184 @@ | |||
| # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. | ||||
| # yarn lockfile v1 | ||||
| 
 | ||||
| 
 | ||||
| "@cspotcode/source-map-consumer@0.8.0": | ||||
|   version "0.8.0" | ||||
|   resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" | ||||
|   integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== | ||||
| 
 | ||||
| "@cspotcode/source-map-support@0.7.0": | ||||
|   version "0.7.0" | ||||
|   resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" | ||||
|   integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== | ||||
|   dependencies: | ||||
|     "@cspotcode/source-map-consumer" "0.8.0" | ||||
| 
 | ||||
| "@tsconfig/node10@^1.0.7": | ||||
|   version "1.0.8" | ||||
|   resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" | ||||
|   integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== | ||||
| 
 | ||||
| "@tsconfig/node12@^1.0.7": | ||||
|   version "1.0.9" | ||||
|   resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" | ||||
|   integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== | ||||
| 
 | ||||
| "@tsconfig/node14@^1.0.0": | ||||
|   version "1.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" | ||||
|   integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== | ||||
| 
 | ||||
| "@tsconfig/node16@^1.0.2": | ||||
|   version "1.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" | ||||
|   integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== | ||||
| 
 | ||||
| "@types/node@^16.9.2": | ||||
|   version "16.11.15" | ||||
|   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.15.tgz#724da13bc1ba99fe8190d0f5cd35cb53c67db942" | ||||
|   integrity sha512-LMGR7iUjwZRxoYnfc9+YELxwqkaLmkJlo4/HUvOMyGvw9DaHO0gtAbH2FUdoFE6PXBTYZIT7x610r7kdo8o1fQ== | ||||
| 
 | ||||
| "@types/tough-cookie@^4.0.1": | ||||
|   version "4.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40" | ||||
|   integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg== | ||||
| 
 | ||||
| acorn-walk@^8.1.1: | ||||
|   version "8.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" | ||||
|   integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== | ||||
| 
 | ||||
| acorn@^8.4.1: | ||||
|   version "8.6.0" | ||||
|   resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895" | ||||
|   integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw== | ||||
| 
 | ||||
| arg@^4.1.0: | ||||
|   version "4.1.3" | ||||
|   resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" | ||||
|   integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== | ||||
| 
 | ||||
| axios@^0.19.2: | ||||
|   version "0.19.2" | ||||
|   resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" | ||||
|   integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== | ||||
|   dependencies: | ||||
|     follow-redirects "1.5.10" | ||||
| 
 | ||||
| axios@^0.24.0: | ||||
|   version "0.24.0" | ||||
|   resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" | ||||
|   integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== | ||||
|   dependencies: | ||||
|     follow-redirects "^1.14.4" | ||||
| 
 | ||||
| create-require@^1.1.0: | ||||
|   version "1.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" | ||||
|   integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== | ||||
| 
 | ||||
| debug@=3.1.0: | ||||
|   version "3.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" | ||||
|   integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== | ||||
|   dependencies: | ||||
|     ms "2.0.0" | ||||
| 
 | ||||
| diff@^4.0.1: | ||||
|   version "4.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" | ||||
|   integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== | ||||
| 
 | ||||
| follow-redirects@1.5.10: | ||||
|   version "1.5.10" | ||||
|   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" | ||||
|   integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== | ||||
|   dependencies: | ||||
|     debug "=3.1.0" | ||||
| 
 | ||||
| follow-redirects@^1.14.4: | ||||
|   version "1.14.6" | ||||
|   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" | ||||
|   integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== | ||||
| 
 | ||||
| http-debug@^0.1.2: | ||||
|   version "0.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/http-debug/-/http-debug-0.1.2.tgz#aadbfef99bc39c206439ece14b99040c5a4b4d6e" | ||||
|   integrity sha1-qtv++ZvDnCBkOezhS5kEDFpLTW4= | ||||
| 
 | ||||
| lodash@^4.17.15: | ||||
|   version "4.17.21" | ||||
|   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" | ||||
|   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== | ||||
| 
 | ||||
| make-error@^1.1.1: | ||||
|   version "1.3.6" | ||||
|   resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" | ||||
|   integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== | ||||
| 
 | ||||
| ms@2.0.0: | ||||
|   version "2.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" | ||||
|   integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= | ||||
| 
 | ||||
| psl@^1.1.33: | ||||
|   version "1.8.0" | ||||
|   resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" | ||||
|   integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== | ||||
| 
 | ||||
| punycode@^2.1.1: | ||||
|   version "2.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" | ||||
|   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== | ||||
| 
 | ||||
| tough-cookie@^4.0.0: | ||||
|   version "4.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" | ||||
|   integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== | ||||
|   dependencies: | ||||
|     psl "^1.1.33" | ||||
|     punycode "^2.1.1" | ||||
|     universalify "^0.1.2" | ||||
| 
 | ||||
| ts-node@^10.2.1: | ||||
|   version "10.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" | ||||
|   integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== | ||||
|   dependencies: | ||||
|     "@cspotcode/source-map-support" "0.7.0" | ||||
|     "@tsconfig/node10" "^1.0.7" | ||||
|     "@tsconfig/node12" "^1.0.7" | ||||
|     "@tsconfig/node14" "^1.0.0" | ||||
|     "@tsconfig/node16" "^1.0.2" | ||||
|     acorn "^8.4.1" | ||||
|     acorn-walk "^8.1.1" | ||||
|     arg "^4.1.0" | ||||
|     create-require "^1.1.0" | ||||
|     diff "^4.0.1" | ||||
|     make-error "^1.1.1" | ||||
|     yn "3.1.1" | ||||
| 
 | ||||
| typescript@^4.4.3: | ||||
|   version "4.5.4" | ||||
|   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" | ||||
|   integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== | ||||
| 
 | ||||
| universalify@^0.1.2: | ||||
|   version "0.1.2" | ||||
|   resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" | ||||
|   integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== | ||||
| 
 | ||||
| yn@3.1.1: | ||||
|   version "3.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" | ||||
|   integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== | ||||
| 
 | ||||
| youtube-music-api@^1.0.6: | ||||
|   version "1.0.6" | ||||
|   resolved "https://registry.yarnpkg.com/youtube-music-api/-/youtube-music-api-1.0.6.tgz#e55cf7d7f6d764b1ac6b053698462f22de379e77" | ||||
|   integrity sha512-/U63iOLaci7zrxKw26dEc7sYZmfPkZLdWJ2MRx9nUmG8gUwhNjLtNYDfny49+vjpxvnLOWO9Pp734G5/beU+kg== | ||||
|   dependencies: | ||||
|     axios "^0.19.2" | ||||
|     lodash "^4.17.15" | ||||
|     tough-cookie "^4.0.0" | ||||
		Loading…
	
		Reference in New Issue