✨ use arktype instead of zod
This commit is contained in:
parent
d4d6fa1662
commit
bfdceea12d
|
|
@ -17,13 +17,11 @@
|
||||||
"clean": "tsc --noEmit && eslint src --fix && prettier src --write && rm tsconfig.tsbuildinfo"
|
"clean": "tsc --noEmit && eslint src --fix && prettier src --write && rm tsconfig.tsbuildinfo"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"arktype": "^1.0.28-alpha",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"tough-cookie": "^4.1.2",
|
"tough-cookie": "^4.1.2"
|
||||||
"zod": "^3.20.2",
|
|
||||||
"zod-to-json-schema": "^3.20.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/json-schema": "^7.0.11",
|
|
||||||
"@types/tough-cookie": "^4.0.2",
|
"@types/tough-cookie": "^4.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "latest",
|
"@typescript-eslint/eslint-plugin": "latest",
|
||||||
"@typescript-eslint/parser": "latest",
|
"@typescript-eslint/parser": "latest",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { type, union } from "arktype"
|
||||||
|
|
||||||
|
export type ThumbnailFull = typeof ThumbnailFull.infer
|
||||||
|
export const ThumbnailFull = type({
|
||||||
|
url: "string",
|
||||||
|
width: "number",
|
||||||
|
height: "number",
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ArtistBasic = typeof ArtistBasic.infer
|
||||||
|
export const ArtistBasic = type({
|
||||||
|
artistId: "string",
|
||||||
|
name: "string",
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AlbumBasic = typeof AlbumBasic.infer
|
||||||
|
export const AlbumBasic = type({
|
||||||
|
albumId: "string",
|
||||||
|
name: "string",
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SongDetailed = typeof SongDetailed.infer
|
||||||
|
export const SongDetailed = type({
|
||||||
|
type: '"SONG"',
|
||||||
|
videoId: "string",
|
||||||
|
name: "string",
|
||||||
|
artists: [ArtistBasic, "[]"],
|
||||||
|
album: AlbumBasic,
|
||||||
|
duration: "number|null",
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type VideoDetailed = typeof VideoDetailed.infer
|
||||||
|
export const VideoDetailed = type({
|
||||||
|
type: '"VIDEO"',
|
||||||
|
videoId: "string",
|
||||||
|
name: "string",
|
||||||
|
artists: [ArtistBasic, "[]"],
|
||||||
|
duration: "number|null",
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ArtistDetailed = typeof ArtistDetailed.infer
|
||||||
|
export const ArtistDetailed = type({
|
||||||
|
artistId: "string",
|
||||||
|
name: "string",
|
||||||
|
type: '"ARTIST"',
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AlbumDetailed = typeof AlbumDetailed.infer
|
||||||
|
export const AlbumDetailed = type({
|
||||||
|
type: '"ALBUM"',
|
||||||
|
albumId: "string",
|
||||||
|
playlistId: "string",
|
||||||
|
name: "string",
|
||||||
|
artists: [ArtistBasic, "[]"],
|
||||||
|
year: "number|null",
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type PlaylistDetailed = typeof PlaylistDetailed.infer
|
||||||
|
export const PlaylistDetailed = type({
|
||||||
|
type: '"PLAYLIST"',
|
||||||
|
playlistId: "string",
|
||||||
|
name: "string",
|
||||||
|
artist: ArtistBasic,
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SongFull = typeof SongFull.infer
|
||||||
|
export const SongFull = type({
|
||||||
|
type: '"SONG"',
|
||||||
|
videoId: "string",
|
||||||
|
name: "string",
|
||||||
|
artists: [ArtistBasic, "[]"],
|
||||||
|
duration: "number",
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
description: "string",
|
||||||
|
formats: "any[]",
|
||||||
|
adaptiveFormats: "any[]",
|
||||||
|
})
|
||||||
|
|
||||||
|
export type VideoFull = typeof VideoFull.infer
|
||||||
|
export const VideoFull = type({
|
||||||
|
type: '"VIDEO"',
|
||||||
|
videoId: "string",
|
||||||
|
name: "string",
|
||||||
|
artists: [ArtistBasic, "[]"],
|
||||||
|
duration: "number",
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
description: "string",
|
||||||
|
unlisted: "boolean",
|
||||||
|
familySafe: "boolean",
|
||||||
|
paid: "boolean",
|
||||||
|
tags: "string[]",
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ArtistFull = typeof ArtistFull.infer
|
||||||
|
export const ArtistFull = type({
|
||||||
|
artistId: "string",
|
||||||
|
name: "string",
|
||||||
|
type: '"ARTIST"',
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
description: "string",
|
||||||
|
topSongs: [SongDetailed, "[]"],
|
||||||
|
topAlbums: [AlbumDetailed, "[]"],
|
||||||
|
topSingles: [AlbumDetailed, "[]"],
|
||||||
|
topVideos: [VideoDetailed, "[]"],
|
||||||
|
featuredOn: [PlaylistDetailed, "[]"],
|
||||||
|
similarArtists: [ArtistDetailed, "[]"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type AlbumFull = typeof AlbumFull.infer
|
||||||
|
export const AlbumFull = type({
|
||||||
|
type: '"ALBUM"',
|
||||||
|
albumId: "string",
|
||||||
|
playlistId: "string",
|
||||||
|
name: "string",
|
||||||
|
artists: [ArtistBasic, "[]"],
|
||||||
|
year: "number|null",
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
description: "string",
|
||||||
|
songs: [SongDetailed, "[]"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type PlaylistFull = typeof PlaylistFull.infer
|
||||||
|
export const PlaylistFull = type({
|
||||||
|
type: '"PLAYLIST"',
|
||||||
|
playlistId: "string",
|
||||||
|
name: "string",
|
||||||
|
artist: ArtistBasic,
|
||||||
|
videoCount: "number",
|
||||||
|
thumbnails: [ThumbnailFull, "[]"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SearchResult = typeof SearchResult.infer
|
||||||
|
export const SearchResult = union(
|
||||||
|
SongDetailed,
|
||||||
|
union(VideoDetailed, union(AlbumDetailed, union(ArtistDetailed, PlaylistDetailed))),
|
||||||
|
)
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
import axios, { AxiosInstance } from "axios"
|
import axios, { AxiosInstance } from "axios"
|
||||||
import { Cookie, CookieJar } from "tough-cookie"
|
import { Cookie, CookieJar } from "tough-cookie"
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
import AlbumParser from "./parsers/AlbumParser"
|
|
||||||
import ArtistParser from "./parsers/ArtistParser"
|
|
||||||
import PlaylistParser from "./parsers/PlaylistParser"
|
|
||||||
import SearchParser from "./parsers/SearchParser"
|
|
||||||
import SongParser from "./parsers/SongParser"
|
|
||||||
import VideoParser from "./parsers/VideoParser"
|
|
||||||
import {
|
import {
|
||||||
AlbumDetailed,
|
AlbumDetailed,
|
||||||
AlbumFull,
|
AlbumFull,
|
||||||
|
|
@ -20,7 +13,13 @@ import {
|
||||||
SongFull,
|
SongFull,
|
||||||
VideoDetailed,
|
VideoDetailed,
|
||||||
VideoFull,
|
VideoFull,
|
||||||
} from "./schemas"
|
} from "./@types/types"
|
||||||
|
import AlbumParser from "./parsers/AlbumParser"
|
||||||
|
import ArtistParser from "./parsers/ArtistParser"
|
||||||
|
import PlaylistParser from "./parsers/PlaylistParser"
|
||||||
|
import SearchParser from "./parsers/SearchParser"
|
||||||
|
import SongParser from "./parsers/SongParser"
|
||||||
|
import VideoParser from "./parsers/VideoParser"
|
||||||
import traverse from "./utils/traverse"
|
import traverse from "./utils/traverse"
|
||||||
import traverseList from "./utils/traverseList"
|
import traverseList from "./utils/traverseList"
|
||||||
import traverseString from "./utils/traverseString"
|
import traverseString from "./utils/traverseString"
|
||||||
|
|
@ -226,7 +225,7 @@ export default class YTMusic {
|
||||||
*
|
*
|
||||||
* @param query Query string
|
* @param query Query string
|
||||||
*/
|
*/
|
||||||
public async search(query: string): Promise<z.infer<typeof SearchResult>[]> {
|
public async search(query: string): Promise<(typeof SearchResult.infer)[]> {
|
||||||
const searchData = await this.constructRequest("search", {
|
const searchData = await this.constructRequest("search", {
|
||||||
query,
|
query,
|
||||||
params: null,
|
params: null,
|
||||||
|
|
@ -234,7 +233,7 @@ export default class YTMusic {
|
||||||
|
|
||||||
return traverseList(searchData, "musicResponsiveListItemRenderer")
|
return traverseList(searchData, "musicResponsiveListItemRenderer")
|
||||||
.map(SearchParser.parse)
|
.map(SearchParser.parse)
|
||||||
.filter(Boolean) as z.infer<typeof SearchResult>[]
|
.filter(Boolean) as (typeof SearchResult.infer)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -242,7 +241,7 @@ export default class YTMusic {
|
||||||
*
|
*
|
||||||
* @param query Query string
|
* @param query Query string
|
||||||
*/
|
*/
|
||||||
public async searchSongs(query: string): Promise<z.infer<typeof SongDetailed>[]> {
|
public async searchSongs(query: string): Promise<(typeof SongDetailed.infer)[]> {
|
||||||
const searchData = await this.constructRequest("search", {
|
const searchData = await this.constructRequest("search", {
|
||||||
query,
|
query,
|
||||||
params: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D",
|
params: "Eg-KAQwIARAAGAAgACgAMABqChAEEAMQCRAFEAo%3D",
|
||||||
|
|
@ -258,7 +257,7 @@ export default class YTMusic {
|
||||||
*
|
*
|
||||||
* @param query Query string
|
* @param query Query string
|
||||||
*/
|
*/
|
||||||
public async searchVideos(query: string): Promise<z.infer<typeof VideoDetailed>[]> {
|
public async searchVideos(query: string): Promise<(typeof VideoDetailed.infer)[]> {
|
||||||
const searchData = await this.constructRequest("search", {
|
const searchData = await this.constructRequest("search", {
|
||||||
query,
|
query,
|
||||||
params: "Eg-KAQwIABABGAAgACgAMABqChAEEAMQCRAFEAo%3D",
|
params: "Eg-KAQwIABABGAAgACgAMABqChAEEAMQCRAFEAo%3D",
|
||||||
|
|
@ -274,7 +273,7 @@ export default class YTMusic {
|
||||||
*
|
*
|
||||||
* @param query Query string
|
* @param query Query string
|
||||||
*/
|
*/
|
||||||
public async searchArtists(query: string): Promise<z.infer<typeof ArtistDetailed>[]> {
|
public async searchArtists(query: string): Promise<(typeof ArtistDetailed.infer)[]> {
|
||||||
const searchData = await this.constructRequest("search", {
|
const searchData = await this.constructRequest("search", {
|
||||||
query,
|
query,
|
||||||
params: "Eg-KAQwIABAAGAAgASgAMABqChAEEAMQCRAFEAo%3D",
|
params: "Eg-KAQwIABAAGAAgASgAMABqChAEEAMQCRAFEAo%3D",
|
||||||
|
|
@ -290,7 +289,7 @@ export default class YTMusic {
|
||||||
*
|
*
|
||||||
* @param query Query string
|
* @param query Query string
|
||||||
*/
|
*/
|
||||||
public async searchAlbums(query: string): Promise<z.infer<typeof AlbumDetailed>[]> {
|
public async searchAlbums(query: string): Promise<(typeof AlbumDetailed.infer)[]> {
|
||||||
const searchData = await this.constructRequest("search", {
|
const searchData = await this.constructRequest("search", {
|
||||||
query,
|
query,
|
||||||
params: "Eg-KAQwIABAAGAEgACgAMABqChAEEAMQCRAFEAo%3D",
|
params: "Eg-KAQwIABAAGAEgACgAMABqChAEEAMQCRAFEAo%3D",
|
||||||
|
|
@ -306,7 +305,7 @@ export default class YTMusic {
|
||||||
*
|
*
|
||||||
* @param query Query string
|
* @param query Query string
|
||||||
*/
|
*/
|
||||||
public async searchPlaylists(query: string): Promise<z.infer<typeof PlaylistDetailed>[]> {
|
public async searchPlaylists(query: string): Promise<(typeof PlaylistDetailed.infer)[]> {
|
||||||
const searchData = await this.constructRequest("search", {
|
const searchData = await this.constructRequest("search", {
|
||||||
query,
|
query,
|
||||||
params: "Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D",
|
params: "Eg-KAQwIABAAGAAgACgBMABqChAEEAMQCRAFEAo%3D",
|
||||||
|
|
@ -323,7 +322,7 @@ export default class YTMusic {
|
||||||
* @param videoId Video ID
|
* @param videoId Video ID
|
||||||
* @returns Song Data
|
* @returns Song Data
|
||||||
*/
|
*/
|
||||||
public async getSong(videoId: string): Promise<z.infer<typeof SongFull>> {
|
public async getSong(videoId: string): Promise<typeof SongFull.infer> {
|
||||||
if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId")
|
if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId")
|
||||||
const data = await this.constructRequest("player", { videoId })
|
const data = await this.constructRequest("player", { videoId })
|
||||||
|
|
||||||
|
|
@ -338,7 +337,7 @@ export default class YTMusic {
|
||||||
* @param videoId Video ID
|
* @param videoId Video ID
|
||||||
* @returns Video Data
|
* @returns Video Data
|
||||||
*/
|
*/
|
||||||
public async getVideo(videoId: string): Promise<z.infer<typeof VideoFull>> {
|
public async getVideo(videoId: string): Promise<typeof VideoFull.infer> {
|
||||||
if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId")
|
if (!videoId.match(/^[a-zA-Z0-9-_]{11}$/)) throw new Error("Invalid videoId")
|
||||||
const data = await this.constructRequest("player", { videoId })
|
const data = await this.constructRequest("player", { videoId })
|
||||||
|
|
||||||
|
|
@ -353,7 +352,7 @@ export default class YTMusic {
|
||||||
* @param artistId Artist ID
|
* @param artistId Artist ID
|
||||||
* @returns Artist Data
|
* @returns Artist Data
|
||||||
*/
|
*/
|
||||||
public async getArtist(artistId: string): Promise<z.infer<typeof ArtistFull>> {
|
public async getArtist(artistId: string): Promise<typeof ArtistFull.infer> {
|
||||||
const data = await this.constructRequest("browse", {
|
const data = await this.constructRequest("browse", {
|
||||||
browseId: artistId,
|
browseId: artistId,
|
||||||
})
|
})
|
||||||
|
|
@ -367,7 +366,7 @@ export default class YTMusic {
|
||||||
* @param artistId Artist ID
|
* @param artistId Artist ID
|
||||||
* @returns Artist's Songs
|
* @returns Artist's Songs
|
||||||
*/
|
*/
|
||||||
public async getArtistSongs(artistId: string): Promise<z.infer<typeof SongDetailed>[]> {
|
public async getArtistSongs(artistId: string): Promise<(typeof SongDetailed.infer)[]> {
|
||||||
const artistData = await this.constructRequest("browse", {
|
const artistData = await this.constructRequest("browse", {
|
||||||
browseId: artistId,
|
browseId: artistId,
|
||||||
})
|
})
|
||||||
|
|
@ -397,7 +396,7 @@ export default class YTMusic {
|
||||||
* @param artistId Artist ID
|
* @param artistId Artist ID
|
||||||
* @returns Artist's Albums
|
* @returns Artist's Albums
|
||||||
*/
|
*/
|
||||||
public async getArtistAlbums(artistId: string): Promise<z.infer<typeof AlbumDetailed>[]> {
|
public async getArtistAlbums(artistId: string): Promise<(typeof AlbumDetailed.infer)[]> {
|
||||||
const artistData = await this.constructRequest("browse", {
|
const artistData = await this.constructRequest("browse", {
|
||||||
browseId: artistId,
|
browseId: artistId,
|
||||||
})
|
})
|
||||||
|
|
@ -420,7 +419,7 @@ export default class YTMusic {
|
||||||
* @param albumId Album ID
|
* @param albumId Album ID
|
||||||
* @returns Album Data
|
* @returns Album Data
|
||||||
*/
|
*/
|
||||||
public async getAlbum(albumId: string): Promise<z.infer<typeof AlbumFull>> {
|
public async getAlbum(albumId: string): Promise<typeof AlbumFull.infer> {
|
||||||
const data = await this.constructRequest("browse", {
|
const data = await this.constructRequest("browse", {
|
||||||
browseId: albumId,
|
browseId: albumId,
|
||||||
})
|
})
|
||||||
|
|
@ -434,7 +433,7 @@ export default class YTMusic {
|
||||||
* @param playlistId Playlist ID
|
* @param playlistId Playlist ID
|
||||||
* @returns Playlist Data
|
* @returns Playlist Data
|
||||||
*/
|
*/
|
||||||
public async getPlaylist(playlistId: string): Promise<z.infer<typeof PlaylistFull>> {
|
public async getPlaylist(playlistId: string): Promise<typeof PlaylistFull.infer> {
|
||||||
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
|
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
|
||||||
const data = await this.constructRequest("browse", {
|
const data = await this.constructRequest("browse", {
|
||||||
browseId: playlistId,
|
browseId: playlistId,
|
||||||
|
|
@ -449,7 +448,7 @@ export default class YTMusic {
|
||||||
* @param playlistId Playlist ID
|
* @param playlistId Playlist ID
|
||||||
* @returns Playlist's Videos
|
* @returns Playlist's Videos
|
||||||
*/
|
*/
|
||||||
public async getPlaylistVideos(playlistId: string): Promise<z.infer<typeof VideoDetailed>[]> {
|
public async getPlaylistVideos(playlistId: string): Promise<(typeof VideoDetailed.infer)[]> {
|
||||||
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
|
if (playlistId.startsWith("PL")) playlistId = "VL" + playlistId
|
||||||
const playlistData = await this.constructRequest("browse", {
|
const playlistData = await this.constructRequest("browse", {
|
||||||
browseId: playlistId,
|
browseId: playlistId,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,6 @@ export type {
|
||||||
ThumbnailFull,
|
ThumbnailFull,
|
||||||
VideoDetailed,
|
VideoDetailed,
|
||||||
VideoFull,
|
VideoFull,
|
||||||
} from "./schemas"
|
} from "./@types/types"
|
||||||
|
|
||||||
export default YTMusic
|
export default YTMusic
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from "../schemas"
|
import { AlbumBasic, AlbumDetailed, AlbumFull, ArtistBasic } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import traverseString from "../utils/traverseString"
|
import traverseString from "../utils/traverseString"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ArtistBasic, ArtistDetailed, ArtistFull } from "../schemas"
|
import { ArtistBasic, ArtistDetailed, ArtistFull } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import traverseString from "../utils/traverseString"
|
import traverseString from "../utils/traverseString"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PlaylistDetailed, PlaylistFull } from "../schemas"
|
import { PlaylistDetailed, PlaylistFull } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import traverseString from "../utils/traverseString"
|
import traverseString from "../utils/traverseString"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { SearchResult } from "../schemas"
|
import { SearchResult } from "../@types/types"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import AlbumParser from "./AlbumParser"
|
import AlbumParser from "./AlbumParser"
|
||||||
import ArtistParser from "./ArtistParser"
|
import ArtistParser from "./ArtistParser"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from "../schemas"
|
import { AlbumBasic, ArtistBasic, SongDetailed, SongFull, ThumbnailFull } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
import traverseString from "../utils/traverseString"
|
import traverseString from "../utils/traverseString"
|
||||||
|
|
@ -81,10 +81,7 @@ export default class SongParser {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistTopSong(
|
public static parseArtistTopSong(item: any, artistBasic: ArtistBasic): SongDetailed {
|
||||||
item: any,
|
|
||||||
artistBasic: ArtistBasic,
|
|
||||||
): Omit<SongDetailed, "duration"> {
|
|
||||||
const flexColumns = traverseList(item, "flexColumns")
|
const flexColumns = traverseList(item, "flexColumns")
|
||||||
const videoId = traverseString(item, "playlistItemData", "videoId")()
|
const videoId = traverseString(item, "playlistItemData", "videoId")()
|
||||||
|
|
||||||
|
|
@ -98,9 +95,10 @@ export default class SongParser {
|
||||||
albumId: traverseString(flexColumns[2], "browseId")(),
|
albumId: traverseString(flexColumns[2], "browseId")(),
|
||||||
name: traverseString(flexColumns[2], "runs", "text")(),
|
name: traverseString(flexColumns[2], "runs", "text")(),
|
||||||
},
|
},
|
||||||
|
duration: null,
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
},
|
},
|
||||||
SongDetailed.omit({ duration: true }),
|
SongDetailed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ArtistBasic, VideoDetailed, VideoFull } from "../schemas"
|
import { ArtistBasic, VideoDetailed, VideoFull } from "../@types/types"
|
||||||
import checkType from "../utils/checkType"
|
import checkType from "../utils/checkType"
|
||||||
import traverse from "../utils/traverse"
|
import traverse from "../utils/traverse"
|
||||||
import traverseList from "../utils/traverseList"
|
import traverseList from "../utils/traverseList"
|
||||||
|
|
@ -46,15 +46,13 @@ export default class VideoParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static parseArtistTopVideo(
|
public static parseArtistTopVideo(item: any, artistBasic: ArtistBasic): VideoDetailed {
|
||||||
item: any,
|
|
||||||
artistBasic: ArtistBasic,
|
|
||||||
): Omit<VideoDetailed, "duration"> {
|
|
||||||
return {
|
return {
|
||||||
type: "VIDEO",
|
type: "VIDEO",
|
||||||
videoId: traverseString(item, "videoId")(),
|
videoId: traverseString(item, "videoId")(),
|
||||||
name: traverseString(item, "runs", "text")(),
|
name: traverseString(item, "runs", "text")(),
|
||||||
artists: [artistBasic],
|
artists: [artistBasic],
|
||||||
|
duration: null,
|
||||||
thumbnails: traverseList(item, "thumbnails"),
|
thumbnails: traverseList(item, "thumbnails"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
141
src/schemas.ts
141
src/schemas.ts
|
|
@ -1,141 +0,0 @@
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
export type ThumbnailFull = z.infer<typeof ThumbnailFull>
|
|
||||||
export const ThumbnailFull = z.object({
|
|
||||||
url: z.string(),
|
|
||||||
width: z.number(),
|
|
||||||
height: z.number(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type ArtistBasic = z.infer<typeof ArtistBasic>
|
|
||||||
export const ArtistBasic = z.object({
|
|
||||||
artistId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type AlbumBasic = z.infer<typeof AlbumBasic>
|
|
||||||
export const AlbumBasic = z.object({
|
|
||||||
albumId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type SongDetailed = z.infer<typeof SongDetailed>
|
|
||||||
export const SongDetailed = z.object({
|
|
||||||
type: z.literal("SONG"),
|
|
||||||
videoId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
artists: z.array(ArtistBasic),
|
|
||||||
album: AlbumBasic,
|
|
||||||
duration: z.number(),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type VideoDetailed = z.infer<typeof VideoDetailed>
|
|
||||||
export const VideoDetailed = z.object({
|
|
||||||
type: z.literal("VIDEO"),
|
|
||||||
videoId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
artists: z.array(ArtistBasic),
|
|
||||||
duration: z.number(),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type ArtistDetailed = z.infer<typeof ArtistDetailed>
|
|
||||||
export const ArtistDetailed = z.object({
|
|
||||||
artistId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
type: z.literal("ARTIST"),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type AlbumDetailed = z.infer<typeof AlbumDetailed>
|
|
||||||
export const AlbumDetailed = z.object({
|
|
||||||
type: z.literal("ALBUM"),
|
|
||||||
albumId: z.string(),
|
|
||||||
playlistId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
artists: z.array(ArtistBasic),
|
|
||||||
year: z.number().nullable(),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type PlaylistDetailed = z.infer<typeof PlaylistDetailed>
|
|
||||||
export const PlaylistDetailed = z.object({
|
|
||||||
type: z.literal("PLAYLIST"),
|
|
||||||
playlistId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
artist: ArtistBasic,
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type SongFull = z.infer<typeof SongFull>
|
|
||||||
export const SongFull = z.object({
|
|
||||||
type: z.literal("SONG"),
|
|
||||||
videoId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
artists: z.array(ArtistBasic),
|
|
||||||
duration: z.number(),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
description: z.string(),
|
|
||||||
formats: z.array(z.any()),
|
|
||||||
adaptiveFormats: z.array(z.any()),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type VideoFull = z.infer<typeof VideoFull>
|
|
||||||
export const VideoFull = z.object({
|
|
||||||
type: z.literal("VIDEO"),
|
|
||||||
videoId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
artists: z.array(ArtistBasic),
|
|
||||||
duration: z.number(),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
description: z.string(),
|
|
||||||
unlisted: z.boolean(),
|
|
||||||
familySafe: z.boolean(),
|
|
||||||
paid: z.boolean(),
|
|
||||||
tags: z.array(z.string()),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type ArtistFull = z.infer<typeof ArtistFull>
|
|
||||||
export const ArtistFull = z.object({
|
|
||||||
artistId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
type: z.literal("ARTIST"),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
description: z.string(),
|
|
||||||
topSongs: z.array(SongDetailed.omit({ duration: true })),
|
|
||||||
topAlbums: z.array(AlbumDetailed),
|
|
||||||
topSingles: z.array(AlbumDetailed),
|
|
||||||
topVideos: z.array(VideoDetailed.omit({ duration: true })),
|
|
||||||
featuredOn: z.array(PlaylistDetailed),
|
|
||||||
similarArtists: z.array(ArtistDetailed),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type AlbumFull = z.infer<typeof AlbumFull>
|
|
||||||
export const AlbumFull = z.object({
|
|
||||||
type: z.literal("ALBUM"),
|
|
||||||
albumId: z.string(),
|
|
||||||
playlistId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
artists: z.array(ArtistBasic),
|
|
||||||
year: z.number().nullable(),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
description: z.string(),
|
|
||||||
songs: z.array(SongDetailed),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type PlaylistFull = z.infer<typeof PlaylistFull>
|
|
||||||
export const PlaylistFull = z.object({
|
|
||||||
type: z.literal("PLAYLIST"),
|
|
||||||
playlistId: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
artist: ArtistBasic,
|
|
||||||
videoCount: z.number(),
|
|
||||||
thumbnails: z.array(ThumbnailFull),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type SearchResult = z.infer<typeof SearchResult>
|
|
||||||
export const SearchResult = SongDetailed.or(VideoDetailed)
|
|
||||||
.or(AlbumDetailed)
|
|
||||||
.or(ArtistDetailed)
|
|
||||||
.or(PlaylistDetailed)
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { arrayOf, Problem, Type, type } from "arktype"
|
||||||
import { equal } from "assert"
|
import { equal } from "assert"
|
||||||
import { afterAll, beforeAll, describe, it } from "bun:test"
|
import { afterAll, beforeAll, describe, it } from "bun:test"
|
||||||
import { z } from "zod"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AlbumDetailed,
|
AlbumDetailed,
|
||||||
|
|
@ -9,21 +9,22 @@ import {
|
||||||
ArtistFull,
|
ArtistFull,
|
||||||
PlaylistDetailed,
|
PlaylistDetailed,
|
||||||
PlaylistFull,
|
PlaylistFull,
|
||||||
|
SearchResult,
|
||||||
SongDetailed,
|
SongDetailed,
|
||||||
SongFull,
|
SongFull,
|
||||||
VideoDetailed,
|
VideoDetailed,
|
||||||
VideoFull,
|
VideoFull,
|
||||||
} from "../schemas"
|
} from "../@types/types"
|
||||||
import YTMusic from "../YTMusic"
|
import YTMusic from "../YTMusic"
|
||||||
|
|
||||||
const errors = <z.ZodError<any>[]>[]
|
const errors: Problem[] = []
|
||||||
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
|
const queries = ["Lilac", "Weekend", "Eill", "Eminem", "Lisa Hannigan"]
|
||||||
const expect = (data: any, schema: z.Schema) => {
|
const expect = (data: any, type: Type) => {
|
||||||
const result = schema.safeParse(data)
|
const result = type(data)
|
||||||
if (!result.success && "error" in result) {
|
if (!result.data && "problems" in result) {
|
||||||
errors.push(result.error)
|
errors.push(...result.problems!)
|
||||||
}
|
}
|
||||||
equal(result.success, true)
|
equal(!!result.data, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ytmusic = new YTMusic()
|
const ytmusic = new YTMusic()
|
||||||
|
|
@ -33,45 +34,37 @@ queries.forEach(query => {
|
||||||
describe("Query: " + query, () => {
|
describe("Query: " + query, () => {
|
||||||
it("Search suggestions", async () => {
|
it("Search suggestions", async () => {
|
||||||
const suggestions = await ytmusic.getSearchSuggestions(query)
|
const suggestions = await ytmusic.getSearchSuggestions(query)
|
||||||
expect(suggestions, z.array(z.string()))
|
expect(suggestions, type("string[]"))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Search Songs", async () => {
|
it("Search Songs", async () => {
|
||||||
const songs = await ytmusic.searchSongs(query)
|
const songs = await ytmusic.searchSongs(query)
|
||||||
expect(songs, z.array(SongDetailed))
|
expect(songs, arrayOf(SongDetailed))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Search Videos", async () => {
|
it("Search Videos", async () => {
|
||||||
const videos = await ytmusic.searchVideos(query)
|
const videos = await ytmusic.searchVideos(query)
|
||||||
expect(videos, z.array(VideoDetailed))
|
expect(videos, arrayOf(VideoDetailed))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Search Artists", async () => {
|
it("Search Artists", async () => {
|
||||||
const artists = await ytmusic.searchArtists(query)
|
const artists = await ytmusic.searchArtists(query)
|
||||||
expect(artists, z.array(ArtistDetailed))
|
expect(artists, arrayOf(ArtistDetailed))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Search Albums", async () => {
|
it("Search Albums", async () => {
|
||||||
const albums = await ytmusic.searchAlbums(query)
|
const albums = await ytmusic.searchAlbums(query)
|
||||||
expect(albums, z.array(AlbumDetailed))
|
expect(albums, arrayOf(AlbumDetailed))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Search Playlists", async () => {
|
it("Search Playlists", async () => {
|
||||||
const playlists = await ytmusic.searchPlaylists(query)
|
const playlists = await ytmusic.searchPlaylists(query)
|
||||||
expect(playlists, z.array(PlaylistDetailed))
|
expect(playlists, arrayOf(PlaylistDetailed))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Search All", async () => {
|
it("Search All", async () => {
|
||||||
const results = await ytmusic.search(query)
|
const results = await ytmusic.search(query)
|
||||||
expect(
|
expect(results, arrayOf(SearchResult))
|
||||||
results,
|
|
||||||
z.array(
|
|
||||||
AlbumDetailed.or(ArtistDetailed)
|
|
||||||
.or(PlaylistDetailed)
|
|
||||||
.or(SongDetailed)
|
|
||||||
.or(VideoDetailed),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Get details of the first song result", async () => {
|
it("Get details of the first song result", async () => {
|
||||||
|
|
@ -95,13 +88,13 @@ queries.forEach(query => {
|
||||||
it("Get the songs of the first artist result", async () => {
|
it("Get the songs of the first artist result", async () => {
|
||||||
const artists = await ytmusic.searchArtists(query)
|
const artists = await ytmusic.searchArtists(query)
|
||||||
const songs = await ytmusic.getArtistSongs(artists[0]!.artistId)
|
const songs = await ytmusic.getArtistSongs(artists[0]!.artistId)
|
||||||
expect(songs, z.array(SongDetailed))
|
expect(songs, arrayOf(SongDetailed))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Get the albums of the first artist result", async () => {
|
it("Get the albums of the first artist result", async () => {
|
||||||
const artists = await ytmusic.searchArtists(query)
|
const artists = await ytmusic.searchArtists(query)
|
||||||
const albums = await ytmusic.getArtistAlbums(artists[0]!.artistId)
|
const albums = await ytmusic.getArtistAlbums(artists[0]!.artistId)
|
||||||
expect(albums, z.array(AlbumDetailed))
|
expect(albums, arrayOf(AlbumDetailed))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("Get details of the first album result", async () => {
|
it("Get details of the first album result", async () => {
|
||||||
|
|
@ -119,7 +112,7 @@ queries.forEach(query => {
|
||||||
it("Get the videos of the first playlist result", async () => {
|
it("Get the videos of the first playlist result", async () => {
|
||||||
const playlists = await ytmusic.searchPlaylists(query)
|
const playlists = await ytmusic.searchPlaylists(query)
|
||||||
const videos = await ytmusic.getPlaylistVideos(playlists[0]!.playlistId)
|
const videos = await ytmusic.getPlaylistVideos(playlists[0]!.playlistId)
|
||||||
expect(videos, z.array(VideoDetailed))
|
expect(videos, arrayOf(VideoDetailed))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
import { z } from "zod"
|
import { Type } from "arktype"
|
||||||
import zodtojson from "zod-to-json-schema"
|
|
||||||
|
|
||||||
export default <T extends z.Schema>(data: z.infer<T>, schema: T): z.infer<T> => {
|
export default <T>(data: T, type: Type<T>): T => {
|
||||||
const result = schema.safeParse(data)
|
const result = type(data)
|
||||||
if (result.success) {
|
if (result.data) {
|
||||||
return data
|
return result.data as T
|
||||||
} else {
|
} else {
|
||||||
if ("error" in result) {
|
if ("error" in result) {
|
||||||
console.error(
|
console.error(
|
||||||
"Invalid data schema, please report to https://github.com/zS1L3NT/ts-npm-ytmusic-api/issues/new/choose",
|
"Invalid data type, please report to https://github.com/zS1L3NT/ts-npm-ytmusic-api/issues/new/choose",
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
schema: zodtojson(schema),
|
type: type.definition,
|
||||||
data,
|
data,
|
||||||
error: result.error,
|
error: result.error,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue