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