Initial Commit - Searching works

This commit is contained in:
Zechariah 2021-12-23 01:01:37 +08:00
commit 14ae240638
12 changed files with 826 additions and 0 deletions

3
.editorconfig Normal file
View File

@ -0,0 +1,3 @@
[*]
indent_size = 4
indent_style = tab

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/node_modules
/.idea
/build
config.json
*.test.ts
.rest

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"arrowParens": "avoid",
"trailingComma": "none",
"jsxSingleQuote": false,
"jsxBracketSameLine": true,
"printWidth": 100
}

28
.vscode/launch.json vendored Normal file
View File

@ -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"]
}
]
}

22
.vscode/settings.json vendored Normal file
View File

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

30
package.json Normal file
View File

@ -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"
]
}

250
src/YTMusic.ts Normal file
View File

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

67
src/types.d.ts vendored Normal file
View File

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

175
src/utils/Parser.ts Normal file
View File

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

30
src/utils/traverse.ts Normal file
View File

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

23
tsconfig.json Normal file
View File

@ -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.*"
]
}

184
yarn.lock Normal file
View File

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