commit 14ae240638c671918d0f992305044fd1e4dbe141 Author: Zechariah Date: Thu Dec 23 01:01:37 2021 +0800 Initial Commit - Searching works diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9c34fd3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*] +indent_size = 4 +indent_style = tab \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..04bbd55 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/.idea +/build +config.json +*.test.ts +.rest \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..56eb3d4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "arrowParens": "avoid", + "trailingComma": "none", + "jsxSingleQuote": false, + "jsxBracketSameLine": true, + "printWidth": 100 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e6c7276 --- /dev/null +++ b/.vscode/launch.json @@ -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": ["/**"], + "runtimeExecutable": "node", + "runtimeArgs": ["--require", "ts-node/register/files"] + }, + { + "type": "node", + "request": "launch", + "name": "YTMusic.ts", + "program": "${workspaceFolder}/src/YTMusic.ts", + "sourceMaps": true, + "skipFiles": ["/**"], + "runtimeExecutable": "node", + "runtimeArgs": ["--require", "ts-node/register/files"] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9346d46 --- /dev/null +++ b/.vscode/settings.json @@ -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 +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..b62694c --- /dev/null +++ b/package.json @@ -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 (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" + ] +} diff --git a/src/YTMusic.ts b/src/YTMusic.ts new file mode 100644 index 0000000..595c751 --- /dev/null +++ b/src/YTMusic.ts @@ -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 + 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 + 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 = {}, + query: Record = {} + ) { + const config = this.assertInitialized() + + const headers: Record = { + ...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 { + 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 + public async search(query: string, category: "PLAYLIST"): Promise + public async search(query: string, category: "VIDEO"): Promise + public async search(query: string, category: "ARTIST"): Promise + public async search(query: string, category: "ALBUM"): Promise + public async search(query: string, category: "PLAYLIST"): Promise + public async search(query: string): Promise + 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") +}) diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..8dad2ba --- /dev/null +++ b/src/types.d.ts @@ -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 +} diff --git a/src/utils/Parser.ts b/src/utils/Parser.ts new file mode 100644 index 0000000..23f678d --- /dev/null +++ b/src/utils/Parser.ts @@ -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]() + }) + } +} diff --git a/src/utils/traverse.ts b/src/utils/traverse.ts new file mode 100644 index 0000000..1022423 --- /dev/null +++ b/src/utils/traverse.ts @@ -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 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..518e7aa --- /dev/null +++ b/tsconfig.json @@ -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.*" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..1140d71 --- /dev/null +++ b/yarn.lock @@ -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"