ghp / packages /shared /src /schemas.ts
QSLY's picture
deploy: build Hugging Face Space from source
00a912e
Raw
History Blame Contribute Delete
8.4 kB
import * as z from 'zod/v4'
import { LIMITS } from './constants.js'
// ---------------------------------------------------------------------------
// Room
// ---------------------------------------------------------------------------
export const roomCreateSchema = z.object({
nickname: z.string().min(1, '昵称不能为空').max(LIMITS.NICKNAME_MAX_LENGTH, '昵称过长'),
roomName: z.string().max(LIMITS.ROOM_NAME_MAX_LENGTH, '房间名过长').optional(),
password: z.string().max(LIMITS.ROOM_PASSWORD_MAX_LENGTH, '密码过长').optional(),
})
export const roomJoinSchema = z.object({
roomId: z.string().min(1, '房间号不能为空'),
nickname: z.string().min(1, '昵称不能为空'),
password: z.string().max(LIMITS.ROOM_PASSWORD_MAX_LENGTH).optional(),
rejoinToken: z.string().min(1).max(500).optional(),
})
export const audioQualitySchema = z.union([
z.literal(128),
z.literal(192),
z.literal(320),
z.literal(999),
z.literal('highest'),
z.literal('netease_dolby'),
z.literal('netease_hires'),
z.literal('netease_jyeffect'),
z.literal('netease_master'),
z.literal('netease_spatial'),
z.literal('tencent_flac'),
z.literal('tencent_master'),
z.literal('kugou_hires'),
z.literal('kugou_master'),
z.literal('bilibili_64'),
z.literal('bilibili_132'),
z.literal('bilibili_192'),
z.literal('bilibili_hires'),
])
export const roomSettingsSchema = z.object({
name: z.string().min(1).max(LIMITS.ROOM_NAME_MAX_LENGTH).optional(),
password: z.string().max(LIMITS.ROOM_PASSWORD_MAX_LENGTH).nullable().optional(),
audioQuality: audioQualitySchema.optional(),
hidden: z.boolean().optional(),
permanent: z.boolean().optional(),
allowTemporaryAdminTrackRemoval: z.boolean().optional(),
allowTemporaryAdminQueueClear: z.boolean().optional(),
})
export const setRoleSchema = z.object({
userId: z.string().min(1),
role: z.enum(['admin', 'member']),
})
// ---------------------------------------------------------------------------
// Room — auto fallback notifications
// ---------------------------------------------------------------------------
export const roomAutoFallbackSchema = z.object({
attemptId: z.string().min(1).max(100),
status: z.enum(['trying', 'success', 'failed']),
fromSource: z.enum(['netease', 'tencent']),
toSource: z.enum(['netease', 'tencent']),
trackTitle: z.string().min(1).max(500),
reasonType: z.enum(['VIP_REQUIRED', 'COPYRIGHT_RESTRICTED', 'NO_RESOURCE', 'TIMEOUT', 'UNKNOWN']).optional(),
reasonDetail: z.string().max(200).optional(),
})
// ---------------------------------------------------------------------------
// Player
// ---------------------------------------------------------------------------
export const playerSeekSchema = z.object({
currentTime: z.number().finite().nonnegative(),
})
export const playerSyncSchema = z.object({
currentTime: z.number().finite().nonnegative(),
hostServerTime: z.number().finite().positive().optional(),
})
export const playerSetModeSchema = z.object({
mode: z.enum(['sequential', 'loop-all', 'loop-one', 'shuffle']),
})
// ---------------------------------------------------------------------------
// Queue
// ---------------------------------------------------------------------------
const trackSchema = z.object({
id: z.string().max(200),
title: z.string().max(500),
artist: z.array(z.string().max(200)).max(20),
album: z.string().max(500),
duration: z.number().finite().nonnegative(),
cover: z.string().max(2000),
bilibiliCover: z.string().max(2000).optional(),
source: z.enum(['netease', 'tencent', 'kugou', 'kugou_concept', 'bilibili']),
sourceId: z.string().max(200),
urlId: z.string().max(200),
lyricId: z.string().max(200).optional(),
picId: z.string().max(200).optional(),
metadataSource: z.enum(['netease', 'tencent', 'kugou', 'kugou_concept']).optional(),
streamUrl: z.string().max(2000).optional(),
requiresServerProxy: z.boolean().optional(),
streamFormat: z.enum(['m4a', 'flac']).optional(),
vip: z.boolean().optional(),
})
export const queueAddSchema = z.object({
track: trackSchema,
})
export const queueInsertAfterCurrentSchema = queueAddSchema
export const queueAddBatchSchema = z.object({
tracks: z.array(trackSchema).min(1).max(LIMITS.QUEUE_BATCH_MAX_SIZE),
playlistName: z.string().max(200).optional(),
})
export const queueRemoveSchema = z.object({ trackId: z.string().max(200) })
export const queueReorderSchema = z.object({
trackIds: z.array(z.string().max(200)).max(LIMITS.QUEUE_MAX_SIZE),
})
export const queueUpdateMetadataSchema = z.object({
trackId: z.string().max(200),
metadataSource: z.enum(['netease', 'tencent', 'kugou', 'kugou_concept']).optional(),
lyricId: z.string().max(200).optional(),
picId: z.string().max(200).optional(),
cover: z.string().max(2000).optional(),
clearMetadata: z.boolean().optional(),
})
// ---------------------------------------------------------------------------
// Chat
// ---------------------------------------------------------------------------
export const chatMessageSchema = z.object({
content: z.string().min(1).max(LIMITS.CHAT_CONTENT_MAX_LENGTH),
})
// ---------------------------------------------------------------------------
// REST API – Music routes
// ---------------------------------------------------------------------------
const musicSourceSchema = z.enum(['netease', 'tencent', 'kugou', 'kugou_concept', 'bilibili'])
const searchQueryCommonShape = {
limit: z.coerce.number().int().min(1).max(LIMITS.SEARCH_PAGE_SIZE_MAX).default(20),
page: z.coerce.number().int().min(1).max(LIMITS.SEARCH_PAGE_MAX).default(1),
type: z.enum(['song', 'album', 'playlist']).optional().default('song'),
roomId: z.string().min(1).max(10).optional(),
}
export const searchQuerySchema = z.discriminatedUnion('source', [
z.object({
source: z.literal('bilibili'),
keyword: z.string().min(1).max(LIMITS.BILIBILI_SEARCH_INPUT_MAX_LENGTH),
...searchQueryCommonShape,
}),
z.object({
source: z.enum(['netease', 'tencent', 'kugou', 'kugou_concept']),
keyword: z.string().min(1).max(LIMITS.SEARCH_KEYWORD_MAX_LENGTH),
...searchQueryCommonShape,
}),
])
export const recommendationsQuerySchema = z.object({
roomId: z.string().min(1).max(10),
limit: z.coerce.number().int().min(1).max(50).default(20),
radarPage: z.coerce.number().int().min(1).max(10_000).default(1),
playlistOffset: z.coerce.number().int().min(0).max(1_000_000).default(0),
})
export const urlQuerySchema = z.object({
source: musicSourceSchema,
urlId: z.string().min(1),
bitrate: z
.preprocess((value) => {
if (typeof value !== 'string' || value.trim() === '') return value
const numeric = Number(value)
return Number.isNaN(numeric) ? value : numeric
}, audioQualitySchema)
.default(320),
})
export const downloadOptionsQuerySchema = z.object({
roomId: z.string().min(1).max(10),
trackId: z.string().min(1).max(200),
})
export const downloadQuerySchema = downloadOptionsQuerySchema.extend({
quality: z.preprocess((value) => {
if (typeof value !== 'string' || value.trim() === '') return value
const numeric = Number(value)
return Number.isNaN(numeric) ? value : numeric
}, audioQualitySchema),
})
export const lyricQuerySchema = z.object({
source: musicSourceSchema,
lyricId: z.string().min(1),
})
export const coverQuerySchema = z.object({
source: musicSourceSchema,
picId: z.string().min(1),
size: z.coerce.number().int().positive().default(300),
})
export const playlistQuerySchema = z.object({
source: musicSourceSchema,
id: z.string().min(1).max(LIMITS.PLAYLIST_ID_MAX_LENGTH),
limit: z.coerce.number().int().min(1).max(1000).default(100),
offset: z.coerce.number().int().min(0).default(0),
total: z.coerce.number().int().min(0).optional(),
roomId: z.string().min(1).max(10).optional(),
type: z.enum(['playlist', 'album']).optional().default('playlist'),
})
// ---------------------------------------------------------------------------
// Voting
// ---------------------------------------------------------------------------
export const voteStartSchema = z.object({
action: z.enum(['pause', 'resume', 'next', 'prev', 'set-mode', 'play-track', 'remove-track']),
payload: z.record(z.string(), z.unknown()).optional(),
})
export const voteCastSchema = z.object({
approve: z.boolean(),
})