Merge branch 'dev' into main
This commit is contained in:
commit
9e5dd71b90
|
|
@ -0,0 +1,7 @@
|
|||
/src
|
||||
.vscode
|
||||
.gitignore
|
||||
.prettierrc
|
||||
.editorconfig
|
||||
nodemon.json
|
||||
tsconfig.json
|
||||
|
|
@ -7,8 +7,8 @@
|
|||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "index.ts",
|
||||
"program": "${workspaceFolder}/src/index.ts",
|
||||
"name": "testing/run.ts",
|
||||
"program": "${workspaceFolder}/src/testing/run.ts",
|
||||
"sourceMaps": true,
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"runtimeExecutable": "node",
|
||||
|
|
@ -17,8 +17,8 @@
|
|||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "YTMusic.ts",
|
||||
"program": "${workspaceFolder}/src/YTMusic.ts",
|
||||
"name": "tests/all.ts",
|
||||
"program": "${workspaceFolder}/src/tests/all.ts",
|
||||
"sourceMaps": true,
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"runtimeExecutable": "node",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,369 @@
|
|||
# yt-music
|
||||
|
||||
YouTube Music API which comes with TypeScript support
|
||||
|
||||
## Initialization
|
||||
|
||||
Import YTMusic from the npm package
|
||||
|
||||
```ts
|
||||
// TypeScript
|
||||
import YTMusic from "yt-music"
|
||||
|
||||
// JavaScript
|
||||
const YTMusic = require("yt-music")
|
||||
```
|
||||
|
||||
Create an instance of the class `YTMusic`.
|
||||
Then, call the `initialize()` to initialize the API before using the API anywhere
|
||||
|
||||
```ts
|
||||
const ytmusic = new YTMusic()
|
||||
ytmusic.initialize().then(() => {
|
||||
// Use API here
|
||||
})
|
||||
```
|
||||
|
||||
## Class Methods
|
||||
### `getSearchSuggestions`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| query | `string` | Search query you want suggestions for |
|
||||
|
||||
The function returns a `Promise<string[]>` which are the suggestion results
|
||||
|
||||
```ts
|
||||
ytmusic.getSearchSuggestions("Lilac").then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
```
|
||||
|
||||
### `search`
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| query | `string` | Search query |
|
||||
| category | `"SONG" \| "VIDEO" \| "ARTIST" \| "ALBUM" \| "PLAYLIST" \| undefined` | Type of results to search for. If not specified, returns all types of search result |
|
||||
|
||||
The function **when nothing is passed as the category** returns a `Promise<`[SearchResult](#SearchResult)`[]>` which are the search results of all categories
|
||||
|
||||
```ts
|
||||
ytmusic.search("Lilac").then(results => {
|
||||
console.log(results)
|
||||
})
|
||||
```
|
||||
|
||||
#### `search (category = "SONG")`
|
||||
When you pass in `"SONG"` as the category,
|
||||
|
||||
The function returns a `Promise<`[SongDetailed](#SongDetailed)`[]>` which are the song results
|
||||
|
||||
```ts
|
||||
ytmusic.search("Lilac", "SONG").then(songs => {
|
||||
console.log(songs)
|
||||
})
|
||||
```
|
||||
|
||||
#### `search (category = "VIDEO")`
|
||||
When you pass in `"VIDEO"` as the category,
|
||||
|
||||
The function returns a `Promise<`[VideoDetailed](#VideoDetailed)`[]>` which are the video results
|
||||
|
||||
```ts
|
||||
ytmusic.search("Lilac", "VIDEO").then(videos => {
|
||||
console.log(videos)
|
||||
})
|
||||
```
|
||||
|
||||
#### `search (category = "ARTIST")`
|
||||
When you pass in `"ARTIST"` as the category
|
||||
|
||||
The function returns a `Promise<`[ArtistDetailed](#ArtistDetailed)`[]>` which are the artist results
|
||||
|
||||
```ts
|
||||
ytmusic.search("Lilac", "ARTIST").then(artists => {
|
||||
console.log(artists)
|
||||
})
|
||||
```
|
||||
|
||||
#### `search (category = "ALBUM")`
|
||||
When you pass in `"ALBUM"` as the category,
|
||||
|
||||
The function returns a `Promise<`[AlbumDetailed](#AlbumDetailed)`[]>` which are the album results
|
||||
|
||||
```ts
|
||||
ytmusic.search("Lilac", "ALBUM").then(albums => {
|
||||
console.log(albums)
|
||||
})
|
||||
```
|
||||
|
||||
#### `search (category = "PLAYLIST")`
|
||||
When you pass in `"PLAYLIST"` as the category,
|
||||
|
||||
The function returns a `Promise<`[PlaylistFull](#PlaylistFull)`[]>` which are the playlist results
|
||||
|
||||
```ts
|
||||
ytmusic.search("Lilac", "PLAYLIST").then(playlists => {
|
||||
console.log(playlists)
|
||||
})
|
||||
```
|
||||
|
||||
### `getSong`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| videoId | `string` | Video ID |
|
||||
|
||||
The function returns a `Promise<`[SongFull](#SongFull)`>` which is the information about the song
|
||||
|
||||
```ts
|
||||
ytmusic.getSong("v7bnOxV4jAc").then(song => {
|
||||
console.log(song)
|
||||
})
|
||||
```
|
||||
|
||||
### `getVideo`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| videoId | `string` | Video ID |
|
||||
|
||||
The function returns a `Promise<`[VideoFull](#VideoFull)`>` which is the information about the video
|
||||
|
||||
```ts
|
||||
ytmusic.getVideo("v7bnOxV4jAc").then(video => {
|
||||
console.log(video)
|
||||
})
|
||||
```
|
||||
|
||||
### `getArtist`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| artistId | `string` | Artist ID |
|
||||
|
||||
The function returns a `Promise<`[ArtistFull](#ArtistFull)`>` which is the information about the artist
|
||||
|
||||
```ts
|
||||
ytmusic.getArtist("UCTUR0sVEkD8T5MlSHqgaI_Q").then(artist => {
|
||||
console.log(artist)
|
||||
})
|
||||
```
|
||||
|
||||
### `getArtistSongs`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| artistId | `string` | Artist ID |
|
||||
|
||||
The function returns a `Promise<`[SongDetailed](#SongDetailed)`[]>` which is the information about all the artist's songs
|
||||
|
||||
```ts
|
||||
ytmusic.getArtistSongs("UCTUR0sVEkD8T5MlSHqgaI_Q").then(artistSongs => {
|
||||
console.log(artistSongs)
|
||||
})
|
||||
```
|
||||
|
||||
### `getArtistAlbums`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| artistId | `string` | Artist ID |
|
||||
|
||||
The function returns a `Promise<`[AlbumDetailed](#AlbumDetailed)`[]>` which is the information about all the artist's albums
|
||||
|
||||
```ts
|
||||
ytmusic.getArtistAlbums("UCTUR0sVEkD8T5MlSHqgaI_Q").then(artistAlbums => {
|
||||
console.log(artistAlbums)
|
||||
})
|
||||
```
|
||||
|
||||
### `getAlbum`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| albumId | `string` | Album ID |
|
||||
|
||||
The function returns a `Promise<`[AlbumFull](#AlbumFull)`>` which is the information about the album
|
||||
|
||||
```ts
|
||||
ytmusic.getAlbum("MPREb_iG5q5DIdhdA").then(album => {
|
||||
console.log(album)
|
||||
})
|
||||
```
|
||||
|
||||
### `getPlaylist`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| playlistId | `string` | Playlist ID |
|
||||
|
||||
The function returns a `Promise<`[PlaylistFull](#PlaylistFull)`>` which is the information about the playlist (without the videos)
|
||||
|
||||
```ts
|
||||
ytmusic.getPlaylist("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(playlist => {
|
||||
console.log(playlist)
|
||||
})
|
||||
```
|
||||
|
||||
### `getPlaylistVideos`
|
||||
|
||||
This function takes in the following parameters
|
||||
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| playlistId | `string` | Playlist ID |
|
||||
|
||||
The function returns a `Promise<Omit<`[VideoDetailed](#VideoDetailed)`, "views">[]>` which is the information about the videos without the view count
|
||||
|
||||
```ts
|
||||
ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(playlistVideos => {
|
||||
console.log(playlistVideos)
|
||||
})
|
||||
```
|
||||
|
||||
## Data Types
|
||||
### `ThumbnailFull`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| url | `string` | Link |
|
||||
| width | `number` | Width of the image |
|
||||
| height | `number` | Height of the image |
|
||||
|
||||
### `SongDetailed`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"SONG"` | Type of data |
|
||||
| videoId | `string \| null` | YouTube Video ID |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Artists |
|
||||
| album | [AlbumBasic](#AlbumBasic) | Album |
|
||||
| duration | `number` | Duration in seconds |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
|
||||
### `SongFull`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"SONG"` | Type of data |
|
||||
| videoId | `string \| null` | YouTube Video ID |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Artists |
|
||||
| duration | `number` | Duration in seconds |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
| description | `string` | Description |
|
||||
| formats | `any[]` | Video Formats |
|
||||
| adaptiveFormats | `any[]` | Adaptive Video Formats |
|
||||
|
||||
### `VideoDetailed`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"VIDEO"` | Type of data |
|
||||
| videoId | `string \| null` | YouTube Video ID |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Channels that created the video |
|
||||
| views | `number` | View count |
|
||||
| duration | `number` | Duration in seconds |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
|
||||
### `VideoFull`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"VIDEO"` | Type of data |
|
||||
| videoId | `string \| null` | YouTube Video ID |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Channels that created the video |
|
||||
| views | `number` | View count |
|
||||
| duration | `number` | Duration in seconds |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
| description | `string` | Description |
|
||||
| unlisted | `boolean` | If the video is unlisted on YouTube |
|
||||
| familySafe | `boolean` | If the video is family safe on YouTube |
|
||||
| paid | `boolean` | If the video is paid on YouTube |
|
||||
| tags | `string[]` | Tags |
|
||||
|
||||
### `ArtistBasic`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| artistId | `string \| null` | Artist ID |
|
||||
| name | `string` | Name |
|
||||
|
||||
### `ArtistDetailed`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"ARTIST"` | Type of data |
|
||||
| artistId | `string` | Artist ID |
|
||||
| name | `string` | Name |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
|
||||
### `ArtistFull`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"ARTIST"` | Type of data |
|
||||
| artistId | `string` | Artist ID |
|
||||
| name | `string` | Name |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
| description | `string \| null` | Description |
|
||||
| subscribers | `number` | Number of subscribers the Artist has|
|
||||
| topSongs | `Omit<`[SongDetailed](#SongDetailed)`, "duration">[]` | Top Songs from Artist |
|
||||
| topAlbums | [AlbumDetailed](#AlbumDetailed)`[]` | Top Albums from Artist |
|
||||
|
||||
### `AlbumBasic`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| albumId | `string` | Album ID |
|
||||
| name | `string` | Name |
|
||||
|
||||
### `AlbumDetailed`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"ALBUM"` | Type of data |
|
||||
| albumId | `string` | Album ID |
|
||||
| playlistId | `string` | Playlist ID for Album |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Creators of the Album |
|
||||
| year | `number` | Publication Year |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
|
||||
### `AlbumFull`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"ALBUM"` | Type of data |
|
||||
| albumId | `string` | Album ID |
|
||||
| playlistId | `string` | Playlist ID for Album |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Creators of the Album |
|
||||
| year | `number` | Publication Year |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
| description | `string \| null` | Description |
|
||||
| songs | [SongDetailed](#SongDetailed)`[]` | Songs in the Album
|
||||
|
||||
### `PlaylistFull`
|
||||
| Name | Data Type | Description |
|
||||
| :--- | :-------- | :---------- |
|
||||
| type | `"PLAYLIST"` | Type of data |
|
||||
| playlistId | `string` | Playlist ID |
|
||||
| name | `string` | Name |
|
||||
| artist | [ArtistBasic](#ArtistBasic) | Creator of the Playlist |
|
||||
| videoCount | `number` | Number of videos in the Playlist |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
|
||||
### `SearchResult`
|
||||
[SongDetailed](#SongDetailed) or [VideoDetailed](#VideoDetailed) or [ArtistDetailed](#ArtistDetailed) or [AlbumDetailed](#AlbumDetailed) or [PlaylistFull](#PlaylistFull)
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
"http-debug": "^0.1.2",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.4.3",
|
||||
"validate-any": "^1.1.1",
|
||||
"youtube-music-api": "^1.0.6"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
|||
224
src/YTMusic.ts
224
src/YTMusic.ts
|
|
@ -1,6 +1,11 @@
|
|||
import AlbumParser from "./parsers/AlbumParser"
|
||||
import ArtistParser from "./parsers/ArtistParser"
|
||||
import axios, { AxiosInstance } from "axios"
|
||||
import Parser from "./utils/Parser"
|
||||
import PlaylistParser from "./parsers/PlaylistParser"
|
||||
import SearchParser from "./parsers/SearchParser"
|
||||
import SongParser from "./parsers/SongParser"
|
||||
import traverse from "./utils/traverse"
|
||||
import VideoParser from "./parsers/VideoParser"
|
||||
import { Cookie, CookieJar } from "tough-cookie"
|
||||
|
||||
export default class YTMusic {
|
||||
|
|
@ -87,19 +92,6 @@ export default class YTMusic {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
@ -114,17 +106,19 @@ export default class YTMusic {
|
|||
body: Record<string, any> = {},
|
||||
query: Record<string, string> = {}
|
||||
) {
|
||||
const config = this.assertInitialized()
|
||||
if (!this.config) {
|
||||
throw new Error("API not initialized. Make sure to call the initialize() method first")
|
||||
}
|
||||
|
||||
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-Goog-Visitor-Id": this.config.VISITOR_DATA,
|
||||
"X-YouTube-Client-Name": this.config.INNERTUBE_CONTEXT_CLIENT_NAME,
|
||||
"X-YouTube-Client-Version": this.config.INNERTUBE_CLIENT_VERSION,
|
||||
"X-YouTube-Device": this.config.DEVICE,
|
||||
"X-YouTube-Page-CL": this.config.PAGE_CL,
|
||||
"X-YouTube-Page-Label": this.config.PAGE_BUILD_LABEL,
|
||||
"X-YouTube-Utc-Offset": String(-new Date().getTimezoneOffset()),
|
||||
"X-YouTube-Time-Zone": new Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
}
|
||||
|
|
@ -132,22 +126,21 @@ export default class YTMusic {
|
|||
const searchParams = new URLSearchParams({
|
||||
...query,
|
||||
alt: "json",
|
||||
key: config.INNERTUBE_API_KEY
|
||||
key: this.config.INNERTUBE_API_KEY
|
||||
})
|
||||
|
||||
// prettier-ignore
|
||||
const res = await this.client.post(
|
||||
`youtubei/${config.INNERTUBE_API_VERSION}/${endpoint}?${searchParams.toString()}`,
|
||||
`youtubei/${this.config.INNERTUBE_API_VERSION}/${endpoint}?${searchParams.toString()}`,
|
||||
{
|
||||
context: {
|
||||
capabilities: {},
|
||||
client: {
|
||||
clientName: config.INNERTUBE_CLIENT_NAME,
|
||||
clientVersion: config.INNERTUBE_CLIENT_VERSION,
|
||||
clientName: this.config.INNERTUBE_CLIENT_NAME,
|
||||
clientVersion: this.config.INNERTUBE_CLIENT_VERSION,
|
||||
experimentIds: [],
|
||||
experimentsToken: "",
|
||||
gl: config.GL,
|
||||
hl: config.HL,
|
||||
gl: this.config.GL,
|
||||
hl: this.config.HL,
|
||||
locationInfo: {
|
||||
locationPermissionAuthorizationStatus:
|
||||
"LOCATION_PERMISSION_AUTHORIZATION_STATUS_UNSUPPORTED"
|
||||
|
|
@ -198,11 +191,12 @@ export default class YTMusic {
|
|||
* @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")
|
||||
return traverse(
|
||||
await this.constructRequest("music/get_search_suggestions", {
|
||||
input: query
|
||||
}),
|
||||
"query"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -212,15 +206,14 @@ export default class YTMusic {
|
|||
* @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, category: "PLAYLIST"): Promise<YTMusic.PlaylistFull[]>
|
||||
public async search(query: string): Promise<YTMusic.SearchResult[]>
|
||||
public async search(query: string, category?: string) {
|
||||
const data = await this.constructRequest("search", {
|
||||
query: query,
|
||||
const searchData = await this.constructRequest("search", {
|
||||
query,
|
||||
params:
|
||||
{
|
||||
SONG: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D",
|
||||
|
|
@ -231,20 +224,149 @@ export default class YTMusic {
|
|||
}[category!] || null
|
||||
})
|
||||
|
||||
const parser = new Parser(data)
|
||||
return (
|
||||
return [traverse(searchData, "musicResponsiveListItemRenderer")].flat().map(
|
||||
{
|
||||
SONG: parser.parseSongsSearchResults,
|
||||
VIDEO: parser.parseVideosSearchResults,
|
||||
ARTIST: parser.parseArtistsSearchResults,
|
||||
ALBUM: parser.parseAlbumsSearchResults,
|
||||
PLAYLIST: parser.parsePlaylistsSearchResults
|
||||
}[category!] || parser.parseSearchResult
|
||||
).call(parser)
|
||||
SONG: SongParser.parseSearchResult,
|
||||
VIDEO: VideoParser.parseSearchResult,
|
||||
ARTIST: ArtistParser.parseSearchResult,
|
||||
ALBUM: AlbumParser.parseSearchResult,
|
||||
PLAYLIST: PlaylistParser.parseSearchResult
|
||||
}[category!] || SearchParser.parse
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all possible information of a Song
|
||||
*
|
||||
* @param videoId Video ID
|
||||
* @returns Song Data
|
||||
*/
|
||||
public async getSong(videoId: string): Promise<YTMusic.SongFull> {
|
||||
const data = await this.constructRequest("player", { videoId })
|
||||
|
||||
return SongParser.parse(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all possible information of a Video
|
||||
*
|
||||
* @param videoId Video ID
|
||||
* @returns Video Data
|
||||
*/
|
||||
public async getVideo(videoId: string): Promise<YTMusic.VideoFull> {
|
||||
const data = await this.constructRequest("player", { videoId })
|
||||
|
||||
return VideoParser.parse(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all possible information of an Artist
|
||||
*
|
||||
* @param artistId Artist ID
|
||||
* @returns Artist Data
|
||||
*/
|
||||
public async getArtist(artistId: string): Promise<YTMusic.ArtistFull> {
|
||||
const data = await this.constructRequest("browse", { browseId: artistId })
|
||||
|
||||
return ArtistParser.parse(data, artistId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of Artist's Songs
|
||||
*
|
||||
* @param artistId Artist ID
|
||||
* @returns Artist's Songs
|
||||
*/
|
||||
public async getArtistSongs(artistId: string): Promise<YTMusic.SongDetailed[]> {
|
||||
const artistData = await this.constructRequest("browse", { browseId: artistId })
|
||||
const browseToken = traverse(artistData, "musicShelfRenderer", "title", "browseId")
|
||||
|
||||
const songsData = await this.constructRequest("browse", { browseId: browseToken })
|
||||
const continueToken = traverse(songsData, "continuation")
|
||||
const moreSongsData = await this.constructRequest(
|
||||
"browse",
|
||||
{},
|
||||
{ continuation: continueToken }
|
||||
)
|
||||
|
||||
return [
|
||||
...traverse(songsData, "musicResponsiveListItemRenderer"),
|
||||
...traverse(moreSongsData, "musicResponsiveListItemRenderer")
|
||||
].map(SongParser.parseArtistSong)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of Artist's Albums
|
||||
*
|
||||
* @param artistId Artist ID
|
||||
* @returns Artist's Albums
|
||||
*/
|
||||
public async getArtistAlbums(artistId: string): Promise<YTMusic.AlbumDetailed[]> {
|
||||
const artistData = await this.constructRequest("browse", { browseId: artistId })
|
||||
const artistAlbumsData = traverse(artistData, "musicCarouselShelfRenderer")[0]
|
||||
const browseBody = traverse(artistAlbumsData, "moreContentButton", "browseEndpoint")
|
||||
|
||||
const albumsData = await this.constructRequest("browse", browseBody)
|
||||
|
||||
return traverse(albumsData, "musicTwoRowItemRenderer").map((item: any) =>
|
||||
AlbumParser.parseArtistAlbum(item, {
|
||||
artistId,
|
||||
name: traverse(albumsData, "header", "text").at(0)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all possible information of an Album
|
||||
*
|
||||
* @param albumId Album ID
|
||||
* @returns Album Data
|
||||
*/
|
||||
public async getAlbum(albumId: string): Promise<YTMusic.AlbumFull> {
|
||||
const data = await this.constructRequest("browse", { browseId: albumId })
|
||||
|
||||
return AlbumParser.parse(data, albumId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all possible information of a Playlist except the tracks
|
||||
*
|
||||
* @param playlistId Playlist ID
|
||||
* @returns Playlist Data
|
||||
*/
|
||||
public async getPlaylist(playlistId: string): Promise<YTMusic.PlaylistFull> {
|
||||
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
|
||||
const data = await this.constructRequest("browse", { browseId: playlistId })
|
||||
|
||||
return PlaylistParser.parse(data, playlistId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all videos in a Playlist
|
||||
*
|
||||
* @param playlistId Playlist ID
|
||||
* @returns Playlist's Videos
|
||||
*/
|
||||
public async getPlaylistVideos(
|
||||
playlistId: string
|
||||
): Promise<Omit<YTMusic.VideoDetailed, "views">[]> {
|
||||
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
|
||||
const playlistData = await this.constructRequest("browse", { browseId: playlistId })
|
||||
|
||||
const songs = traverse(
|
||||
playlistData,
|
||||
"musicPlaylistShelfRenderer",
|
||||
"musicResponsiveListItemRenderer"
|
||||
)
|
||||
let continuation = traverse(playlistData, "musicPlaylistShelfRenderer", "continuation")
|
||||
while (true) {
|
||||
if (continuation instanceof Array) break
|
||||
|
||||
const songsData = await this.constructRequest("browse", {}, { continuation })
|
||||
songs.push(...traverse(songsData, "musicResponsiveListItemRenderer"))
|
||||
continuation = traverse(songsData, "continuation")
|
||||
}
|
||||
|
||||
return songs.map(VideoParser.parsePlaylistVideo)
|
||||
}
|
||||
}
|
||||
|
||||
const ytmusicapi = new YTMusic()
|
||||
ytmusicapi.initialize().then(async () => {
|
||||
console.log("Initialized")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
import SongParser from "./SongParser"
|
||||
import traverse from "../utils/traverse"
|
||||
|
||||
export default class AlbumParser {
|
||||
public static parse(data: any, albumId: string): YTMusic.AlbumFull {
|
||||
const albumBasic = {
|
||||
albumId,
|
||||
name: traverse(data, "header", "title", "text").at(0)
|
||||
}
|
||||
const artists = traverse(data, "header", "subtitle", "runs")
|
||||
.filter((run: any) => "navigationEndpoint" in run)
|
||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text }))
|
||||
const thumbnails = [traverse(data, "header", "thumbnails")].flat()
|
||||
const description = traverse(data, "description", "text")
|
||||
|
||||
return {
|
||||
type: "ALBUM",
|
||||
...albumBasic,
|
||||
playlistId: traverse(data, "buttonRenderer", "playlistId"),
|
||||
artists,
|
||||
year: +traverse(data, "header", "subtitle", "text").at(-1),
|
||||
thumbnails,
|
||||
description: description instanceof Array ? null : description,
|
||||
songs: [traverse(data, "musicResponsiveListItemRenderer")]
|
||||
.flat()
|
||||
.map((item: any) =>
|
||||
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): YTMusic.AlbumDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
|
||||
return {
|
||||
type: "ALBUM",
|
||||
albumId: [traverse(item, "browseId")].flat().at(-1),
|
||||
playlistId: traverse(item, "overlay", "playlistId"),
|
||||
artists: traverse(flexColumns[1], "runs")
|
||||
.filter((run: any) => "navigationEndpoint" in run)
|
||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
year: +traverse(flexColumns[1], "runs", "text").at(-1),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
|
||||
public static parseArtistAlbum(
|
||||
item: any,
|
||||
artistBasic: YTMusic.ArtistBasic
|
||||
): YTMusic.AlbumDetailed {
|
||||
return {
|
||||
type: "ALBUM",
|
||||
albumId: [traverse(item, "browseId")].flat().at(-1),
|
||||
playlistId: traverse(item, "thumbnailOverlay", "playlistId"),
|
||||
name: traverse(item, "title", "text").at(0),
|
||||
artists: [artistBasic],
|
||||
year: +traverse(item, "subtitle", "text").at(-1),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
|
||||
public static parseArtistTopAlbums(
|
||||
item: any,
|
||||
artistBasic: YTMusic.ArtistBasic
|
||||
): YTMusic.AlbumDetailed {
|
||||
return {
|
||||
type: "ALBUM",
|
||||
albumId: traverse(item, "browseId").at(-1),
|
||||
playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"),
|
||||
name: traverse(item, "title", "text").at(0),
|
||||
artists: [artistBasic],
|
||||
year: +traverse(item, "subtitle", "text").at(-1),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import AlbumParser from "./AlbumParser"
|
||||
import Parse from "./Parser"
|
||||
import SongParser from "./SongParser"
|
||||
import traverse from "../utils/traverse"
|
||||
|
||||
export default class ArtistParser {
|
||||
public static parse(data: any, artistId: string): YTMusic.ArtistFull {
|
||||
const artistBasic = {
|
||||
artistId,
|
||||
name: traverse(data, "header", "title", "text").at(0)
|
||||
}
|
||||
|
||||
const description = traverse(data, "header", "description", "text")
|
||||
|
||||
return {
|
||||
type: "ARTIST",
|
||||
...artistBasic,
|
||||
thumbnails: [traverse(data, "header", "thumbnails")].flat(),
|
||||
description: description instanceof Array ? null : description,
|
||||
subscribers: Parse.parseNumber(traverse(data, "subscriberCountText", "text")),
|
||||
topSongs: traverse(data, "musicShelfRenderer", "contents").map((item: any) =>
|
||||
SongParser.parseArtistTopSong(item, artistBasic)
|
||||
),
|
||||
topAlbums: [traverse(data, "musicCarouselShelfRenderer")]
|
||||
.flat()
|
||||
.at(0)
|
||||
.contents.map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic))
|
||||
}
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): YTMusic.ArtistDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
|
||||
return {
|
||||
type: "ARTIST",
|
||||
artistId: traverse(item, "browseId"),
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
export default class Parse {
|
||||
public static parseDuration(time: string) {
|
||||
const [seconds, minutes, hours] = time
|
||||
.split(":")
|
||||
.reverse()
|
||||
.map(n => +n) as (number | undefined)[]
|
||||
|
||||
return (seconds || 0) + (minutes || 0) * 60 + (hours || 0) * 60 * 60
|
||||
}
|
||||
|
||||
public static parseNumber(string: string): number {
|
||||
if (string.at(-1)!.match(/^[A-Z]+$/)) {
|
||||
const number = +string.slice(0, -1)
|
||||
const multiplier = string.at(-1)
|
||||
|
||||
return (
|
||||
{
|
||||
K: number * 1000,
|
||||
M: number * 1000 * 1000,
|
||||
B: number * 1000 * 1000 * 1000,
|
||||
T: number * 1000 * 1000 * 1000 * 1000
|
||||
}[multiplier!] || NaN
|
||||
)
|
||||
} else {
|
||||
return +string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import traverse from "../utils/traverse"
|
||||
|
||||
export default class PlaylistParser {
|
||||
public static parse(data: any, playlistId: string): YTMusic.PlaylistFull {
|
||||
return {
|
||||
type: "PLAYLIST",
|
||||
playlistId,
|
||||
name: traverse(data, "header", "title", "text").at(0),
|
||||
artist: {
|
||||
artistId: traverse(data, "header", "subtitle", "browseId"),
|
||||
name: traverse(data, "header", "subtitle", "text").at(2)
|
||||
},
|
||||
videoCount: +traverse(data, "header", "secondSubtitle", "text")
|
||||
.at(0)
|
||||
.split(" ")
|
||||
.at(0)
|
||||
.replaceAll(",", ""),
|
||||
thumbnails: traverse(data, "header", "thumbnails")
|
||||
}
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): YTMusic.PlaylistFull {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const artistId = traverse(flexColumns[1], "browseId")
|
||||
|
||||
return {
|
||||
type: "PLAYLIST",
|
||||
playlistId: traverse(item, "overlay", "playlistId"),
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
artist: {
|
||||
artistId: artistId instanceof Array ? null : artistId,
|
||||
name: traverse(flexColumns[1], "runs", "text").at(-2)
|
||||
},
|
||||
videoCount: +traverse(flexColumns[1], "runs", "text")
|
||||
.at(-1)
|
||||
.split(" ")
|
||||
.at(0)
|
||||
.replaceAll(",", ""),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import AlbumParser from "./AlbumParser"
|
||||
import ArtistParser from "./ArtistParser"
|
||||
import PlaylistParser from "./PlaylistParser"
|
||||
import SongParser from "./SongParser"
|
||||
import traverse from "../utils/traverse"
|
||||
import VideoParser from "./VideoParser"
|
||||
|
||||
export default class SearchParser {
|
||||
public static parse(item: any): YTMusic.SearchResult {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const type = traverse(flexColumns[1], "runs", "text").at(0) as
|
||||
| "Song"
|
||||
| "Video"
|
||||
| "Artist"
|
||||
| "EP"
|
||||
| "Single"
|
||||
| "Album"
|
||||
| "Playlist"
|
||||
|
||||
return {
|
||||
Song: SongParser.parseSearchResult,
|
||||
Video: VideoParser.parseSearchResult,
|
||||
Artist: ArtistParser.parseSearchResult,
|
||||
EP: AlbumParser.parseSearchResult,
|
||||
Single: AlbumParser.parseSearchResult,
|
||||
Album: AlbumParser.parseSearchResult,
|
||||
Playlist: PlaylistParser.parseSearchResult
|
||||
}[type](item)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
import Parser from "./Parser"
|
||||
import traverse from "../utils/traverse"
|
||||
|
||||
export default class SongParser {
|
||||
public static parse(data: any): YTMusic.SongFull {
|
||||
return {
|
||||
type: "SONG",
|
||||
videoId: traverse(data, "videoDetails", "videoId"),
|
||||
name: traverse(data, "videoDetails", "title"),
|
||||
artists: [
|
||||
{
|
||||
artistId: traverse(data, "videoDetails", "channelId"),
|
||||
name: traverse(data, "author")
|
||||
}
|
||||
],
|
||||
duration: +traverse(data, "videoDetails", "lengthSeconds"),
|
||||
thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(),
|
||||
description: traverse(data, "description"),
|
||||
formats: traverse(data, "streamingData", "formats"),
|
||||
adaptiveFormats: traverse(data, "streamingData", "adaptiveFormats")
|
||||
}
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): YTMusic.SongDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
||||
|
||||
return {
|
||||
type: "SONG",
|
||||
videoId: videoId instanceof Array ? null : videoId,
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
artists: traverse(flexColumns[1], "runs")
|
||||
.filter((run: any) => "navigationEndpoint" in run)
|
||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text }))
|
||||
.slice(0, -1),
|
||||
album: {
|
||||
albumId: traverse(item, "browseId").at(-1),
|
||||
name: traverse(flexColumns[1], "runs", "text").at(-3)
|
||||
},
|
||||
duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
|
||||
public static parseArtistSong(item: any): YTMusic.SongDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
||||
|
||||
return {
|
||||
type: "SONG",
|
||||
videoId: videoId instanceof Array ? null : videoId,
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
artists: [traverse(flexColumns[1], "runs")]
|
||||
.flat()
|
||||
.filter((item: any) => "navigationEndpoint" in item)
|
||||
.map((run: any) => ({
|
||||
artistId: traverse(run, "browseId"),
|
||||
name: run.text
|
||||
})),
|
||||
album: {
|
||||
albumId: traverse(flexColumns[2], "browseId"),
|
||||
name: traverse(flexColumns[2], "runs", "text")
|
||||
},
|
||||
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
|
||||
public static parseArtistTopSong(
|
||||
item: any,
|
||||
artistBasic: YTMusic.ArtistBasic
|
||||
): Omit<YTMusic.SongDetailed, "duration"> {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
||||
|
||||
return {
|
||||
type: "SONG",
|
||||
videoId: videoId instanceof Array ? null : videoId,
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
artists: [artistBasic],
|
||||
album: {
|
||||
albumId: traverse(flexColumns[2], "runs", "text"),
|
||||
name: traverse(flexColumns[2], "browseId")
|
||||
},
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
|
||||
public static parseAlbumSong(
|
||||
item: any,
|
||||
artists: YTMusic.ArtistBasic[],
|
||||
albumBasic: YTMusic.AlbumBasic,
|
||||
thumbnails: YTMusic.ThumbnailFull[]
|
||||
): YTMusic.SongDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
||||
|
||||
return {
|
||||
type: "SONG",
|
||||
videoId: videoId instanceof Array ? null : videoId,
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
artists,
|
||||
album: albumBasic,
|
||||
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
|
||||
thumbnails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import Parser from "./Parser"
|
||||
import traverse from "../utils/traverse"
|
||||
|
||||
export default class VideoParser {
|
||||
public static parse(data: any): YTMusic.VideoFull {
|
||||
return {
|
||||
type: "VIDEO",
|
||||
videoId: traverse(data, "videoDetails", "videoId"),
|
||||
name: traverse(data, "videoDetails", "title"),
|
||||
artists: [
|
||||
{
|
||||
artistId: traverse(data, "videoDetails", "channelId"),
|
||||
name: traverse(data, "author")
|
||||
}
|
||||
],
|
||||
views: +traverse(data, "videoDetails", "viewCount"),
|
||||
duration: +traverse(data, "videoDetails", "lengthSeconds"),
|
||||
thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(),
|
||||
description: traverse(data, "description"),
|
||||
unlisted: traverse(data, "unlisted"),
|
||||
familySafe: traverse(data, "familySafe"),
|
||||
paid: traverse(data, "paid"),
|
||||
tags: traverse(data, "tags")
|
||||
}
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): YTMusic.VideoDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playNavigationEndpoint", "videoId")
|
||||
|
||||
return {
|
||||
type: "VIDEO",
|
||||
videoId: videoId instanceof Array ? null : videoId,
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
artists: [traverse(flexColumns[1], "runs")]
|
||||
.flat()
|
||||
.filter((run: any) => "navigationEndpoint" in run)
|
||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
|
||||
views: Parser.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)),
|
||||
duration: Parser.parseDuration(traverse(flexColumns[1], "text").at(-1)),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
|
||||
public static parsePlaylistVideo(item: any): Omit<YTMusic.VideoDetailed, "views"> {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playNavigationEndpoint", "videoId")
|
||||
|
||||
return {
|
||||
type: "VIDEO",
|
||||
videoId: videoId instanceof Array ? null : videoId,
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
artists: [traverse(flexColumns[1], "runs")]
|
||||
.flat()
|
||||
.filter((run: any) => "navigationEndpoint" in run)
|
||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
|
||||
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import Validator from "validate-any/build/classes/Validator"
|
||||
import YTMusic from "../YTMusic"
|
||||
import {
|
||||
ALBUM_DETAILED,
|
||||
ALBUM_FULL,
|
||||
ARTIST_DETAILED,
|
||||
ARTIST_FULL,
|
||||
PLAYLIST_DETAILED,
|
||||
PLAYLIST_VIDEO,
|
||||
SONG_DETAILED,
|
||||
SONG_FULL,
|
||||
VIDEO_DETAILED,
|
||||
VIDEO_FULL
|
||||
} from "./interfaces"
|
||||
import { LIST, validate } from "validate-any"
|
||||
|
||||
const queries = ["Lilac", "Weekend", "Yours Raiden", "Eminem", "Lisa Hannigan"]
|
||||
const ytmusic = new YTMusic()
|
||||
|
||||
ytmusic.initialize().then(() =>
|
||||
queries.forEach(async query => {
|
||||
const [songs, videos, artists, albums, playlists, results] = await Promise.all([
|
||||
ytmusic.search(query, "SONG"),
|
||||
ytmusic.search(query, "VIDEO"),
|
||||
ytmusic.search(query, "ARTIST"),
|
||||
ytmusic.search(query, "ALBUM"),
|
||||
ytmusic.search(query, "PLAYLIST"),
|
||||
ytmusic.search(query)
|
||||
])
|
||||
|
||||
const [song, video, artist, artistSongs, artistAlbums, album, playlist, playlistVideos] =
|
||||
await Promise.all([
|
||||
ytmusic.getSong(songs[0].videoId!),
|
||||
ytmusic.getVideo(videos[0].videoId!),
|
||||
ytmusic.getArtist(artists[0].artistId),
|
||||
ytmusic.getArtistSongs(artists[0].artistId),
|
||||
ytmusic.getArtistAlbums(artists[0].artistId),
|
||||
ytmusic.getAlbum(albums[0].albumId),
|
||||
ytmusic.getPlaylist(playlists[0].playlistId),
|
||||
ytmusic.getPlaylistVideos(playlists[0].playlistId)
|
||||
])
|
||||
|
||||
const tests: [any, Validator<any>][] = [
|
||||
[songs, LIST(SONG_DETAILED)],
|
||||
[videos, LIST(VIDEO_DETAILED)],
|
||||
[artists, LIST(ARTIST_DETAILED)],
|
||||
[albums, LIST(ALBUM_DETAILED)],
|
||||
[playlists, LIST(PLAYLIST_DETAILED)],
|
||||
[
|
||||
results,
|
||||
LIST(
|
||||
ALBUM_DETAILED,
|
||||
ARTIST_DETAILED,
|
||||
PLAYLIST_DETAILED,
|
||||
SONG_DETAILED,
|
||||
VIDEO_DETAILED
|
||||
)
|
||||
],
|
||||
[song, SONG_FULL],
|
||||
[video, VIDEO_FULL],
|
||||
[artist, ARTIST_FULL],
|
||||
[artistSongs, LIST(SONG_DETAILED)],
|
||||
[artistAlbums, LIST(ALBUM_DETAILED)],
|
||||
[album, ALBUM_FULL],
|
||||
[playlist, PLAYLIST_DETAILED],
|
||||
[playlistVideos, LIST(PLAYLIST_VIDEO)]
|
||||
]
|
||||
|
||||
for (const [value, validator] of tests) {
|
||||
const result = validate(value, validator)
|
||||
if (!result.success) {
|
||||
console.log(JSON.stringify(value))
|
||||
console.log(validator.formatSchema())
|
||||
console.log(result.errors)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Valid 🎉 - ${query}`)
|
||||
})
|
||||
)
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import ObjectValidator from "validate-any/build/validators/ObjectValidator"
|
||||
import { BOOLEAN, LIST, NULL, NUMBER, OBJECT, OR, STRING } from "validate-any"
|
||||
|
||||
export const THUMBNAIL_FULL: ObjectValidator<YTMusic.ThumbnailFull> = OBJECT({
|
||||
url: STRING(),
|
||||
width: NUMBER(),
|
||||
height: NUMBER()
|
||||
})
|
||||
|
||||
export const ARTIST_BASIC: ObjectValidator<YTMusic.ArtistBasic> = OBJECT({
|
||||
artistId: OR(STRING(), NULL()),
|
||||
name: STRING()
|
||||
})
|
||||
|
||||
export const ALBUM_BASIC: ObjectValidator<YTMusic.AlbumBasic> = OBJECT({
|
||||
albumId: STRING(),
|
||||
name: STRING()
|
||||
})
|
||||
|
||||
export const SONG_DETAILED: ObjectValidator<YTMusic.SongDetailed> = OBJECT({
|
||||
type: STRING("SONG"),
|
||||
videoId: OR(STRING(), NULL()),
|
||||
name: STRING(),
|
||||
artists: LIST(ARTIST_BASIC),
|
||||
album: ALBUM_BASIC,
|
||||
duration: NUMBER(),
|
||||
thumbnails: LIST(THUMBNAIL_FULL)
|
||||
})
|
||||
|
||||
export const VIDEO_DETAILED: ObjectValidator<YTMusic.VideoDetailed> = OBJECT({
|
||||
type: STRING("VIDEO"),
|
||||
videoId: OR(STRING(), NULL()),
|
||||
name: STRING(),
|
||||
artists: LIST(ARTIST_BASIC),
|
||||
views: NUMBER(),
|
||||
duration: NUMBER(),
|
||||
thumbnails: LIST(THUMBNAIL_FULL)
|
||||
})
|
||||
|
||||
export const ARTIST_DETAILED: ObjectValidator<YTMusic.ArtistDetailed> = OBJECT({
|
||||
artistId: STRING(),
|
||||
name: STRING(),
|
||||
type: STRING("ARTIST"),
|
||||
thumbnails: LIST(THUMBNAIL_FULL)
|
||||
})
|
||||
|
||||
export const ALBUM_DETAILED: ObjectValidator<YTMusic.AlbumDetailed> = OBJECT({
|
||||
type: STRING("ALBUM"),
|
||||
albumId: STRING(),
|
||||
playlistId: STRING(),
|
||||
name: STRING(),
|
||||
artists: LIST(ARTIST_BASIC),
|
||||
year: NUMBER(),
|
||||
thumbnails: LIST(THUMBNAIL_FULL)
|
||||
})
|
||||
|
||||
export const SONG_FULL: ObjectValidator<YTMusic.SongFull> = OBJECT({
|
||||
type: STRING("SONG"),
|
||||
videoId: OR(STRING(), NULL()),
|
||||
name: STRING(),
|
||||
artists: LIST(ARTIST_BASIC),
|
||||
duration: NUMBER(),
|
||||
thumbnails: LIST(THUMBNAIL_FULL),
|
||||
description: STRING(),
|
||||
formats: LIST(OBJECT()),
|
||||
adaptiveFormats: LIST(OBJECT())
|
||||
})
|
||||
|
||||
export const VIDEO_FULL: ObjectValidator<YTMusic.VideoFull> = OBJECT({
|
||||
type: STRING("VIDEO"),
|
||||
videoId: OR(STRING(), NULL()),
|
||||
name: STRING(),
|
||||
artists: LIST(ARTIST_BASIC),
|
||||
views: NUMBER(),
|
||||
duration: NUMBER(),
|
||||
thumbnails: LIST(THUMBNAIL_FULL),
|
||||
description: STRING(),
|
||||
unlisted: BOOLEAN(),
|
||||
familySafe: BOOLEAN(),
|
||||
paid: BOOLEAN(),
|
||||
tags: LIST(STRING())
|
||||
})
|
||||
|
||||
export const ARTIST_FULL: ObjectValidator<YTMusic.ArtistFull> = OBJECT({
|
||||
artistId: STRING(),
|
||||
name: STRING(),
|
||||
type: STRING("ARTIST"),
|
||||
thumbnails: LIST(THUMBNAIL_FULL),
|
||||
description: OR(STRING(), NULL()),
|
||||
subscribers: NUMBER(),
|
||||
topSongs: LIST(
|
||||
OBJECT({
|
||||
type: STRING("SONG"),
|
||||
videoId: STRING(),
|
||||
name: STRING(),
|
||||
artists: LIST(ARTIST_BASIC),
|
||||
album: ALBUM_BASIC,
|
||||
thumbnails: LIST(THUMBNAIL_FULL)
|
||||
})
|
||||
),
|
||||
topAlbums: LIST(ALBUM_DETAILED)
|
||||
})
|
||||
|
||||
export const ALBUM_FULL: ObjectValidator<YTMusic.AlbumFull> = OBJECT({
|
||||
type: STRING("ALBUM"),
|
||||
albumId: STRING(),
|
||||
playlistId: STRING(),
|
||||
name: STRING(),
|
||||
artists: LIST(ARTIST_BASIC),
|
||||
year: NUMBER(),
|
||||
thumbnails: LIST(THUMBNAIL_FULL),
|
||||
description: OR(STRING(), NULL()),
|
||||
songs: LIST(SONG_DETAILED)
|
||||
})
|
||||
|
||||
export const PLAYLIST_DETAILED: ObjectValidator<YTMusic.PlaylistDetailed> = OBJECT({
|
||||
type: STRING("PLAYLIST"),
|
||||
playlistId: STRING(),
|
||||
name: STRING(),
|
||||
artist: ARTIST_BASIC,
|
||||
videoCount: NUMBER(),
|
||||
thumbnails: LIST(THUMBNAIL_FULL)
|
||||
})
|
||||
|
||||
export const PLAYLIST_VIDEO: ObjectValidator<Omit<YTMusic.VideoDetailed, "views">> = OBJECT({
|
||||
type: STRING("VIDEO"),
|
||||
videoId: OR(STRING(), NULL()),
|
||||
name: STRING(),
|
||||
artists: LIST(ARTIST_BASIC),
|
||||
duration: NUMBER(),
|
||||
thumbnails: LIST(THUMBNAIL_FULL)
|
||||
})
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import YTMusic from "../YTMusic"
|
||||
|
||||
const ytmusic = new YTMusic()
|
||||
ytmusic.initialize().then(() => {
|
||||
ytmusic.search("Lilac", "SONG").then(res => {
|
||||
ytmusic.getSong(res.find(r => !!r.videoId)!.videoId!).then(res => {
|
||||
console.log(res)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
const traverse = (data: any, keys: string, single = false) => {
|
||||
const again = (data: any, key: string): any => {
|
||||
var res = []
|
||||
|
||||
data.hasOwnProperty(key) && res.push(data[key])
|
||||
if (single && data.hasOwnProperty(key)) {
|
||||
return res.shift()
|
||||
}
|
||||
|
||||
if (data instanceof Array) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
res = res.concat(again(data[i], key))
|
||||
}
|
||||
} else if (data instanceof Object) {
|
||||
const c = Object.keys(data)
|
||||
if (c.length > 0) {
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
res = res.concat(again(data[c[i]], key))
|
||||
}
|
||||
}
|
||||
}
|
||||
return res.length == 1 ? res.shift() : res
|
||||
}
|
||||
|
||||
let z = keys.split(":"),
|
||||
value = data
|
||||
for (let i = 0; i < z.length; i++) {
|
||||
value = again(value, z[i])
|
||||
}
|
||||
console.log(value)
|
||||
}
|
||||
|
||||
traverse(require("./data.json"), "playNavigationEndpoint:videoId")
|
||||
|
||||
export default {}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
const traverse = (data: any, keys: string[], single: boolean = false) => {
|
||||
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
|
||||
}
|
||||
|
||||
traverse(require("./data.json")[0], ["playNavigationEndpoint", "videoId"], true)
|
||||
|
||||
export default {}
|
||||
|
|
@ -7,7 +7,7 @@ declare namespace YTMusic {
|
|||
|
||||
interface SongDetailed {
|
||||
type: "SONG"
|
||||
videoId: string
|
||||
videoId: string | null
|
||||
name: string
|
||||
artists: ArtistBasic[]
|
||||
album: AlbumBasic
|
||||
|
|
@ -15,26 +15,48 @@ declare namespace YTMusic {
|
|||
thumbnails: ThumbnailFull[]
|
||||
}
|
||||
|
||||
interface SongFull extends Omit<SongDetailed, "album"> {
|
||||
description: string
|
||||
formats: any[]
|
||||
adaptiveFormats: any[]
|
||||
}
|
||||
|
||||
interface VideoDetailed {
|
||||
type: "VIDEO"
|
||||
videoId: string
|
||||
videoId: string | null
|
||||
name: string
|
||||
artist: ArtistBasic
|
||||
artists: ArtistBasic[]
|
||||
views: number
|
||||
duration: number
|
||||
thumbnails: ThumbnailFull[]
|
||||
}
|
||||
|
||||
interface VideoFull extends VideoDetailed {
|
||||
description: string
|
||||
unlisted: boolean
|
||||
familySafe: boolean
|
||||
paid: boolean
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
interface ArtistBasic {
|
||||
artistId: string
|
||||
artistId: string | null
|
||||
name: string
|
||||
}
|
||||
|
||||
interface ArtistDetailed extends ArtistBasic {
|
||||
type: "ARTIST"
|
||||
artistId: string
|
||||
thumbnails: ThumbnailFull[]
|
||||
}
|
||||
|
||||
interface ArtistFull extends ArtistDetailed {
|
||||
description: string | null
|
||||
subscribers: number
|
||||
topSongs: Omit<SongDetailed, "duration">[]
|
||||
topAlbums: AlbumDetailed[]
|
||||
}
|
||||
|
||||
interface AlbumBasic {
|
||||
albumId: string
|
||||
name: string
|
||||
|
|
@ -48,20 +70,24 @@ declare namespace YTMusic {
|
|||
thumbnails: ThumbnailFull[]
|
||||
}
|
||||
|
||||
interface PlaylistDetailed {
|
||||
interface AlbumFull extends AlbumDetailed {
|
||||
description: string | null
|
||||
songs: SongDetailed[]
|
||||
}
|
||||
|
||||
interface PlaylistFull {
|
||||
type: "PLAYLIST"
|
||||
playlistId: string
|
||||
name: string
|
||||
artist: ArtistBasic
|
||||
trackCount: number
|
||||
videoCount: number
|
||||
thumbnails: ThumbnailFull[]
|
||||
}
|
||||
|
||||
type SearchResult =
|
||||
| SongDetailed
|
||||
| PlaylistDetailed
|
||||
| VideoDetailed
|
||||
| AlbumDetailed
|
||||
| ArtistDetailed
|
||||
| PlaylistDetailed
|
||||
| PlaylistFull
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,175 +0,0 @@
|
|||
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]()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,5 @@
|
|||
"outDir": "build",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.test.*"
|
||||
]
|
||||
}
|
||||
"exclude": ["**/*.test.*", "**/tests"]
|
||||
}
|
||||
|
|
|
|||
162
yarn.lock
162
yarn.lock
|
|
@ -44,6 +44,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40"
|
||||
integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
|
||||
|
|
@ -73,6 +78,37 @@ axios@^0.24.0:
|
|||
dependencies:
|
||||
follow-redirects "^1.14.4"
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
commander@^2.19.0:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
config-chain@^1.1.12:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.13.tgz#fad0795aa6a6cdaff9ed1b68e9dff94372c232f4"
|
||||
integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==
|
||||
dependencies:
|
||||
ini "^1.3.4"
|
||||
proto-list "~1.2.1"
|
||||
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
|
|
@ -90,6 +126,16 @@ diff@^4.0.1:
|
|||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
editorconfig@^0.15.3:
|
||||
version "0.15.3"
|
||||
resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5"
|
||||
integrity sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==
|
||||
dependencies:
|
||||
commander "^2.19.0"
|
||||
lru-cache "^4.1.5"
|
||||
semver "^5.6.0"
|
||||
sigmund "^1.0.1"
|
||||
|
||||
follow-redirects@1.5.10:
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
||||
|
|
@ -102,26 +148,115 @@ follow-redirects@^1.14.4:
|
|||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd"
|
||||
integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==
|
||||
|
||||
fs.realpath@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
glob@^7.1.3:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
|
||||
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.4"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
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=
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
|
||||
dependencies:
|
||||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ini@^1.3.4:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
|
||||
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
|
||||
|
||||
js-beautify@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.0.tgz#2ce790c555d53ce1e3d7363227acf5dc69024c2d"
|
||||
integrity sha512-yuck9KirNSCAwyNJbqW+BxJqJ0NLJ4PwBUzQQACl5O3qHMBXVkXb/rD0ilh/Lat/tn88zSZ+CAHOlk0DsY7GuQ==
|
||||
dependencies:
|
||||
config-chain "^1.1.12"
|
||||
editorconfig "^0.15.3"
|
||||
glob "^7.1.3"
|
||||
nopt "^5.0.0"
|
||||
|
||||
lodash@^4.17.15:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lru-cache@^4.1.5:
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
|
||||
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
|
||||
dependencies:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
|
||||
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==
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
|
||||
|
||||
nopt@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
|
||||
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
|
||||
dependencies:
|
||||
abbrev "1"
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
path-is-absolute@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
|
||||
|
||||
proto-list@~1.2.1:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||
integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=
|
||||
|
||||
pseudomap@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||
integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
|
||||
|
||||
psl@^1.1.33:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
|
||||
|
|
@ -132,6 +267,16 @@ punycode@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
semver@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
sigmund@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
|
||||
integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=
|
||||
|
||||
tough-cookie@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4"
|
||||
|
|
@ -169,6 +314,23 @@ universalify@^0.1.2:
|
|||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||
|
||||
validate-any@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/validate-any/-/validate-any-1.1.1.tgz#0845d9e8f2b44e342633e6a73ffa21973b71507f"
|
||||
integrity sha512-L3bCSEn/eH/mdPp+hFQg4nTBeMr19V4kbKMxMSOwRlbMA6N6LWi52Duz0gMQK9gJkz8VbMcvx5Yka2u9J8epjQ==
|
||||
dependencies:
|
||||
js-beautify "^1.14.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
yallist@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
|
|
|
|||
Loading…
Reference in New Issue