use arktype instead of zod

This commit is contained in:
zS1L3NT Mac 2023-12-22 03:23:42 +08:00
parent d4d6fa1662
commit bfdceea12d
No known key found for this signature in database
GPG Key ID: 02BE07CD431E4F42
14 changed files with 203 additions and 218 deletions

BIN
bun.lockb

Binary file not shown.

View File

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

141
src/@types/types.ts Normal file
View File

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

View File

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

View File

@ -15,6 +15,6 @@ export type {
ThumbnailFull, ThumbnailFull,
VideoDetailed, VideoDetailed,
VideoFull, VideoFull,
} from "./schemas" } from "./@types/types"
export default YTMusic export default YTMusic

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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