Merge branch 'type-safety' into main
This commit is contained in:
commit
ec98127f62
|
|
@ -3,5 +3,4 @@
|
|||
.gitignore
|
||||
.prettierrc
|
||||
.editorconfig
|
||||
tsconfig.json
|
||||
babel.config.js
|
||||
tsconfig.json
|
||||
|
|
@ -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
|
||||
|
||||
With `yarn`
|
||||
|
||||
```
|
||||
$ yarn add ytmusic-api
|
||||
```
|
||||
|
||||
With `npm`
|
||||
|
||||
```
|
||||
$ npm i ytmusic-api
|
||||
```
|
||||
|
|
@ -294,7 +286,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
|||
| Name | Data Type | Description |
|
||||
| :--------- | :---------------------------------- | :------------------ |
|
||||
| type | `"SONG"` | Type of data |
|
||||
| videoId | `string \| null` | YouTube Video ID |
|
||||
| videoId | `string` | YouTube Video ID |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Artists |
|
||||
| album | [AlbumBasic](#AlbumBasic) | Album |
|
||||
|
|
@ -306,7 +298,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
|||
| Name | Data Type | Description |
|
||||
| :-------------- | :---------------------------------- | :--------------------- |
|
||||
| type | `"SONG"` | Type of data |
|
||||
| videoId | `string \| null` | YouTube Video ID |
|
||||
| videoId | `string` | YouTube Video ID |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Artists |
|
||||
| duration | `number` | Duration in seconds |
|
||||
|
|
@ -320,7 +312,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
|||
| Name | Data Type | Description |
|
||||
| :--------- | :---------------------------------- | :------------------------------ |
|
||||
| type | `"VIDEO"` | Type of data |
|
||||
| videoId | `string \| null` | YouTube Video ID |
|
||||
| videoId | `string` | YouTube Video ID |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Channels that created the video |
|
||||
| views | `number` | View count |
|
||||
|
|
@ -332,7 +324,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
|||
| Name | Data Type | Description |
|
||||
| :---------- | :---------------------------------- | :------------------------------------- |
|
||||
| type | `"VIDEO"` | Type of data |
|
||||
| videoId | `string \| null` | YouTube Video ID |
|
||||
| videoId | `string` | YouTube Video ID |
|
||||
| name | `string` | Name |
|
||||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Channels that created the video |
|
||||
| views | `number` | View count |
|
||||
|
|
@ -348,7 +340,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
|||
|
||||
| Name | Data Type | Description |
|
||||
| :------- | :--------------- | :---------- |
|
||||
| artistId | `string \| null` | Artist ID |
|
||||
| artistId | `string` | Artist ID |
|
||||
| name | `string` | Name |
|
||||
|
||||
#### `ArtistDetailed`
|
||||
|
|
@ -368,7 +360,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
|||
| artistId | `string` | Artist ID |
|
||||
| name | `string` | Name |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
| description | `string \| null` | Description |
|
||||
| description | `string` | 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 |
|
||||
|
|
@ -403,7 +395,7 @@ ytmusic.getPlaylistVideos("OLAK5uy_nRb467jR73IXKybwzw22_rTYIJ808x4Yc").then(play
|
|||
| artists | [ArtistBasic](#ArtistBasic)`[]` | Creators of the Album |
|
||||
| year | `number` | Publication Year |
|
||||
| thumbnails | [ThumbnailFull](#ThumbnailFull)`[]` | Thumbnails |
|
||||
| description | `string \| null` | Description |
|
||||
| description | `string` | Description |
|
||||
| songs | [SongDetailed](#SongDetailed)`[]` | Songs in the Album |
|
||||
|
||||
#### `PlaylistFull`
|
||||
|
|
@ -426,7 +418,7 @@ A lot of the credit should go to [youtube-music-api](https://npmjs.com/package/y
|
|||
|
||||
## 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
|
||||
|
|
@ -435,18 +427,17 @@ $ npm run test
|
|||
## Built with
|
||||
|
||||
- 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/typescript)
|
||||
- Axios
|
||||
- [](https://npmjs.com/package/axios)
|
||||
- Tough Cookie
|
||||
- [](https://npmjs.com/package/tough-cookie)
|
||||
- Jest
|
||||
- [](https://npmjs.com/package/@babel/core)
|
||||
- [](https://npmjs.com/package/@babel/preset-env)
|
||||
- [](https://npmjs.com/package/@babel/preset-typescript)
|
||||
- [](https://npmjs.com/package/babel-jest)
|
||||
- [](https://npmjs.com/package/jest)
|
||||
- Mocha
|
||||
- [](https://npmjs.com/package/mocha)
|
||||
- [](https://npmjs.com/package/mocha.parallel)
|
||||
- [](https://npmjs.com/package/ts-mocha)
|
||||
- Miscellaneous
|
||||
- [](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",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.5",
|
||||
"description": "YouTube Music API",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
@ -11,22 +11,21 @@
|
|||
"url": "https://github.com/zS1L3NT/ts-npm-ytmusic-api"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
"test": "ts-mocha --timeout 10000 src/__tests__/**/*.spec.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.25.0",
|
||||
"tough-cookie": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@types/jest": "^27.4.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/tough-cookie": "^4.0.1",
|
||||
"babel-jest": "^27.4.6",
|
||||
"jest": "^27.4.7",
|
||||
"mocha": "^9.2.2",
|
||||
"mocha.parallel": "^0.15.6",
|
||||
"ts-mocha": "^9.0.2",
|
||||
"typescript": "^4.5.5",
|
||||
"validate-any": "^1.2.0"
|
||||
"validate-any": "1.3.1"
|
||||
},
|
||||
"keywords": [
|
||||
"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 SearchParser from "./parsers/SearchParser"
|
||||
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 {
|
||||
AlbumDetailed,
|
||||
|
|
@ -203,7 +205,7 @@ export default class YTMusic {
|
|||
* @returns Search suggestions
|
||||
*/
|
||||
public async getSearchSuggestions(query: string): Promise<string[]> {
|
||||
return traverse(
|
||||
return traverseList(
|
||||
await this.constructRequest("music/get_search_suggestions", {
|
||||
input: query
|
||||
}),
|
||||
|
|
@ -236,7 +238,7 @@ export default class YTMusic {
|
|||
}[category!] || null
|
||||
})
|
||||
|
||||
return [traverse(searchData, "musicResponsiveListItemRenderer")].flat().map(
|
||||
return traverseList(searchData, "musicResponsiveListItemRenderer").map(
|
||||
{
|
||||
SONG: SongParser.parseSearchResult,
|
||||
VIDEO: VideoParser.parseSearchResult,
|
||||
|
|
@ -308,8 +310,8 @@ export default class YTMusic {
|
|||
)
|
||||
|
||||
return [
|
||||
...traverse(songsData, "musicResponsiveListItemRenderer"),
|
||||
...traverse(moreSongsData, "musicResponsiveListItemRenderer")
|
||||
...traverseList(songsData, "musicResponsiveListItemRenderer"),
|
||||
...traverseList(moreSongsData, "musicResponsiveListItemRenderer")
|
||||
].map(SongParser.parseArtistSong)
|
||||
}
|
||||
|
||||
|
|
@ -321,15 +323,15 @@ export default class YTMusic {
|
|||
*/
|
||||
public async getArtistAlbums(artistId: string): Promise<AlbumDetailed[]> {
|
||||
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 albumsData = await this.constructRequest("browse", browseBody)
|
||||
|
||||
return traverse(albumsData, "musicTwoRowItemRenderer").map((item: any) =>
|
||||
return traverseList(albumsData, "musicTwoRowItemRenderer").map(item =>
|
||||
AlbumParser.parseArtistAlbum(item, {
|
||||
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
|
||||
const playlistData = await this.constructRequest("browse", { browseId: playlistId })
|
||||
|
||||
const songs = traverse(
|
||||
const songs = traverseList(
|
||||
playlistData,
|
||||
"musicPlaylistShelfRenderer",
|
||||
"musicResponsiveListItemRenderer"
|
||||
|
|
@ -379,7 +381,7 @@ export default class YTMusic {
|
|||
if (continuation instanceof Array) break
|
||||
|
||||
const songsData = await this.constructRequest("browse", {}, { continuation })
|
||||
songs.push(...traverse(songsData, "musicResponsiveListItemRenderer"))
|
||||
songs.push(...traverseList(songsData, "musicResponsiveListItemRenderer"))
|
||||
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 YTMusic, {
|
||||
AlbumBasic,
|
||||
AlbumDetailed,
|
||||
AlbumFull,
|
||||
ArtistBasic,
|
||||
ArtistDetailed,
|
||||
ArtistFull,
|
||||
PlaylistFull,
|
||||
SongDetailed,
|
||||
SongFull,
|
||||
ThumbnailFull,
|
||||
VideoDetailed,
|
||||
VideoFull
|
||||
} from ".."
|
||||
import YTMusic from ".."
|
||||
import {
|
||||
BOOLEAN,
|
||||
iValidationError,
|
||||
LIST,
|
||||
NULL,
|
||||
NUMBER,
|
||||
OBJECT,
|
||||
OR,
|
||||
STRING,
|
||||
validate
|
||||
} from "validate-any"
|
||||
|
||||
//#region Interfaces
|
||||
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
|
||||
ALBUM_DETAILED,
|
||||
ALBUM_FULL,
|
||||
ARTIST_DETAILED,
|
||||
ARTIST_FULL,
|
||||
PLAYLIST_FULL,
|
||||
PLAYLIST_VIDEO,
|
||||
SONG_DETAILED,
|
||||
SONG_FULL,
|
||||
VIDEO_DETAILED,
|
||||
VIDEO_FULL
|
||||
} from "../interfaces"
|
||||
import { iValidationError, LIST, STRING, validate } from "validate-any"
|
||||
|
||||
const issues: iValidationError[][] = []
|
||||
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)
|
||||
if (errors.length > 0) {
|
||||
issues.push(errors)
|
||||
}
|
||||
|
||||
expect(errors.length).toBe(0)
|
||||
assert.equal(errors.length, 0)
|
||||
}
|
||||
|
||||
const ytmusic = new YTMusic()
|
||||
|
||||
beforeAll(() => ytmusic.initialize())
|
||||
|
||||
beforeEach(() => jest.setTimeout(10_000))
|
||||
before(() => ytmusic.initialize())
|
||||
|
||||
queries.forEach(query => {
|
||||
describe("Query: " + query, () => {
|
||||
test("Search suggestions", done => {
|
||||
ytmusic
|
||||
.getSearchSuggestions(query)
|
||||
.then(suggestions => {
|
||||
_expect(suggestions, LIST(STRING()))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
describeParallel("Query: " + query, () => {
|
||||
it("Search suggestions", async () => {
|
||||
const suggestions = await ytmusic.getSearchSuggestions(query)
|
||||
expect(suggestions, LIST(STRING()))
|
||||
})
|
||||
|
||||
test("Search Songs", done => {
|
||||
ytmusic
|
||||
.search(query, "SONG")
|
||||
.then(songs => {
|
||||
_expect(songs, LIST(SONG_DETAILED))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Search Songs", async () => {
|
||||
const songs = await ytmusic.search(query, "SONG")
|
||||
expect(songs, LIST(SONG_DETAILED))
|
||||
})
|
||||
|
||||
test("Search Videos", done => {
|
||||
ytmusic
|
||||
.search(query, "VIDEO")
|
||||
.then(videos => {
|
||||
_expect(videos, LIST(VIDEO_DETAILED))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Search Videos", async () => {
|
||||
const videos = await ytmusic.search(query, "VIDEO")
|
||||
expect(videos, LIST(VIDEO_DETAILED))
|
||||
})
|
||||
|
||||
test("Search Artists", done => {
|
||||
ytmusic
|
||||
.search(query, "ARTIST")
|
||||
.then(artists => {
|
||||
_expect(artists, LIST(ARTIST_DETAILED))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Search Artists", async () => {
|
||||
const artists = await ytmusic.search(query, "ARTIST")
|
||||
expect(artists, LIST(ARTIST_DETAILED))
|
||||
})
|
||||
|
||||
test("Search Albums", done => {
|
||||
ytmusic
|
||||
.search(query, "ALBUM")
|
||||
.then(albums => {
|
||||
_expect(albums, LIST(ALBUM_DETAILED))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Search Albums", async () => {
|
||||
const albums = await ytmusic.search(query, "ALBUM")
|
||||
expect(albums, LIST(ALBUM_DETAILED))
|
||||
})
|
||||
|
||||
test("Search Playlists", done => {
|
||||
ytmusic
|
||||
.search(query, "PLAYLIST")
|
||||
.then(playlists => {
|
||||
_expect(playlists, LIST(PLAYLIST_DETAILED))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Search Playlists", async () => {
|
||||
const playlists = await ytmusic.search(query, "PLAYLIST")
|
||||
expect(playlists, LIST(PLAYLIST_FULL))
|
||||
})
|
||||
|
||||
test("Search All", done => {
|
||||
ytmusic
|
||||
.search(query)
|
||||
.then(results => {
|
||||
_expect(
|
||||
results,
|
||||
LIST(
|
||||
ALBUM_DETAILED,
|
||||
ARTIST_DETAILED,
|
||||
PLAYLIST_DETAILED,
|
||||
SONG_DETAILED,
|
||||
VIDEO_DETAILED
|
||||
)
|
||||
)
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Search All", async () => {
|
||||
const results = await ytmusic.search(query)
|
||||
expect(
|
||||
results,
|
||||
LIST(ALBUM_DETAILED, ARTIST_DETAILED, PLAYLIST_FULL, SONG_DETAILED, VIDEO_DETAILED)
|
||||
)
|
||||
})
|
||||
|
||||
test("Get details of the first song result", done => {
|
||||
ytmusic
|
||||
.search(query, "SONG")
|
||||
.then(songs => ytmusic.getSong(songs[0]!.videoId!))
|
||||
.then(song => {
|
||||
_expect(song, SONG_FULL)
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Get details of the first song result", async () => {
|
||||
const songs = await ytmusic.search(query, "SONG")
|
||||
const song = await ytmusic.getSong(songs[0]!.videoId)
|
||||
expect(song, SONG_FULL)
|
||||
})
|
||||
|
||||
test("Get details of the first video result", done => {
|
||||
ytmusic
|
||||
.search(query, "VIDEO")
|
||||
.then(videos => ytmusic.getVideo(videos[0]!.videoId!))
|
||||
.then(video => {
|
||||
_expect(video, VIDEO_FULL)
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Get details of the first video result", async () => {
|
||||
const videos = await ytmusic.search(query, "VIDEO")
|
||||
const video = await ytmusic.getVideo(videos[0]!.videoId)
|
||||
expect(video, VIDEO_FULL)
|
||||
})
|
||||
|
||||
test("Get details of the first artist result", done => {
|
||||
ytmusic
|
||||
.search(query, "ARTIST")
|
||||
.then(artists => ytmusic.getArtist(artists[0]!.artistId!))
|
||||
.then(artist => {
|
||||
_expect(artist, ARTIST_FULL)
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Get details of the first artist result", async () => {
|
||||
const artists = await ytmusic.search(query, "ARTIST")
|
||||
const artist = await ytmusic.getArtist(artists[0]!.artistId)
|
||||
expect(artist, ARTIST_FULL)
|
||||
})
|
||||
|
||||
test("Get the songs of the first artist result", done => {
|
||||
ytmusic
|
||||
.search(query, "ARTIST")
|
||||
.then(artists => ytmusic.getArtistSongs(artists[0]!.artistId!))
|
||||
.then(songs => {
|
||||
_expect(songs, LIST(SONG_DETAILED))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Get the songs of the first artist result", async () => {
|
||||
const artists = await ytmusic.search(query, "ARTIST")
|
||||
const songs = await ytmusic.getArtistSongs(artists[0]!.artistId)
|
||||
expect(songs, LIST(SONG_DETAILED))
|
||||
})
|
||||
|
||||
test("Get the albums of the first artist result", done => {
|
||||
ytmusic
|
||||
.search(query, "ARTIST")
|
||||
.then(artists => ytmusic.getArtistAlbums(artists[0]!.artistId!))
|
||||
.then(albums => {
|
||||
_expect(albums, LIST(ALBUM_DETAILED))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Get the albums of the first artist result", async () => {
|
||||
const artists = await ytmusic.search(query, "ARTIST")
|
||||
const albums = await ytmusic.getArtistAlbums(artists[0]!.artistId)
|
||||
expect(albums, LIST(ALBUM_DETAILED))
|
||||
})
|
||||
|
||||
test("Get details of the first album result", done => {
|
||||
ytmusic
|
||||
.search(query, "ALBUM")
|
||||
.then(albums => ytmusic.getAlbum(albums[0]!.albumId!))
|
||||
.then(album => {
|
||||
_expect(album, ALBUM_FULL)
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Get details of the first album result", async () => {
|
||||
const albums = await ytmusic.search(query, "ALBUM")
|
||||
const album = await ytmusic.getAlbum(albums[0]!.albumId)
|
||||
expect(album, ALBUM_FULL)
|
||||
})
|
||||
|
||||
test("Get details of the first playlist result", done => {
|
||||
ytmusic
|
||||
.search(query, "PLAYLIST")
|
||||
.then(playlists => ytmusic.getPlaylist(playlists[0]!.playlistId!))
|
||||
.then(playlist => {
|
||||
_expect(playlist, PLAYLIST_DETAILED)
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Get details of the first playlist result", async () => {
|
||||
const playlists = await ytmusic.search(query, "PLAYLIST")
|
||||
const playlist = await ytmusic.getPlaylist(playlists[0]!.playlistId)
|
||||
expect(playlist, PLAYLIST_FULL)
|
||||
})
|
||||
|
||||
test("Get the videos of the first playlist result", done => {
|
||||
ytmusic
|
||||
.search(query, "PLAYLIST")
|
||||
.then(playlists => ytmusic.getPlaylistVideos(playlists[0]!.playlistId!))
|
||||
.then(videos => {
|
||||
_expect(videos, LIST(PLAYLIST_VIDEO))
|
||||
done()
|
||||
})
|
||||
.catch(done)
|
||||
it("Get the videos of the first playlist result", async () => {
|
||||
const playlists = await ytmusic.search(query, "PLAYLIST")
|
||||
const videos = await ytmusic.getPlaylistVideos(playlists[0]!.playlistId)
|
||||
expect(videos, LIST(PLAYLIST_VIDEO))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
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 {
|
||||
type: "SONG"
|
||||
videoId: string | null
|
||||
videoId: string
|
||||
name: string
|
||||
artists: ArtistBasic[]
|
||||
album: AlbumBasic
|
||||
|
|
@ -24,7 +24,7 @@ export interface SongFull extends Omit<SongDetailed, "album"> {
|
|||
|
||||
export interface VideoDetailed {
|
||||
type: "VIDEO"
|
||||
videoId: string | null
|
||||
videoId: string
|
||||
name: string
|
||||
artists: ArtistBasic[]
|
||||
views: number
|
||||
|
|
@ -41,7 +41,7 @@ export interface VideoFull extends VideoDetailed {
|
|||
}
|
||||
|
||||
export interface ArtistBasic {
|
||||
artistId: string | null
|
||||
artistId: string
|
||||
name: string
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ export interface ArtistDetailed extends ArtistBasic {
|
|||
}
|
||||
|
||||
export interface ArtistFull extends ArtistDetailed {
|
||||
description: string | null
|
||||
description: string
|
||||
subscribers: number
|
||||
topSongs: Omit<SongDetailed, "duration">[]
|
||||
topAlbums: AlbumDetailed[]
|
||||
|
|
@ -72,7 +72,7 @@ export interface AlbumDetailed extends AlbumBasic {
|
|||
}
|
||||
|
||||
export interface AlbumFull extends AlbumDetailed {
|
||||
description: string | null
|
||||
description: string
|
||||
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 traverse from "../traverse"
|
||||
import { AlbumDetailed, AlbumFull, ArtistBasic } from ".."
|
||||
import traverseList from "../utils/traverseList"
|
||||
import traverseString from "../utils/traverseString"
|
||||
import { ALBUM_DETAILED, ALBUM_FULL } from "../interfaces"
|
||||
import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from ".."
|
||||
|
||||
export default class AlbumParser {
|
||||
public static parse(data: any, albumId: string): AlbumFull {
|
||||
const albumBasic = {
|
||||
const albumBasic: AlbumBasic = {
|
||||
albumId,
|
||||
name: traverse(data, "header", "title", "text").at(0)
|
||||
name: traverseString(data, "header", "title", "text")()
|
||||
}
|
||||
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")
|
||||
const artists: ArtistBasic[] = traverseList(data, "header", "subtitle", "runs")
|
||||
.filter(run => "navigationEndpoint" in run)
|
||||
.map(run => ({
|
||||
artistId: traverseString(run, "browseId")(),
|
||||
name: traverseString(run, "text")()
|
||||
}))
|
||||
const thumbnails = traverseList(data, "header", "thumbnails")
|
||||
|
||||
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) =>
|
||||
return checkType<AlbumFull>(
|
||||
{
|
||||
type: "ALBUM",
|
||||
...albumBasic,
|
||||
playlistId: traverseString(data, "buttonRenderer", "playlistId")(),
|
||||
artists,
|
||||
year: +traverseString(data, "header", "subtitle", "text")(-1),
|
||||
thumbnails,
|
||||
description: traverseString(data, "description", "text")(),
|
||||
songs: traverseList(data, "musicResponsiveListItemRenderer").map(item =>
|
||||
SongParser.parseAlbumSong(item, artists, albumBasic, thumbnails)
|
||||
)
|
||||
}
|
||||
},
|
||||
ALBUM_FULL
|
||||
)
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): AlbumDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const flexColumns = traverseList(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()
|
||||
}
|
||||
return checkType<AlbumDetailed>(
|
||||
{
|
||||
type: "ALBUM",
|
||||
albumId: traverseString(item, "browseId")(-1),
|
||||
playlistId: traverseString(item, "overlay", "playlistId")(),
|
||||
artists: traverseList(flexColumns[1], "runs")
|
||||
.filter(run => "navigationEndpoint" in run)
|
||||
.map(run => ({
|
||||
artistId: traverseString(run, "browseId")(),
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
return checkType<AlbumDetailed>(
|
||||
{
|
||||
type: "ALBUM",
|
||||
albumId: traverseString(item, "browseId")(-1),
|
||||
playlistId: traverseString(item, "thumbnailOverlay", "playlistId")(),
|
||||
name: traverseString(item, "title", "text")(),
|
||||
artists: [artistBasic],
|
||||
year: +traverseString(item, "subtitle", "text")(-1),
|
||||
thumbnails: traverseList(item, "thumbnails")
|
||||
},
|
||||
ALBUM_DETAILED
|
||||
)
|
||||
}
|
||||
|
||||
public static parseArtistTopAlbums(item: any, artistBasic: ArtistBasic): 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()
|
||||
}
|
||||
return checkType<AlbumDetailed>(
|
||||
{
|
||||
type: "ALBUM",
|
||||
albumId: traverseString(item, "browseId")(-1),
|
||||
playlistId: traverseString(item, "musicPlayButtonRenderer", "playlistId")(),
|
||||
name: traverseString(item, "title", "text")(),
|
||||
artists: [artistBasic],
|
||||
year: +traverseString(item, "subtitle", "text")(-1),
|
||||
thumbnails: traverseList(item, "thumbnails")
|
||||
},
|
||||
ALBUM_DETAILED
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,54 @@
|
|||
import AlbumParser from "./AlbumParser"
|
||||
import checkType from "../utils/checkType"
|
||||
import Parser from "./Parser"
|
||||
import SongParser from "./SongParser"
|
||||
import traverse from "../traverse"
|
||||
import { ArtistDetailed, ArtistFull } from ".."
|
||||
import traverseList from "../utils/traverseList"
|
||||
import traverseString from "../utils/traverseString"
|
||||
import { ARTIST_DETAILED, ARTIST_FULL } from "../interfaces"
|
||||
import { ArtistBasic, ArtistDetailed, ArtistFull } from ".."
|
||||
|
||||
export default class ArtistParser {
|
||||
public static parse(data: any, artistId: string): ArtistFull {
|
||||
const artistBasic = {
|
||||
const artistBasic: ArtistBasic = {
|
||||
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 {
|
||||
type: "ARTIST",
|
||||
...artistBasic,
|
||||
thumbnails: [traverse(data, "header", "thumbnails")].flat(),
|
||||
description: description instanceof Array ? null : description,
|
||||
subscribers: Parser.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))
|
||||
}
|
||||
return checkType<ArtistFull>(
|
||||
{
|
||||
type: "ARTIST",
|
||||
...artistBasic,
|
||||
thumbnails: traverseList(data, "header", "thumbnails"),
|
||||
description,
|
||||
subscribers: Parser.parseNumber(
|
||||
traverseString(data, "subscriberCountText", "text")()
|
||||
),
|
||||
topSongs: traverseList(data, "musicShelfRenderer", "contents").map(item =>
|
||||
SongParser.parseArtistTopSong(item, artistBasic)
|
||||
),
|
||||
topAlbums: traverseList(data, "musicCarouselShelfRenderer")
|
||||
.at(0)
|
||||
.contents.map((item: any) =>
|
||||
AlbumParser.parseArtistTopAlbums(item, artistBasic)
|
||||
)
|
||||
},
|
||||
ARTIST_FULL
|
||||
)
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): ArtistDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
|
||||
return {
|
||||
type: "ARTIST",
|
||||
artistId: traverse(item, "browseId"),
|
||||
name: traverse(flexColumns[0], "runs", "text"),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
return checkType<ArtistDetailed>(
|
||||
{
|
||||
type: "ARTIST",
|
||||
artistId: traverseString(item, "browseId")(),
|
||||
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 ".."
|
||||
|
||||
export default class PlaylistParser {
|
||||
public static parse(data: any, playlistId: string): 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)
|
||||
return checkType<PlaylistFull>(
|
||||
{
|
||||
type: "PLAYLIST",
|
||||
playlistId,
|
||||
name: traverseString(data, "header", "title", "text")(),
|
||||
artist: {
|
||||
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")
|
||||
.at(0)
|
||||
.split(" ")
|
||||
.at(0)
|
||||
.replaceAll(",", ""),
|
||||
thumbnails: traverse(data, "header", "thumbnails")
|
||||
}
|
||||
PLAYLIST_FULL
|
||||
)
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): PlaylistFull {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const artistId = traverse(flexColumns[1], "browseId")
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
const artistId = traverseString(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)
|
||||
return checkType<PlaylistFull>(
|
||||
{
|
||||
type: "PLAYLIST",
|
||||
playlistId: traverseString(item, "overlay", "playlistId")(),
|
||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||
artist: {
|
||||
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")
|
||||
.at(-1)
|
||||
.split(" ")
|
||||
.at(0)
|
||||
.replaceAll(",", ""),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
PLAYLIST_FULL
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import AlbumParser from "./AlbumParser"
|
|||
import ArtistParser from "./ArtistParser"
|
||||
import PlaylistParser from "./PlaylistParser"
|
||||
import SongParser from "./SongParser"
|
||||
import traverse from "../traverse"
|
||||
import traverseList from "../utils/traverseList"
|
||||
import VideoParser from "./VideoParser"
|
||||
import { SearchResult } from ".."
|
||||
|
||||
export default class SearchParser {
|
||||
public static parse(item: any): SearchResult {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const type = [traverse(flexColumns[1], "runs", "text")].flat().at(0) as
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
const type = traverseList(flexColumns[1], "runs", "text").at(0) as
|
||||
| "Song"
|
||||
| "Video"
|
||||
| "Artist"
|
||||
|
|
|
|||
|
|
@ -1,90 +1,116 @@
|
|||
import checkType from "../utils/checkType"
|
||||
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 { LIST, OBJECT, STRING } from "validate-any"
|
||||
|
||||
export default class SongParser {
|
||||
public static parse(data: any): 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")
|
||||
}
|
||||
return checkType<SongFull>(
|
||||
{
|
||||
type: "SONG",
|
||||
videoId: traverseString(data, "videoDetails", "videoId")(),
|
||||
name: traverseString(data, "videoDetails", "title")(),
|
||||
artists: [
|
||||
{
|
||||
artistId: traverseString(data, "videoDetails", "channelId")(),
|
||||
name: traverseString(data, "author")()
|
||||
}
|
||||
],
|
||||
duration: +traverseString(data, "videoDetails", "lengthSeconds"),
|
||||
thumbnails: traverseList(data, "videoDetails", "thumbnails"),
|
||||
description: traverseString(data, "description")(),
|
||||
formats: traverseList(data, "streamingData", "formats"),
|
||||
adaptiveFormats: traverseList(data, "streamingData", "adaptiveFormats")
|
||||
},
|
||||
SONG_FULL
|
||||
)
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): SongDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
|
||||
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)
|
||||
return checkType<SongDetailed>(
|
||||
{
|
||||
type: "SONG",
|
||||
videoId: traverseString(item, "playlistItemData", "videoId")(),
|
||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||
artists: traverseList(flexColumns[1], "runs")
|
||||
.filter(run => "navigationEndpoint" in run)
|
||||
.map(run => ({
|
||||
artistId: traverseString(run, "browseId")(),
|
||||
name: traverseString(run, "text")()
|
||||
}))
|
||||
.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)),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
SONG_DETAILED
|
||||
)
|
||||
}
|
||||
|
||||
public static parseArtistSong(item: any): SongDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
const videoId = traverseString(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")
|
||||
return checkType<SongDetailed>(
|
||||
{
|
||||
type: "SONG",
|
||||
videoId,
|
||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||
artists: traverseList(flexColumns[1], "runs")
|
||||
.filter(run => "navigationEndpoint" in run)
|
||||
.map(run => ({
|
||||
artistId: traverseString(run, "browseId")(),
|
||||
name: traverseString(run, "text")()
|
||||
})),
|
||||
album: {
|
||||
albumId: traverseString(flexColumns[2], "browseId")(),
|
||||
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")),
|
||||
thumbnails: [traverse(item, "thumbnails")].flat()
|
||||
}
|
||||
SONG_DETAILED
|
||||
)
|
||||
}
|
||||
|
||||
public static parseArtistTopSong(
|
||||
item: any,
|
||||
artistBasic: ArtistBasic
|
||||
): Omit<SongDetailed, "duration"> {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
const videoId = traverseString(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")
|
||||
return checkType<Omit<SongDetailed, "duration">>(
|
||||
{
|
||||
type: "SONG",
|
||||
videoId,
|
||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||
artists: [artistBasic],
|
||||
album: {
|
||||
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(
|
||||
|
|
@ -93,17 +119,22 @@ export default class SongParser {
|
|||
albumBasic: AlbumBasic,
|
||||
thumbnails: ThumbnailFull[]
|
||||
): SongDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playlistItemData", "videoId")
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
const videoId = traverseString(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
|
||||
}
|
||||
return checkType<SongDetailed>(
|
||||
{
|
||||
type: "SONG",
|
||||
videoId,
|
||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||
artists,
|
||||
album: albumBasic,
|
||||
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 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 ".."
|
||||
|
||||
export default class VideoParser {
|
||||
public static parse(data: any): VideoFull {
|
||||
return {
|
||||
type: "VIDEO",
|
||||
videoId: traverse(data, "videoDetails", "videoId"),
|
||||
name: traverse(data, "videoDetails", "title"),
|
||||
videoId: traverseString(data, "videoDetails", "videoId")(),
|
||||
name: traverseString(data, "videoDetails", "title")(),
|
||||
artists: [
|
||||
{
|
||||
artistId: traverse(data, "videoDetails", "channelId"),
|
||||
name: traverse(data, "author")
|
||||
artistId: traverseString(data, "videoDetails", "channelId")(),
|
||||
name: traverseString(data, "author")()
|
||||
}
|
||||
],
|
||||
views: +traverse(data, "videoDetails", "viewCount"),
|
||||
duration: +traverse(data, "videoDetails", "lengthSeconds"),
|
||||
thumbnails: [traverse(data, "videoDetails", "thumbnails")].flat(),
|
||||
description: traverse(data, "description"),
|
||||
views: +traverseString(data, "videoDetails", "viewCount")(),
|
||||
duration: +traverseString(data, "videoDetails", "lengthSeconds")(),
|
||||
thumbnails: traverseList(data, "videoDetails", "thumbnails"),
|
||||
description: traverseString(data, "description")(),
|
||||
unlisted: traverse(data, "unlisted"),
|
||||
familySafe: traverse(data, "familySafe"),
|
||||
paid: traverse(data, "paid"),
|
||||
tags: traverse(data, "tags")
|
||||
tags: traverseList(data, "tags")
|
||||
}
|
||||
}
|
||||
|
||||
public static parseSearchResult(item: any): VideoDetailed {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playNavigationEndpoint", "videoId")
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
const videoId = traverseString(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()
|
||||
videoId,
|
||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||
artists: traverseList(flexColumns[1], "runs")
|
||||
.filter(run => "navigationEndpoint" in run)
|
||||
.map(run => ({
|
||||
artistId: traverseString(run, "browseId")(),
|
||||
name: traverseString(run, "text")()
|
||||
})),
|
||||
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"> {
|
||||
const flexColumns = traverse(item, "flexColumns")
|
||||
const videoId = traverse(item, "playNavigationEndpoint", "videoId")
|
||||
const flexColumns = traverseList(item, "flexColumns")
|
||||
const videoId = traverseString(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()
|
||||
}
|
||||
return checkType<Omit<VideoDetailed, "views">>(
|
||||
{
|
||||
type: "VIDEO",
|
||||
videoId,
|
||||
name: traverseString(flexColumns[0], "runs", "text")(),
|
||||
artists: traverseList(flexColumns[1], "runs")
|
||||
.filter(run => "navigationEndpoint" in run)
|
||||
.map(run => ({
|
||||
artistId: traverseString(run, "browseId")(),
|
||||
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