Merge branch 'type-safety' into main
This commit is contained in:
commit
ec98127f62
|
|
@ -3,5 +3,4 @@
|
||||||
.gitignore
|
.gitignore
|
||||||
.prettierrc
|
.prettierrc
|
||||||
.editorconfig
|
.editorconfig
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
babel.config.js
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
// 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": "testing/run.ts",
|
|
||||||
"program": "${workspaceFolder}/src/testing/run.ts",
|
|
||||||
"sourceMaps": true,
|
|
||||||
"skipFiles": ["<node_internals>/**"],
|
|
||||||
"runtimeExecutable": "node",
|
|
||||||
"runtimeArgs": ["--require", "ts-node/register/files"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "tests/all.ts",
|
|
||||||
"program": "${workspaceFolder}/src/tests/all.ts",
|
|
||||||
"sourceMaps": true,
|
|
||||||
"skipFiles": ["<node_internals>/**"],
|
|
||||||
"runtimeExecutable": "node",
|
|
||||||
"runtimeArgs": ["--require", "ts-node/register/files"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
37
README.md
37
README.md
|
|
@ -22,14 +22,6 @@ Because of this, I decided to build my own version of a youtube music api with T
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
With `yarn`
|
|
||||||
|
|
||||||
```
|
|
||||||
$ yarn add ytmusic-api
|
|
||||||
```
|
|
||||||
|
|
||||||
With `npm`
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm i ytmusic-api
|
$ npm i ytmusic-api
|
||||||
```
|
```
|
||||||
|
|
@ -294,7 +286,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
||||||
| Name | Data Type | Description |
|
| Name | Data Type | Description |
|
||||||
| :--------- | :---------------------------------- | :------------------ |
|
| :--------- | :---------------------------------- | :------------------ |
|
||||||
| type | `"SONG"` | Type of data |
|
| type | `"SONG"` | Type of data |
|
||||||
| videoId | `string \| null` | YouTube Video ID |
|
| videoId | `string` | YouTube Video ID |
|
||||||
| name | `string` | Name |
|
| name | `string` | Name |
|
||||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Artists |
|
| artists | [ArtistBasic](#ArtistBasic)`[]` | Artists |
|
||||||
| album | [AlbumBasic](#AlbumBasic) | Album |
|
| album | [AlbumBasic](#AlbumBasic) | Album |
|
||||||
|
|
@ -306,7 +298,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
||||||
| Name | Data Type | Description |
|
| Name | Data Type | Description |
|
||||||
| :-------------- | :---------------------------------- | :--------------------- |
|
| :-------------- | :---------------------------------- | :--------------------- |
|
||||||
| type | `"SONG"` | Type of data |
|
| type | `"SONG"` | Type of data |
|
||||||
| videoId | `string \| null` | YouTube Video ID |
|
| videoId | `string` | YouTube Video ID |
|
||||||
| name | `string` | Name |
|
| name | `string` | Name |
|
||||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Artists |
|
| artists | [ArtistBasic](#ArtistBasic)`[]` | Artists |
|
||||||
| duration | `number` | Duration in seconds |
|
| duration | `number` | Duration in seconds |
|
||||||
|
|
@ -320,7 +312,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
||||||
| Name | Data Type | Description |
|
| Name | Data Type | Description |
|
||||||
| :--------- | :---------------------------------- | :------------------------------ |
|
| :--------- | :---------------------------------- | :------------------------------ |
|
||||||
| type | `"VIDEO"` | Type of data |
|
| type | `"VIDEO"` | Type of data |
|
||||||
| videoId | `string \| null` | YouTube Video ID |
|
| videoId | `string` | YouTube Video ID |
|
||||||
| name | `string` | Name |
|
| name | `string` | Name |
|
||||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Channels that created the video |
|
| artists | [ArtistBasic](#ArtistBasic)`[]` | Channels that created the video |
|
||||||
| views | `number` | View count |
|
| views | `number` | View count |
|
||||||
|
|
@ -332,7 +324,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
||||||
| Name | Data Type | Description |
|
| Name | Data Type | Description |
|
||||||
| :---------- | :---------------------------------- | :------------------------------------- |
|
| :---------- | :---------------------------------- | :------------------------------------- |
|
||||||
| type | `"VIDEO"` | Type of data |
|
| type | `"VIDEO"` | Type of data |
|
||||||
| videoId | `string \| null` | YouTube Video ID |
|
| videoId | `string` | YouTube Video ID |
|
||||||
| name | `string` | Name |
|
| name | `string` | Name |
|
||||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Channels that created the video |
|
| artists | [ArtistBasic](#ArtistBasic)`[]` | Channels that created the video |
|
||||||
| views | `number` | View count |
|
| views | `number` | View count |
|
||||||
|
|
@ -348,7 +340,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
||||||
|
|
||||||
| Name | Data Type | Description |
|
| Name | Data Type | Description |
|
||||||
| :------- | :--------------- | :---------- |
|
| :------- | :--------------- | :---------- |
|
||||||
| artistId | `string \| null` | Artist ID |
|
| artistId | `string` | Artist ID |
|
||||||
| name | `string` | Name |
|
| name | `string` | Name |
|
||||||
|
|
||||||
#### `ArtistDetailed`
|
#### `ArtistDetailed`
|
||||||
|
|
@ -368,7 +360,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
||||||
| artistId | `string` | Artist ID |
|
| artistId | `string` | Artist ID |
|
||||||
| name | `string` | Name |
|
| name | `string` | Name |
|
||||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||||
| description | `string \| null` | Description |
|
| description | `string` | Description |
|
||||||
| subscribers | `number` | Number of subscribers the Artist has |
|
| subscribers | `number` | Number of subscribers the Artist has |
|
||||||
| topSongs | `Omit<`[SongDetailed](#SongDetailed)`, "duration">[]` | Top Songs from Artist |
|
| topSongs | `Omit<`[SongDetailed](#SongDetailed)`, "duration">[]` | Top Songs from Artist |
|
||||||
| topAlbums | [AlbumDetailed](#AlbumDetailed)`[]` | Top Albums from Artist |
|
| topAlbums | [AlbumDetailed](#AlbumDetailed)`[]` | Top Albums from Artist |
|
||||||
|
|
@ -403,7 +395,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
||||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Creators of the Album |
|
| artists | [ArtistBasic](#ArtistBasic)`[]` | Creators of the Album |
|
||||||
| year | `number` | Publication Year |
|
| year | `number` | Publication Year |
|
||||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||||
| description | `string \| null` | Description |
|
| description | `string` | Description |
|
||||||
| songs | [SongDetailed](#SongDetailed)`[]` | Songs in the Album |
|
| songs | [SongDetailed](#SongDetailed)`[]` | Songs in the Album |
|
||||||
|
|
||||||
#### `PlaylistFull`
|
#### `PlaylistFull`
|
||||||
|
|
@ -426,7 +418,7 @@ A lot of the credit should go to [youtube-music-api](https://npmjs.com/package/y
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
YTMusic API's data return types are tested with Jest. To run the tests, run the command
|
YTMusic API's data return types are tested with Mocha. To run the tests, run the command
|
||||||
|
|
||||||
```
|
```
|
||||||
$ npm run test
|
$ npm run test
|
||||||
|
|
@ -435,18 +427,17 @@ $ npm run test
|
||||||
## Built with
|
## Built with
|
||||||
|
|
||||||
- TypeScript
|
- TypeScript
|
||||||
- [](https://npmjs.com/package/@types/jest)
|
- [](https://npmjs.com/package/@types/mocha)
|
||||||
|
- [](https://npmjs.com/package/@types/node)
|
||||||
- [](https://npmjs.com/package/@types/tough-cookie)
|
- [](https://npmjs.com/package/@types/tough-cookie)
|
||||||
- [](https://npmjs.com/package/typescript)
|
- [](https://npmjs.com/package/typescript)
|
||||||
- Axios
|
- Axios
|
||||||
- [](https://npmjs.com/package/axios)
|
- [](https://npmjs.com/package/axios)
|
||||||
- Tough Cookie
|
- Tough Cookie
|
||||||
- [](https://npmjs.com/package/tough-cookie)
|
- [](https://npmjs.com/package/tough-cookie)
|
||||||
- Jest
|
- Mocha
|
||||||
- [](https://npmjs.com/package/@babel/core)
|
- [](https://npmjs.com/package/mocha)
|
||||||
- [](https://npmjs.com/package/@babel/preset-env)
|
- [](https://npmjs.com/package/mocha.parallel)
|
||||||
- [](https://npmjs.com/package/@babel/preset-typescript)
|
- [](https://npmjs.com/package/ts-mocha)
|
||||||
- [](https://npmjs.com/package/babel-jest)
|
|
||||||
- [](https://npmjs.com/package/jest)
|
|
||||||
- Miscellaneous
|
- Miscellaneous
|
||||||
- [](https://npmjs.com/package/validate-any)
|
- [](https://npmjs.com/package/validate-any)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
["@babel/preset-env", { targets: { node: "current" } }],
|
|
||||||
"@babel/preset-typescript"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
17
package.json
17
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ytmusic-api",
|
"name": "ytmusic-api",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"description": "YouTube Music API",
|
"description": "YouTube Music API",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|
@ -11,22 +11,21 @@
|
||||||
"url": "https://github.com/zS1L3NT/ts-npm-ytmusic-api"
|
"url": "https://github.com/zS1L3NT/ts-npm-ytmusic-api"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest"
|
"test": "ts-mocha --timeout 10000 src/__tests__/**/*.spec.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.25.0",
|
"axios": "^0.25.0",
|
||||||
"tough-cookie": "^4.0.0"
|
"tough-cookie": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.0",
|
"@types/mocha": "^9.1.0",
|
||||||
"@babel/preset-env": "^7.16.11",
|
"@types/node": "^17.0.23",
|
||||||
"@babel/preset-typescript": "^7.16.7",
|
|
||||||
"@types/jest": "^27.4.0",
|
|
||||||
"@types/tough-cookie": "^4.0.1",
|
"@types/tough-cookie": "^4.0.1",
|
||||||
"babel-jest": "^27.4.6",
|
"mocha": "^9.2.2",
|
||||||
"jest": "^27.4.7",
|
"mocha.parallel": "^0.15.6",
|
||||||
|
"ts-mocha": "^9.0.2",
|
||||||
"typescript": "^4.5.5",
|
"typescript": "^4.5.5",
|
||||||
"validate-any": "^1.2.0"
|
"validate-any": "1.3.1"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"youtube",
|
"youtube",
|
||||||
|
|
|
||||||
3512
pnpm-lock.yaml
3512
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1 @@
|
||||||
|
declare module "mocha.parallel"
|
||||||
|
|
@ -4,7 +4,9 @@ import axios, { AxiosInstance } from "axios"
|
||||||
import PlaylistParser from "./parsers/PlaylistParser"
|
import PlaylistParser from "./parsers/PlaylistParser"
|
||||||
import SearchParser from "./parsers/SearchParser"
|
import SearchParser from "./parsers/SearchParser"
|
||||||
import SongParser from "./parsers/SongParser"
|
import SongParser from "./parsers/SongParser"
|
||||||
import traverse from "./traverse"
|
import traverse from "./utils/traverse"
|
||||||
|
import traverseList from "./utils/traverseList"
|
||||||
|
import traverseString from "./utils/traverseString"
|
||||||
import VideoParser from "./parsers/VideoParser"
|
import VideoParser from "./parsers/VideoParser"
|
||||||
import {
|
import {
|
||||||
AlbumDetailed,
|
AlbumDetailed,
|
||||||
|
|
@ -203,7 +205,7 @@ export default class YTMusic {
|
||||||
* @returns Search suggestions
|
* @returns Search suggestions
|
||||||
*/
|
*/
|
||||||
public async getSearchSuggestions(query: string): Promise<string[]> {
|
public async getSearchSuggestions(query: string): Promise<string[]> {
|
||||||
return traverse(
|
return traverseList(
|
||||||
await this.constructRequest("music/get_search_suggestions", {
|
await this.constructRequest("music/get_search_suggestions", {
|
||||||
input: query
|
input: query
|
||||||
}),
|
}),
|
||||||
|
|
@ -236,7 +238,7 @@ export default class YTMusic {
|
||||||
}[category!] || null
|
}[category!] || null
|
||||||
})
|
})
|
||||||
|
|
||||||
return [traverse(searchData, "musicResponsiveListItemRenderer")].flat().map(
|
return traverseList(searchData, "musicResponsiveListItemRenderer").map(
|
||||||
{
|
{
|
||||||
SONG: SongParser.parseSearchResult,
|
SONG: SongParser.parseSearchResult,
|
||||||
VIDEO: VideoParser.parseSearchResult,
|
VIDEO: VideoParser.parseSearchResult,
|
||||||
|
|
@ -308,8 +310,8 @@ export default class YTMusic {
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...traverse(songsData, "musicResponsiveListItemRenderer"),
|
...traverseList(songsData, "musicResponsiveListItemRenderer"),
|
||||||
...traverse(moreSongsData, "musicResponsiveListItemRenderer")
|
...traverseList(moreSongsData, "musicResponsiveListItemRenderer")
|
||||||
].map(SongParser.parseArtistSong)
|
].map(SongParser.parseArtistSong)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,15 +323,15 @@ export default class YTMusic {
|
||||||
*/
|
*/
|
||||||
public async getArtistAlbums(artistId: string): Promise<AlbumDetailed[]> {
|
public async getArtistAlbums(artistId: string): Promise<AlbumDetailed[]> {
|
||||||
const artistData = await this.constructRequest("browse", { browseId: artistId })
|
const artistData = await this.constructRequest("browse", { browseId: artistId })
|
||||||
const artistAlbumsData = traverse(artistData, "musicCarouselShelfRenderer")[0]
|
const artistAlbumsData = traverseList(artistData, "musicCarouselShelfRenderer")[0]
|
||||||
const browseBody = traverse(artistAlbumsData, "moreContentButton", "browseEndpoint")
|
const browseBody = traverse(artistAlbumsData, "moreContentButton", "browseEndpoint")
|
||||||
|
|
||||||
const albumsData = await this.constructRequest("browse", browseBody)
|
const albumsData = await this.constructRequest("browse", browseBody)
|
||||||
|
|
||||||
return traverse(albumsData, "musicTwoRowItemRenderer").map((item: any) =>
|
return traverseList(albumsData, "musicTwoRowItemRenderer").map(item =>
|
||||||
AlbumParser.parseArtistAlbum(item, {
|
AlbumParser.parseArtistAlbum(item, {
|
||||||
artistId,
|
artistId,
|
||||||
name: traverse(albumsData, "header", "text").at(0)
|
name: traverseString(albumsData, "header", "text")()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +371,7 @@ export default class YTMusic {
|
||||||
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
|
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
|
||||||
const playlistData = await this.constructRequest("browse", { browseId: playlistId })
|
const playlistData = await this.constructRequest("browse", { browseId: playlistId })
|
||||||
|
|
||||||
const songs = traverse(
|
const songs = traverseList(
|
||||||
playlistData,
|
playlistData,
|
||||||
"musicPlaylistShelfRenderer",
|
"musicPlaylistShelfRenderer",
|
||||||
"musicResponsiveListItemRenderer"
|
"musicResponsiveListItemRenderer"
|
||||||
|
|
@ -379,7 +381,7 @@ export default class YTMusic {
|
||||||
if (continuation instanceof Array) break
|
if (continuation instanceof Array) break
|
||||||
|
|
||||||
const songsData = await this.constructRequest("browse", {}, { continuation })
|
const songsData = await this.constructRequest("browse", {}, { continuation })
|
||||||
songs.push(...traverse(songsData, "musicResponsiveListItemRenderer"))
|
songs.push(...traverseList(songsData, "musicResponsiveListItemRenderer"))
|
||||||
continuation = traverse(songsData, "continuation")
|
continuation = traverse(songsData, "continuation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,349 +1,122 @@
|
||||||
import ObjectValidator from "validate-any/dist/validators/ObjectValidator"
|
import assert from "assert"
|
||||||
|
import describeParallel from "mocha.parallel"
|
||||||
import Validator from "validate-any/dist/classes/Validator"
|
import Validator from "validate-any/dist/classes/Validator"
|
||||||
import YTMusic, {
|
import YTMusic from ".."
|
||||||
AlbumBasic,
|
|
||||||
AlbumDetailed,
|
|
||||||
AlbumFull,
|
|
||||||
ArtistBasic,
|
|
||||||
ArtistDetailed,
|
|
||||||
ArtistFull,
|
|
||||||
PlaylistFull,
|
|
||||||
SongDetailed,
|
|
||||||
SongFull,
|
|
||||||
ThumbnailFull,
|
|
||||||
VideoDetailed,
|
|
||||||
VideoFull
|
|
||||||
} from ".."
|
|
||||||
import {
|
import {
|
||||||
BOOLEAN,
|
ALBUM_DETAILED,
|
||||||
iValidationError,
|
ALBUM_FULL,
|
||||||
LIST,
|
ARTIST_DETAILED,
|
||||||
NULL,
|
ARTIST_FULL,
|
||||||
NUMBER,
|
PLAYLIST_FULL,
|
||||||
OBJECT,
|
PLAYLIST_VIDEO,
|
||||||
OR,
|
SONG_DETAILED,
|
||||||
STRING,
|
SONG_FULL,
|
||||||
validate
|
VIDEO_DETAILED,
|
||||||
} from "validate-any"
|
VIDEO_FULL
|
||||||
|
} from "../interfaces"
|
||||||
//#region Interfaces
|
import { iValidationError, LIST, STRING, validate } from "validate-any"
|
||||||
const THUMBNAIL_FULL: ObjectValidator<ThumbnailFull> = OBJECT({
|
|
||||||
url: STRING(),
|
|
||||||
width: NUMBER(),
|
|
||||||
height: NUMBER()
|
|
||||||
})
|
|
||||||
|
|
||||||
const ARTIST_BASIC: ObjectValidator<ArtistBasic> = OBJECT({
|
|
||||||
artistId: OR(STRING(), NULL()),
|
|
||||||
name: STRING()
|
|
||||||
})
|
|
||||||
|
|
||||||
const ALBUM_BASIC: ObjectValidator<AlbumBasic> = OBJECT({
|
|
||||||
albumId: STRING(),
|
|
||||||
name: STRING()
|
|
||||||
})
|
|
||||||
|
|
||||||
const SONG_DETAILED: ObjectValidator<SongDetailed> = OBJECT({
|
|
||||||
type: STRING("SONG"),
|
|
||||||
videoId: OR(STRING(), NULL()),
|
|
||||||
name: STRING(),
|
|
||||||
artists: LIST(ARTIST_BASIC),
|
|
||||||
album: ALBUM_BASIC,
|
|
||||||
duration: NUMBER(),
|
|
||||||
thumbnails: LIST(THUMBNAIL_FULL)
|
|
||||||
})
|
|
||||||
|
|
||||||
const VIDEO_DETAILED: ObjectValidator<VideoDetailed> = OBJECT({
|
|
||||||
type: STRING("VIDEO"),
|
|
||||||
videoId: OR(STRING(), NULL()),
|
|
||||||
name: STRING(),
|
|
||||||
artists: LIST(ARTIST_BASIC),
|
|
||||||
views: NUMBER(),
|
|
||||||
duration: NUMBER(),
|
|
||||||
thumbnails: LIST(THUMBNAIL_FULL)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ARTIST_DETAILED: ObjectValidator<ArtistDetailed> = OBJECT({
|
|
||||||
artistId: STRING(),
|
|
||||||
name: STRING(),
|
|
||||||
type: STRING("ARTIST"),
|
|
||||||
thumbnails: LIST(THUMBNAIL_FULL)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ALBUM_DETAILED: ObjectValidator<AlbumDetailed> = OBJECT({
|
|
||||||
type: STRING("ALBUM"),
|
|
||||||
albumId: STRING(),
|
|
||||||
playlistId: STRING(),
|
|
||||||
name: STRING(),
|
|
||||||
artists: LIST(ARTIST_BASIC),
|
|
||||||
year: NUMBER(),
|
|
||||||
thumbnails: LIST(THUMBNAIL_FULL)
|
|
||||||
})
|
|
||||||
|
|
||||||
const SONG_FULL: ObjectValidator<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())
|
|
||||||
})
|
|
||||||
|
|
||||||
const VIDEO_FULL: ObjectValidator<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())
|
|
||||||
})
|
|
||||||
|
|
||||||
const ARTIST_FULL: ObjectValidator<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)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ALBUM_FULL: ObjectValidator<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)
|
|
||||||
})
|
|
||||||
|
|
||||||
const PLAYLIST_DETAILED: ObjectValidator<PlaylistFull> = OBJECT({
|
|
||||||
type: STRING("PLAYLIST"),
|
|
||||||
playlistId: STRING(),
|
|
||||||
name: STRING(),
|
|
||||||
artist: ARTIST_BASIC,
|
|
||||||
videoCount: NUMBER(),
|
|
||||||
thumbnails: LIST(THUMBNAIL_FULL)
|
|
||||||
})
|
|
||||||
|
|
||||||
const PLAYLIST_VIDEO: ObjectValidator<Omit<VideoDetailed, "views">> = OBJECT({
|
|
||||||
type: STRING("VIDEO"),
|
|
||||||
videoId: OR(STRING(), NULL()),
|
|
||||||
name: STRING(),
|
|
||||||
artists: LIST(ARTIST_BASIC),
|
|
||||||
duration: NUMBER(),
|
|
||||||
thumbnails: LIST(THUMBNAIL_FULL)
|
|
||||||
})
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
const issues: iValidationError[][] = []
|
const issues: iValidationError[][] = []
|
||||||
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
|
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
|
||||||
const _expect = (data: any, validator: Validator<any>) => {
|
const expect = (data: any, validator: Validator<any>) => {
|
||||||
const { errors } = validate(data, validator)
|
const { errors } = validate(data, validator)
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
issues.push(errors)
|
issues.push(errors)
|
||||||
}
|
}
|
||||||
|
assert.equal(errors.length, 0)
|
||||||
expect(errors.length).toBe(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ytmusic = new YTMusic()
|
const ytmusic = new YTMusic()
|
||||||
|
before(() => ytmusic.initialize())
|
||||||
beforeAll(() => ytmusic.initialize())
|
|
||||||
|
|
||||||
beforeEach(() => jest.setTimeout(10_000))
|
|
||||||
|
|
||||||
queries.forEach(query => {
|
queries.forEach(query => {
|
||||||
describe("Query: " + query, () => {
|
describeParallel("Query: " + query, () => {
|
||||||
test("Search suggestions", done => {
|
it("Search suggestions", async () => {
|
||||||
ytmusic
|
const suggestions = await ytmusic.getSearchSuggestions(query)
|
||||||
.getSearchSuggestions(query)
|
expect(suggestions, LIST(STRING()))
|
||||||
.then(suggestions => {
|
|
||||||
_expect(suggestions, LIST(STRING()))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Search Songs", done => {
|
it("Search Songs", async () => {
|
||||||
ytmusic
|
const songs = await ytmusic.search(query, "SONG")
|
||||||
.search(query, "SONG")
|
expect(songs, LIST(SONG_DETAILED))
|
||||||
.then(songs => {
|
|
||||||
_expect(songs, LIST(SONG_DETAILED))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Search Videos", done => {
|
it("Search Videos", async () => {
|
||||||
ytmusic
|
const videos = await ytmusic.search(query, "VIDEO")
|
||||||
.search(query, "VIDEO")
|
expect(videos, LIST(VIDEO_DETAILED))
|
||||||
.then(videos => {
|
|
||||||
_expect(videos, LIST(VIDEO_DETAILED))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Search Artists", done => {
|
it("Search Artists", async () => {
|
||||||
ytmusic
|
const artists = await ytmusic.search(query, "ARTIST")
|
||||||
.search(query, "ARTIST")
|
expect(artists, LIST(ARTIST_DETAILED))
|
||||||
.then(artists => {
|
|
||||||
_expect(artists, LIST(ARTIST_DETAILED))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Search Albums", done => {
|
it("Search Albums", async () => {
|
||||||
ytmusic
|
const albums = await ytmusic.search(query, "ALBUM")
|
||||||
.search(query, "ALBUM")
|
expect(albums, LIST(ALBUM_DETAILED))
|
||||||
.then(albums => {
|
|
||||||
_expect(albums, LIST(ALBUM_DETAILED))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Search Playlists", done => {
|
it("Search Playlists", async () => {
|
||||||
ytmusic
|
const playlists = await ytmusic.search(query, "PLAYLIST")
|
||||||
.search(query, "PLAYLIST")
|
expect(playlists, LIST(PLAYLIST_FULL))
|
||||||
.then(playlists => {
|
|
||||||
_expect(playlists, LIST(PLAYLIST_DETAILED))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Search All", done => {
|
it("Search All", async () => {
|
||||||
ytmusic
|
const results = await ytmusic.search(query)
|
||||||
.search(query)
|
expect(
|
||||||
.then(results => {
|
results,
|
||||||
_expect(
|
LIST(ALBUM_DETAILED, ARTIST_DETAILED, PLAYLIST_FULL, SONG_DETAILED, VIDEO_DETAILED)
|
||||||
results,
|
)
|
||||||
LIST(
|
|
||||||
ALBUM_DETAILED,
|
|
||||||
ARTIST_DETAILED,
|
|
||||||
PLAYLIST_DETAILED,
|
|
||||||
SONG_DETAILED,
|
|
||||||
VIDEO_DETAILED
|
|
||||||
)
|
|
||||||
)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Get details of the first song result", done => {
|
it("Get details of the first song result", async () => {
|
||||||
ytmusic
|
const songs = await ytmusic.search(query, "SONG")
|
||||||
.search(query, "SONG")
|
const song = await ytmusic.getSong(songs[0]!.videoId)
|
||||||
.then(songs => ytmusic.getSong(songs[0]!.videoId!))
|
expect(song, SONG_FULL)
|
||||||
.then(song => {
|
|
||||||
_expect(song, SONG_FULL)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Get details of the first video result", done => {
|
it("Get details of the first video result", async () => {
|
||||||
ytmusic
|
const videos = await ytmusic.search(query, "VIDEO")
|
||||||
.search(query, "VIDEO")
|
const video = await ytmusic.getVideo(videos[0]!.videoId)
|
||||||
.then(videos => ytmusic.getVideo(videos[0]!.videoId!))
|
expect(video, VIDEO_FULL)
|
||||||
.then(video => {
|
|
||||||
_expect(video, VIDEO_FULL)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Get details of the first artist result", done => {
|
it("Get details of the first artist result", async () => {
|
||||||
ytmusic
|
const artists = await ytmusic.search(query, "ARTIST")
|
||||||
.search(query, "ARTIST")
|
const artist = await ytmusic.getArtist(artists[0]!.artistId)
|
||||||
.then(artists => ytmusic.getArtist(artists[0]!.artistId!))
|
expect(artist, ARTIST_FULL)
|
||||||
.then(artist => {
|
|
||||||
_expect(artist, ARTIST_FULL)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Get the songs of the first artist result", done => {
|
it("Get the songs of the first artist result", async () => {
|
||||||
ytmusic
|
const artists = await ytmusic.search(query, "ARTIST")
|
||||||
.search(query, "ARTIST")
|
const songs = await ytmusic.getArtistSongs(artists[0]!.artistId)
|
||||||
.then(artists => ytmusic.getArtistSongs(artists[0]!.artistId!))
|
expect(songs, LIST(SONG_DETAILED))
|
||||||
.then(songs => {
|
|
||||||
_expect(songs, LIST(SONG_DETAILED))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Get the albums of the first artist result", done => {
|
it("Get the albums of the first artist result", async () => {
|
||||||
ytmusic
|
const artists = await ytmusic.search(query, "ARTIST")
|
||||||
.search(query, "ARTIST")
|
const albums = await ytmusic.getArtistAlbums(artists[0]!.artistId)
|
||||||
.then(artists => ytmusic.getArtistAlbums(artists[0]!.artistId!))
|
expect(albums, LIST(ALBUM_DETAILED))
|
||||||
.then(albums => {
|
|
||||||
_expect(albums, LIST(ALBUM_DETAILED))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Get details of the first album result", done => {
|
it("Get details of the first album result", async () => {
|
||||||
ytmusic
|
const albums = await ytmusic.search(query, "ALBUM")
|
||||||
.search(query, "ALBUM")
|
const album = await ytmusic.getAlbum(albums[0]!.albumId)
|
||||||
.then(albums => ytmusic.getAlbum(albums[0]!.albumId!))
|
expect(album, ALBUM_FULL)
|
||||||
.then(album => {
|
|
||||||
_expect(album, ALBUM_FULL)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Get details of the first playlist result", done => {
|
it("Get details of the first playlist result", async () => {
|
||||||
ytmusic
|
const playlists = await ytmusic.search(query, "PLAYLIST")
|
||||||
.search(query, "PLAYLIST")
|
const playlist = await ytmusic.getPlaylist(playlists[0]!.playlistId)
|
||||||
.then(playlists => ytmusic.getPlaylist(playlists[0]!.playlistId!))
|
expect(playlist, PLAYLIST_FULL)
|
||||||
.then(playlist => {
|
|
||||||
_expect(playlist, PLAYLIST_DETAILED)
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("Get the videos of the first playlist result", done => {
|
it("Get the videos of the first playlist result", async () => {
|
||||||
ytmusic
|
const playlists = await ytmusic.search(query, "PLAYLIST")
|
||||||
.search(query, "PLAYLIST")
|
const videos = await ytmusic.getPlaylistVideos(playlists[0]!.playlistId)
|
||||||
.then(playlists => ytmusic.getPlaylistVideos(playlists[0]!.playlistId!))
|
expect(videos, LIST(PLAYLIST_VIDEO))
|
||||||
.then(videos => {
|
|
||||||
_expect(videos, LIST(PLAYLIST_VIDEO))
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(done)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => console.log("Issues:", issues))
|
after(() => console.log("Issues:", issues))
|
||||||
|
|
|
||||||
10
src/index.ts
10
src/index.ts
|
|
@ -8,7 +8,7 @@ export interface ThumbnailFull {
|
||||||
|
|
||||||
export interface SongDetailed {
|
export interface SongDetailed {
|
||||||
type: "SONG"
|
type: "SONG"
|
||||||
videoId: string | null
|
videoId: string
|
||||||
name: string
|
name: string
|
||||||
artists: ArtistBasic[]
|
artists: ArtistBasic[]
|
||||||
album: AlbumBasic
|
album: AlbumBasic
|
||||||
|
|
@ -24,7 +24,7 @@ export interface SongFull extends Omit<SongDetailed, "album"> {
|
||||||
|
|
||||||
export interface VideoDetailed {
|
export interface VideoDetailed {
|
||||||
type: "VIDEO"
|
type: "VIDEO"
|
||||||
videoId: string | null
|
videoId: string
|
||||||
name: string
|
name: string
|
||||||
artists: ArtistBasic[]
|
artists: ArtistBasic[]
|
||||||
views: number
|
views: number
|
||||||
|
|
@ -41,7 +41,7 @@ export interface VideoFull extends VideoDetailed {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtistBasic {
|
export interface ArtistBasic {
|
||||||
artistId: string | null
|
artistId: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ export interface ArtistDetailed extends ArtistBasic {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtistFull extends ArtistDetailed {
|
export interface ArtistFull extends ArtistDetailed {
|
||||||
description: string | null
|
description: string
|
||||||
subscribers: number
|
subscribers: number
|
||||||
topSongs: Omit<SongDetailed, "duration">[]
|
topSongs: Omit<SongDetailed, "duration">[]
|
||||||
topAlbums: AlbumDetailed[]
|
topAlbums: AlbumDetailed[]
|
||||||
|
|
@ -72,7 +72,7 @@ export interface AlbumDetailed extends AlbumBasic {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlbumFull extends AlbumDetailed {
|
export interface AlbumFull extends AlbumDetailed {
|
||||||
description: string | null
|
description: string
|
||||||
songs: SongDetailed[]
|
songs: SongDetailed[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import ObjectValidator from "validate-any/dist/validators/ObjectValidator"
|
||||||
|
import {
|
||||||
|
AlbumBasic,
|
||||||
|
AlbumDetailed,
|
||||||
|
AlbumFull,
|
||||||
|
ArtistBasic,
|
||||||
|
ArtistDetailed,
|
||||||
|
ArtistFull,
|
||||||
|
PlaylistFull,
|
||||||
|
SongDetailed,
|
||||||
|
SongFull,
|
||||||
|
ThumbnailFull,
|
||||||
|
VideoDetailed,
|
||||||
|
VideoFull
|
||||||
|
} from "."
|
||||||
|
import { BOOLEAN, LIST, NUMBER, OBJECT, STRING } from "validate-any"
|
||||||
|
|
||||||
|
export const THUMBNAIL_FULL: ObjectValidator<ThumbnailFull> = OBJECT({
|
||||||
|
url: STRING(),
|
||||||
|
width: NUMBER(),
|
||||||
|
height: NUMBER()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ARTIST_BASIC: ObjectValidator<ArtistBasic> = OBJECT({
|
||||||
|
artistId: STRING(),
|
||||||
|
name: STRING()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ALBUM_BASIC: ObjectValidator<AlbumBasic> = OBJECT({
|
||||||
|
albumId: STRING(),
|
||||||
|
name: STRING()
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SONG_DETAILED: ObjectValidator<SongDetailed> = OBJECT({
|
||||||
|
type: STRING("SONG"),
|
||||||
|
videoId: STRING(),
|
||||||
|
name: STRING(),
|
||||||
|
artists: LIST(ARTIST_BASIC),
|
||||||
|
album: ALBUM_BASIC,
|
||||||
|
duration: NUMBER(),
|
||||||
|
thumbnails: LIST(THUMBNAIL_FULL)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const VIDEO_DETAILED: ObjectValidator<VideoDetailed> = OBJECT({
|
||||||
|
type: STRING("VIDEO"),
|
||||||
|
videoId: STRING(),
|
||||||
|
name: STRING(),
|
||||||
|
artists: LIST(ARTIST_BASIC),
|
||||||
|
views: NUMBER(),
|
||||||
|
duration: NUMBER(),
|
||||||
|
thumbnails: LIST(THUMBNAIL_FULL)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ARTIST_DETAILED: ObjectValidator<ArtistDetailed> = OBJECT({
|
||||||
|
artistId: STRING(),
|
||||||
|
name: STRING(),
|
||||||
|
type: STRING("ARTIST"),
|
||||||
|
thumbnails: LIST(THUMBNAIL_FULL)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ALBUM_DETAILED: ObjectValidator<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<SongFull> = OBJECT({
|
||||||
|
type: STRING("SONG"),
|
||||||
|
videoId: STRING(),
|
||||||
|
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<VideoFull> = OBJECT({
|
||||||
|
type: STRING("VIDEO"),
|
||||||
|
videoId: STRING(),
|
||||||
|
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<ArtistFull> = OBJECT({
|
||||||
|
artistId: STRING(),
|
||||||
|
name: STRING(),
|
||||||
|
type: STRING("ARTIST"),
|
||||||
|
thumbnails: LIST(THUMBNAIL_FULL),
|
||||||
|
description: STRING(),
|
||||||
|
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<AlbumFull> = OBJECT({
|
||||||
|
type: STRING("ALBUM"),
|
||||||
|
albumId: STRING(),
|
||||||
|
playlistId: STRING(),
|
||||||
|
name: STRING(),
|
||||||
|
artists: LIST(ARTIST_BASIC),
|
||||||
|
year: NUMBER(),
|
||||||
|
thumbnails: LIST(THUMBNAIL_FULL),
|
||||||
|
description: STRING(),
|
||||||
|
songs: LIST(SONG_DETAILED)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PLAYLIST_FULL: ObjectValidator<PlaylistFull> = OBJECT({
|
||||||
|
type: STRING("PLAYLIST"),
|
||||||
|
playlistId: STRING(),
|
||||||
|
name: STRING(),
|
||||||
|
artist: ARTIST_BASIC,
|
||||||
|
videoCount: NUMBER(),
|
||||||
|
thumbnails: LIST(THUMBNAIL_FULL)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const PLAYLIST_VIDEO: ObjectValidator<Omit<VideoDetailed, "views">> = OBJECT({
|
||||||
|
type: STRING("VIDEO"),
|
||||||
|
videoId: STRING(),
|
||||||
|
name: STRING(),
|
||||||
|
artists: LIST(ARTIST_BASIC),
|
||||||
|
duration: NUMBER(),
|
||||||
|
thumbnails: LIST(THUMBNAIL_FULL)
|
||||||
|
})
|
||||||
|
|
@ -1,72 +1,90 @@
|
||||||
|
import checkType from "../utils/checkType"
|
||||||
import SongParser from "./SongParser"
|
import SongParser from "./SongParser"
|
||||||
import traverse from "../traverse"
|
import traverseList from "../utils/traverseList"
|
||||||
import { AlbumDetailed, AlbumFull, ArtistBasic } from ".."
|
import traverseString from "../utils/traverseString"
|
||||||
|
import { ALBUM_DETAILED, ALBUM_FULL } from "../interfaces"
|
||||||
|
import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from ".."
|
||||||
|
|
||||||
export default class AlbumParser {
|
export default class AlbumParser {
|
||||||
public static parse(data: any, albumId: string): AlbumFull {
|
public static parse(data: any, albumId: string): AlbumFull {
|
||||||
const albumBasic = {
|
const albumBasic: AlbumBasic = {
|
||||||
albumId,
|
albumId,
|
||||||
name: traverse(data, "header", "title", "text").at(0)
|
name: traverseString(data, "header", "title", "text")()
|
||||||
}
|
}
|
||||||
const artists = traverse(data, "header", "subtitle", "runs")
|
const artists: ArtistBasic[] = traverseList(data, "header", "subtitle", "runs")
|
||||||
.filter((run: any) => "navigationEndpoint" in run)
|
.filter(run => "navigationEndpoint" in run)
|
||||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text }))
|
.map(run => ({
|
||||||
const thumbnails = [traverse(data, "header", "thumbnails")].flat()
|
artistId: traverseString(run, "browseId")(),
|
||||||
const description = traverse(data, "description", "text")
|
name: traverseString(run, "text")()
|
||||||
|
}))
|
||||||
|
const thumbnails = traverseList(data, "header", "thumbnails")
|
||||||
|
|
||||||
return {
|
return checkType<AlbumFull>(
|
||||||
type: "ALBUM",
|
{
|
||||||
...albumBasic,
|
type: "ALBUM",
|
||||||
playlistId: traverse(data, "buttonRenderer", "playlistId"),
|
...albumBasic,
|
||||||
artists,
|
playlistId: traverseString(data, "buttonRenderer", "playlistId")(),
|
||||||
year: +traverse(data, "header", "subtitle", "text").at(-1),
|
artists,
|
||||||
thumbnails,
|
year: +traverseString(data, "header", "subtitle", "text")(-1),
|
||||||
description: description instanceof Array ? null : description,
|
thumbnails,
|
||||||
songs: [traverse(data, "musicResponsiveListItemRenderer")]
|
description: traverseString(data, "description", "text")(),
|
||||||
.flat()
|
songs: traverseList(data, "musicResponsiveListItemRenderer").map(item =>
|
||||||
.map((item: any) =>
|
|
||||||
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
|
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
ALBUM_FULL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): AlbumDetailed {
|
public static parseSearchResult(item: any): AlbumDetailed {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
|
|
||||||
return {
|
return checkType<AlbumDetailed>(
|
||||||
type: "ALBUM",
|
{
|
||||||
albumId: [traverse(item, "browseId")].flat().at(-1),
|
type: "ALBUM",
|
||||||
playlistId: traverse(item, "overlay", "playlistId"),
|
albumId: traverseString(item, "browseId")(-1),
|
||||||
artists: traverse(flexColumns[1], "runs")
|
playlistId: traverseString(item, "overlay", "playlistId")(),
|
||||||
.filter((run: any) => "navigationEndpoint" in run)
|
artists: traverseList(flexColumns[1], "runs")
|
||||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
|
.filter(run => "navigationEndpoint" in run)
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
.map(run => ({
|
||||||
year: +traverse(flexColumns[1], "runs", "text").at(-1),
|
artistId: traverseString(run, "browseId")(),
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
name: traverseString(run, "text")()
|
||||||
}
|
})),
|
||||||
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
|
year: +traverseString(flexColumns[1], "runs", "text")(-1),
|
||||||
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
|
},
|
||||||
|
ALBUM_DETAILED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistAlbum(item: any, artistBasic: ArtistBasic): AlbumDetailed {
|
public static parseArtistAlbum(item: any, artistBasic: ArtistBasic): AlbumDetailed {
|
||||||
return {
|
return checkType<AlbumDetailed>(
|
||||||
type: "ALBUM",
|
{
|
||||||
albumId: [traverse(item, "browseId")].flat().at(-1),
|
type: "ALBUM",
|
||||||
playlistId: traverse(item, "thumbnailOverlay", "playlistId"),
|
albumId: traverseString(item, "browseId")(-1),
|
||||||
name: traverse(item, "title", "text").at(0),
|
playlistId: traverseString(item, "thumbnailOverlay", "playlistId")(),
|
||||||
artists: [artistBasic],
|
name: traverseString(item, "title", "text")(),
|
||||||
year: +traverse(item, "subtitle", "text").at(-1),
|
artists: [artistBasic],
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
year: +traverseString(item, "subtitle", "text")(-1),
|
||||||
}
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
|
},
|
||||||
|
ALBUM_DETAILED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): AlbumDetailed {
|
public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): AlbumDetailed {
|
||||||
return {
|
return checkType<AlbumDetailed>(
|
||||||
type: "ALBUM",
|
{
|
||||||
albumId: traverse(item, "browseId").at(-1),
|
type: "ALBUM",
|
||||||
playlistId: traverse(item, "musicPlayButtonRenderer", "playlistId"),
|
albumId: traverseString(item, "browseId")(-1),
|
||||||
name: traverse(item, "title", "text").at(0),
|
playlistId: traverseString(item, "musicPlayButtonRenderer", "playlistId")(),
|
||||||
artists: [artistBasic],
|
name: traverseString(item, "title", "text")(),
|
||||||
year: +traverse(item, "subtitle", "text").at(-1),
|
artists: [artistBasic],
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
year: +traverseString(item, "subtitle", "text")(-1),
|
||||||
}
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
|
},
|
||||||
|
ALBUM_DETAILED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,54 @@
|
||||||
import AlbumParser from "./AlbumParser"
|
import AlbumParser from "./AlbumParser"
|
||||||
|
import checkType from "../utils/checkType"
|
||||||
import Parser from "./Parser"
|
import Parser from "./Parser"
|
||||||
import SongParser from "./SongParser"
|
import SongParser from "./SongParser"
|
||||||
import traverse from "../traverse"
|
import traverseList from "../utils/traverseList"
|
||||||
import { ArtistDetailed, ArtistFull } from ".."
|
import traverseString from "../utils/traverseString"
|
||||||
|
import { ARTIST_DETAILED, ARTIST_FULL } from "../interfaces"
|
||||||
|
import { ArtistBasic, ArtistDetailed, ArtistFull } from ".."
|
||||||
|
|
||||||
export default class ArtistParser {
|
export default class ArtistParser {
|
||||||
public static parse(data: any, artistId: string): ArtistFull {
|
public static parse(data: any, artistId: string): ArtistFull {
|
||||||
const artistBasic = {
|
const artistBasic: ArtistBasic = {
|
||||||
artistId,
|
artistId,
|
||||||
name: traverse(data, "header", "title", "text").at(0)
|
name: traverseString(data, "header", "title", "text")()
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = traverse(data, "header", "description", "text")
|
const description = traverseString(data, "header", "description", "text")()
|
||||||
|
|
||||||
return {
|
return checkType<ArtistFull>(
|
||||||
type: "ARTIST",
|
{
|
||||||
...artistBasic,
|
type: "ARTIST",
|
||||||
thumbnails: [traverse(data, "header", "thumbnails")].flat(),
|
...artistBasic,
|
||||||
description: description instanceof Array ? null : description,
|
thumbnails: traverseList(data, "header", "thumbnails"),
|
||||||
subscribers: Parser.parseNumber(traverse(data, "subscriberCountText", "text")),
|
description,
|
||||||
topSongs: traverse(data, "musicShelfRenderer", "contents").map((item: any) =>
|
subscribers: Parser.parseNumber(
|
||||||
SongParser.parseArtistTopSong(item, artistBasic)
|
traverseString(data, "subscriberCountText", "text")()
|
||||||
),
|
),
|
||||||
topAlbums: [traverse(data, "musicCarouselShelfRenderer")]
|
topSongs: traverseList(data, "musicShelfRenderer", "contents").map(item =>
|
||||||
.flat()
|
SongParser.parseArtistTopSong(item, artistBasic)
|
||||||
.at(0)
|
),
|
||||||
.contents.map((item: any) => AlbumParser.parseArtistTopAlbums(item, artistBasic))
|
topAlbums: traverseList(data, "musicCarouselShelfRenderer")
|
||||||
}
|
.at(0)
|
||||||
|
.contents.map((item: any) =>
|
||||||
|
AlbumParser.parseArtistTopAlbums(item, artistBasic)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
ARTIST_FULL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): ArtistDetailed {
|
public static parseSearchResult(item: any): ArtistDetailed {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
|
|
||||||
return {
|
return checkType<ArtistDetailed>(
|
||||||
type: "ARTIST",
|
{
|
||||||
artistId: traverse(item, "browseId"),
|
type: "ARTIST",
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
artistId: traverseString(item, "browseId")(),
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
}
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
|
},
|
||||||
|
ARTIST_DETAILED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,52 @@
|
||||||
import traverse from "../traverse"
|
import checkType from "../utils/checkType"
|
||||||
|
import traverseList from "../utils/traverseList"
|
||||||
|
import traverseString from "../utils/traverseString"
|
||||||
|
import { PLAYLIST_FULL } from "../interfaces"
|
||||||
import { PlaylistFull } from ".."
|
import { PlaylistFull } from ".."
|
||||||
|
|
||||||
export default class PlaylistParser {
|
export default class PlaylistParser {
|
||||||
public static parse(data: any, playlistId: string): PlaylistFull {
|
public static parse(data: any, playlistId: string): PlaylistFull {
|
||||||
return {
|
return checkType<PlaylistFull>(
|
||||||
type: "PLAYLIST",
|
{
|
||||||
playlistId,
|
type: "PLAYLIST",
|
||||||
name: traverse(data, "header", "title", "text").at(0),
|
playlistId,
|
||||||
artist: {
|
name: traverseString(data, "header", "title", "text")(),
|
||||||
artistId: traverse(data, "header", "subtitle", "browseId"),
|
artist: {
|
||||||
name: traverse(data, "header", "subtitle", "text").at(2)
|
artistId: traverseString(data, "header", "subtitle", "browseId")(),
|
||||||
|
name: traverseString(data, "header", "subtitle", "text")(2)
|
||||||
|
},
|
||||||
|
videoCount: +traverseList(data, "header", "secondSubtitle", "text")
|
||||||
|
.at(0)
|
||||||
|
.split(" ")
|
||||||
|
.at(0)
|
||||||
|
.replaceAll(",", ""),
|
||||||
|
thumbnails: traverseList(data, "header", "thumbnails")
|
||||||
},
|
},
|
||||||
videoCount: +traverse(data, "header", "secondSubtitle", "text")
|
PLAYLIST_FULL
|
||||||
.at(0)
|
)
|
||||||
.split(" ")
|
|
||||||
.at(0)
|
|
||||||
.replaceAll(",", ""),
|
|
||||||
thumbnails: traverse(data, "header", "thumbnails")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): PlaylistFull {
|
public static parseSearchResult(item: any): PlaylistFull {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const artistId = traverse(flexColumns[1], "browseId")
|
const artistId = traverseString(flexColumns[1], "browseId")()
|
||||||
|
|
||||||
return {
|
return checkType<PlaylistFull>(
|
||||||
type: "PLAYLIST",
|
{
|
||||||
playlistId: traverse(item, "overlay", "playlistId"),
|
type: "PLAYLIST",
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
playlistId: traverseString(item, "overlay", "playlistId")(),
|
||||||
artist: {
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
artistId: artistId instanceof Array ? null : artistId,
|
artist: {
|
||||||
name: traverse(flexColumns[1], "runs", "text").at(-2)
|
artistId,
|
||||||
|
name: traverseString(flexColumns[1], "runs", "text")(-2)
|
||||||
|
},
|
||||||
|
videoCount: +traverseList(flexColumns[1], "runs", "text")
|
||||||
|
.at(-1)
|
||||||
|
.split(" ")
|
||||||
|
.at(0)
|
||||||
|
.replaceAll(",", ""),
|
||||||
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
},
|
},
|
||||||
videoCount: +traverse(flexColumns[1], "runs", "text")
|
PLAYLIST_FULL
|
||||||
.at(-1)
|
)
|
||||||
.split(" ")
|
|
||||||
.at(0)
|
|
||||||
.replaceAll(",", ""),
|
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ import AlbumParser from "./AlbumParser"
|
||||||
import ArtistParser from "./ArtistParser"
|
import ArtistParser from "./ArtistParser"
|
||||||
import PlaylistParser from "./PlaylistParser"
|
import PlaylistParser from "./PlaylistParser"
|
||||||
import SongParser from "./SongParser"
|
import SongParser from "./SongParser"
|
||||||
import traverse from "../traverse"
|
import traverseList from "../utils/traverseList"
|
||||||
import VideoParser from "./VideoParser"
|
import VideoParser from "./VideoParser"
|
||||||
import { SearchResult } from ".."
|
import { SearchResult } from ".."
|
||||||
|
|
||||||
export default class SearchParser {
|
export default class SearchParser {
|
||||||
public static parse(item: any): SearchResult {
|
public static parse(item: any): SearchResult {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const type = [traverse(flexColumns[1], "runs", "text")].flat().at(0) as
|
const type = traverseList(flexColumns[1], "runs", "text").at(0) as
|
||||||
| "Song"
|
| "Song"
|
||||||
| "Video"
|
| "Video"
|
||||||
| "Artist"
|
| "Artist"
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,116 @@
|
||||||
|
import checkType from "../utils/checkType"
|
||||||
import Parser from "./Parser"
|
import Parser from "./Parser"
|
||||||
import traverse from "../traverse"
|
import traverseList from "../utils/traverseList"
|
||||||
|
import traverseString from "../utils/traverseString"
|
||||||
|
import { ALBUM_BASIC, ARTIST_BASIC, SONG_DETAILED, SONG_FULL, THUMBNAIL_FULL } from "../interfaces"
|
||||||
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from ".."
|
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from ".."
|
||||||
|
import { LIST, OBJECT, STRING } from "validate-any"
|
||||||
|
|
||||||
export default class SongParser {
|
export default class SongParser {
|
||||||
public static parse(data: any): SongFull {
|
public static parse(data: any): SongFull {
|
||||||
return {
|
return checkType<SongFull>(
|
||||||
type: "SONG",
|
{
|
||||||
videoId: traverse(data, "videoDetails", "videoId"),
|
type: "SONG",
|
||||||
name: traverse(data, "videoDetails", "title"),
|
videoId: traverseString(data, "videoDetails", "videoId")(),
|
||||||
artists: [
|
name: traverseString(data, "videoDetails", "title")(),
|
||||||
{
|
artists: [
|
||||||
artistId: traverse(data, "videoDetails", "channelId"),
|
{
|
||||||
name: traverse(data, "author")
|
artistId: traverseString(data, "videoDetails", "channelId")(),
|
||||||
}
|
name: traverseString(data, "author")()
|
||||||
],
|
}
|
||||||
duration: +traverse(data, "videoDetails", "lengthSeconds"),
|
],
|
||||||
thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(),
|
duration: +traverseString(data, "videoDetails", "lengthSeconds"),
|
||||||
description: traverse(data, "description"),
|
thumbnails: traverseList(data, "videoDetails", "thumbnails"),
|
||||||
formats: traverse(data, "streamingData", "formats"),
|
description: traverseString(data, "description")(),
|
||||||
adaptiveFormats: traverse(data, "streamingData", "adaptiveFormats")
|
formats: traverseList(data, "streamingData", "formats"),
|
||||||
}
|
adaptiveFormats: traverseList(data, "streamingData", "adaptiveFormats")
|
||||||
|
},
|
||||||
|
SONG_FULL
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): SongDetailed {
|
public static parseSearchResult(item: any): SongDetailed {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
|
||||||
|
|
||||||
return {
|
return checkType<SongDetailed>(
|
||||||
type: "SONG",
|
{
|
||||||
videoId: videoId instanceof Array ? null : videoId,
|
type: "SONG",
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
videoId: traverseString(item, "playlistItemData", "videoId")(),
|
||||||
artists: traverse(flexColumns[1], "runs")
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
.filter((run: any) => "navigationEndpoint" in run)
|
artists: traverseList(flexColumns[1], "runs")
|
||||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text }))
|
.filter(run => "navigationEndpoint" in run)
|
||||||
.slice(0, -1),
|
.map(run => ({
|
||||||
album: {
|
artistId: traverseString(run, "browseId")(),
|
||||||
albumId: traverse(item, "browseId").at(-1),
|
name: traverseString(run, "text")()
|
||||||
name: traverse(flexColumns[1], "runs", "text").at(-3)
|
}))
|
||||||
|
.slice(0, -1),
|
||||||
|
album: {
|
||||||
|
albumId: traverseString(item, "browseId")(-1),
|
||||||
|
name: traverseString(flexColumns[1], "runs", "text")(-3)
|
||||||
|
},
|
||||||
|
duration: Parser.parseDuration(traverseString(flexColumns[1], "runs", "text")(-1)),
|
||||||
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
},
|
},
|
||||||
duration: Parser.parseDuration(traverse(flexColumns[1], "runs", "text").at(-1)),
|
SONG_DETAILED
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistSong(item: any): SongDetailed {
|
public static parseArtistSong(item: any): SongDetailed {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
const videoId = traverseString(item, "playlistItemData", "videoId")()
|
||||||
|
|
||||||
return {
|
return checkType<SongDetailed>(
|
||||||
type: "SONG",
|
{
|
||||||
videoId: videoId instanceof Array ? null : videoId,
|
type: "SONG",
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
videoId,
|
||||||
artists: [traverse(flexColumns[1], "runs")]
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
.flat()
|
artists: traverseList(flexColumns[1], "runs")
|
||||||
.filter((item: any) => "navigationEndpoint" in item)
|
.filter(run => "navigationEndpoint" in run)
|
||||||
.map((run: any) => ({
|
.map(run => ({
|
||||||
artistId: traverse(run, "browseId"),
|
artistId: traverseString(run, "browseId")(),
|
||||||
name: run.text
|
name: traverseString(run, "text")()
|
||||||
})),
|
})),
|
||||||
album: {
|
album: {
|
||||||
albumId: traverse(flexColumns[2], "browseId"),
|
albumId: traverseString(flexColumns[2], "browseId")(),
|
||||||
name: traverse(flexColumns[2], "runs", "text")
|
name: traverseString(flexColumns[2], "runs", "text")()
|
||||||
|
},
|
||||||
|
duration: Parser.parseDuration(
|
||||||
|
traverseString(item, "fixedColumns", "runs", "text")()
|
||||||
|
),
|
||||||
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
},
|
},
|
||||||
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
|
SONG_DETAILED
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistTopSong(
|
public static parseArtistTopSong(
|
||||||
item: any,
|
item: any,
|
||||||
artistBasic: ArtistBasic
|
artistBasic: ArtistBasic
|
||||||
): Omit<SongDetailed, "duration"> {
|
): Omit<SongDetailed, "duration"> {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
const videoId = traverseString(item, "playlistItemData", "videoId")()
|
||||||
|
|
||||||
return {
|
return checkType<Omit<SongDetailed, "duration">>(
|
||||||
type: "SONG",
|
{
|
||||||
videoId: videoId instanceof Array ? null : videoId,
|
type: "SONG",
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
videoId,
|
||||||
artists: [artistBasic],
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
album: {
|
artists: [artistBasic],
|
||||||
albumId: traverse(flexColumns[2], "runs", "text"),
|
album: {
|
||||||
name: traverse(flexColumns[2], "browseId")
|
albumId: traverseString(flexColumns[2], "runs", "text")(),
|
||||||
|
name: traverseString(flexColumns[2], "browseId")()
|
||||||
|
},
|
||||||
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
},
|
},
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
OBJECT({
|
||||||
}
|
type: STRING("SONG"),
|
||||||
|
videoId: STRING(),
|
||||||
|
name: STRING(),
|
||||||
|
artists: LIST(ARTIST_BASIC),
|
||||||
|
album: ALBUM_BASIC,
|
||||||
|
thumbnails: LIST(THUMBNAIL_FULL)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseAlbumSong(
|
public static parseAlbumSong(
|
||||||
|
|
@ -93,17 +119,22 @@ export default class SongParser {
|
||||||
albumBasic: AlbumBasic,
|
albumBasic: AlbumBasic,
|
||||||
thumbnails: ThumbnailFull[]
|
thumbnails: ThumbnailFull[]
|
||||||
): SongDetailed {
|
): SongDetailed {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
const videoId = traverseString(item, "playlistItemData", "videoId")()
|
||||||
|
|
||||||
return {
|
return checkType<SongDetailed>(
|
||||||
type: "SONG",
|
{
|
||||||
videoId: videoId instanceof Array ? null : videoId,
|
type: "SONG",
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
videoId,
|
||||||
artists,
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
album: albumBasic,
|
artists,
|
||||||
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
|
album: albumBasic,
|
||||||
thumbnails
|
duration: Parser.parseDuration(
|
||||||
}
|
traverseString(item, "fixedColumns", "runs", "text")()
|
||||||
|
),
|
||||||
|
thumbnails
|
||||||
|
},
|
||||||
|
SONG_DETAILED
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,77 @@
|
||||||
|
import checkType from "../utils/checkType"
|
||||||
import Parser from "./Parser"
|
import Parser from "./Parser"
|
||||||
import traverse from "../traverse"
|
import traverse from "../utils/traverse"
|
||||||
|
import traverseList from "../utils/traverseList"
|
||||||
|
import traverseString from "../utils/traverseString"
|
||||||
|
import { PLAYLIST_VIDEO } from "../interfaces"
|
||||||
import { VideoDetailed, VideoFull } from ".."
|
import { VideoDetailed, VideoFull } from ".."
|
||||||
|
|
||||||
export default class VideoParser {
|
export default class VideoParser {
|
||||||
public static parse(data: any): VideoFull {
|
public static parse(data: any): VideoFull {
|
||||||
return {
|
return {
|
||||||
type: "VIDEO",
|
type: "VIDEO",
|
||||||
videoId: traverse(data, "videoDetails", "videoId"),
|
videoId: traverseString(data, "videoDetails", "videoId")(),
|
||||||
name: traverse(data, "videoDetails", "title"),
|
name: traverseString(data, "videoDetails", "title")(),
|
||||||
artists: [
|
artists: [
|
||||||
{
|
{
|
||||||
artistId: traverse(data, "videoDetails", "channelId"),
|
artistId: traverseString(data, "videoDetails", "channelId")(),
|
||||||
name: traverse(data, "author")
|
name: traverseString(data, "author")()
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
views: +traverse(data, "videoDetails", "viewCount"),
|
views: +traverseString(data, "videoDetails", "viewCount")(),
|
||||||
duration: +traverse(data, "videoDetails", "lengthSeconds"),
|
duration: +traverseString(data, "videoDetails", "lengthSeconds")(),
|
||||||
thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(),
|
thumbnails: traverseList(data, "videoDetails", "thumbnails"),
|
||||||
description: traverse(data, "description"),
|
description: traverseString(data, "description")(),
|
||||||
unlisted: traverse(data, "unlisted"),
|
unlisted: traverse(data, "unlisted"),
|
||||||
familySafe: traverse(data, "familySafe"),
|
familySafe: traverse(data, "familySafe"),
|
||||||
paid: traverse(data, "paid"),
|
paid: traverse(data, "paid"),
|
||||||
tags: traverse(data, "tags")
|
tags: traverseList(data, "tags")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseSearchResult(item: any): VideoDetailed {
|
public static parseSearchResult(item: any): VideoDetailed {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const videoId = traverse(item, "playNavigationEndpoint", "videoId")
|
const videoId = traverseString(item, "playNavigationEndpoint", "videoId")()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "VIDEO",
|
type: "VIDEO",
|
||||||
videoId: videoId instanceof Array ? null : videoId,
|
videoId,
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
artists: [traverse(flexColumns[1], "runs")]
|
artists: traverseList(flexColumns[1], "runs")
|
||||||
.flat()
|
.filter(run => "navigationEndpoint" in run)
|
||||||
.filter((run: any) => "navigationEndpoint" in run)
|
.map(run => ({
|
||||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
|
artistId: traverseString(run, "browseId")(),
|
||||||
views: Parser.parseNumber(traverse(flexColumns[1], "runs", "text").at(-3).slice(0, -6)),
|
name: traverseString(run, "text")()
|
||||||
duration: Parser.parseDuration(traverse(flexColumns[1], "text").at(-1)),
|
})),
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
views: Parser.parseNumber(
|
||||||
|
traverseString(flexColumns[1], "runs", "text")(-3).slice(0, -6)
|
||||||
|
),
|
||||||
|
duration: Parser.parseDuration(traverseString(flexColumns[1], "text")(-1)),
|
||||||
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parsePlaylistVideo(item: any): Omit<VideoDetailed, "views"> {
|
public static parsePlaylistVideo(item: any): Omit<VideoDetailed, "views"> {
|
||||||
const flexColumns = traverse(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const videoId = traverse(item, "playNavigationEndpoint", "videoId")
|
const videoId = traverseString(item, "playNavigationEndpoint", "videoId")()
|
||||||
|
|
||||||
return {
|
return checkType<Omit<VideoDetailed, "views">>(
|
||||||
type: "VIDEO",
|
{
|
||||||
videoId: videoId instanceof Array ? null : videoId,
|
type: "VIDEO",
|
||||||
name: traverse(flexColumns[0], "runs", "text"),
|
videoId,
|
||||||
artists: [traverse(flexColumns[1], "runs")]
|
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||||
.flat()
|
artists: traverseList(flexColumns[1], "runs")
|
||||||
.filter((run: any) => "navigationEndpoint" in run)
|
.filter(run => "navigationEndpoint" in run)
|
||||||
.map((run: any) => ({ artistId: traverse(run, "browseId"), name: run.text })),
|
.map(run => ({
|
||||||
duration: Parser.parseDuration(traverse(item, "fixedColumns", "runs", "text")),
|
artistId: traverseString(run, "browseId")(),
|
||||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
name: traverseString(run, "text")()
|
||||||
}
|
})),
|
||||||
|
duration: Parser.parseDuration(
|
||||||
|
traverseString(item, "fixedColumns", "runs", "text")()
|
||||||
|
),
|
||||||
|
thumbnails: traverseList(item, "thumbnails")
|
||||||
|
},
|
||||||
|
PLAYLIST_VIDEO
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import Validator from "validate-any/dist/classes/Validator"
|
||||||
|
import { validate } from "validate-any"
|
||||||
|
|
||||||
|
export default <T>(data: T, validator: Validator<T>): T => {
|
||||||
|
const result = validate(data, validator)
|
||||||
|
if (result.success) {
|
||||||
|
return result.data
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
"Invalid data schema, please report to https://github.com/zS1L3NT/ts-npm-ytmusic-api/issues/new/choose",
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
expected: validator.getSchema(),
|
||||||
|
actual: data,
|
||||||
|
errors: result.errors
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import traverse from "./traverse"
|
||||||
|
|
||||||
|
export default (data: any, ...keys: string[]): any[] => {
|
||||||
|
const value = traverse(data, ...keys)
|
||||||
|
const flatValue = [value].flat()
|
||||||
|
return flatValue
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import traverse from "./traverse"
|
||||||
|
|
||||||
|
export default (data: any, ...keys: string[]) => (index = 0): string => {
|
||||||
|
const value = traverse(data, ...keys)
|
||||||
|
const flatValue = [value].flat().at(index)
|
||||||
|
return flatValue || ""
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue