From aab636eac7c56bb78db3564a0d5e89b413e40182 Mon Sep 17 00:00:00 2001 From: noelorin Date: Sat, 26 Sep 2026 19:40:43 +0800 Subject: [PATCH] feat(types): danmaku match candidates, import payload, prefs filters --- packages/types/src/media-schemas.test.ts | 75 +++++++++++++++++++++++- packages/types/src/media-schemas.ts | 31 +++++++--- 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/packages/types/src/media-schemas.test.ts b/packages/types/src/media-schemas.test.ts index 8841623..b721ced 100644 --- a/packages/types/src/media-schemas.test.ts +++ b/packages/types/src/media-schemas.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { mountSchemas, playbackSchemas, scrapeSchemas } from "./media-schemas.js"; +import { danmakuSchemas, mountSchemas, playbackSchemas, scrapeSchemas } from "./media-schemas.js"; describe("mountSchemas.create", () => { it("拒绝缺 baseUrl", () => { @@ -38,3 +38,76 @@ describe("playbackSchemas.report", () => { ).toBe(false); }); }); + +describe("danmakuSchemas.settings", () => { + it("缺省时填充默认值", () => { + const r = danmakuSchemas.settings.safeParse({}); + expect(r.success).toBe(true); + if (!r.success) return; + expect(r.data).toEqual({ + enabled: true, + opacity: 0.8, + density: 1, + blockKeywords: [], + blockTypes: [], + }); + }); + it("blockTypes 只接受 scroll|top|bottom", () => { + expect( + danmakuSchemas.settings.safeParse({ blockTypes: ["scroll", "top", "bottom"] }) + .success, + ).toBe(true); + expect(danmakuSchemas.settings.safeParse({ blockTypes: ["reverse"] }).success).toBe( + false, + ); + }); + it("opacity 越界拒绝", () => { + expect(danmakuSchemas.settings.safeParse({ opacity: 1.5 }).success).toBe(false); + }); +}); + +describe("danmakuSchemas.import", () => { + const validXml = `hi`; + + it("接受含 { + const r = danmakuSchemas.import.safeParse({ mediaItemId: "m1", xml: validXml }); + expect(r.success).toBe(true); + }); + it("拒绝空 xml", () => { + expect(danmakuSchemas.import.safeParse({ mediaItemId: "m1", xml: "" }).success).toBe( + false, + ); + }); + it("拒绝不含 { + expect( + danmakuSchemas.import.safeParse({ mediaItemId: "m1", xml: "no" }) + .success, + ).toBe(false); + }); + it("拒绝超过 20MB 的 xml", () => { + const huge = ` { + expect( + danmakuSchemas.import.safeParse({ + mediaItemId: "m1", + xml: validXml, + fileName: "a".repeat(256), + }).success, + ).toBe(false); + }); +}); + +describe("danmakuSchemas.selectMatch", () => { + it("要求正整数 episodeId", () => { + expect( + danmakuSchemas.selectMatch.safeParse({ mediaItemId: "m1", episodeId: 42 }).success, + ).toBe(true); + expect( + danmakuSchemas.selectMatch.safeParse({ mediaItemId: "m1", episodeId: 0 }).success, + ).toBe(false); + }); +}); diff --git a/packages/types/src/media-schemas.ts b/packages/types/src/media-schemas.ts index 9b650f9..e4d8451 100644 --- a/packages/types/src/media-schemas.ts +++ b/packages/types/src/media-schemas.ts @@ -148,17 +148,29 @@ export const scrapeSchemas = { export const danmakuSchemas = { /** 开放网络:按媒体文件取弹幕 */ fetch: z.object({ mediaItemId: id }), - /** 手动弹弹play 文件识别(match),命中后写刮削元数据 + 弹幕身份 */ + /** 手动识别:返回候选列表,不自动绑 */ match: z.object({ mediaItemId: id }), - /** 本地 XML:仅登记导入元数据(正文在客户端喂给播放器) */ - importMeta: z.object({ + /** 手选绑定 episodeId */ + selectMatch: z.object({ mediaItemId: id, - byteSize: z.number().min(0).max(20_000_000), + episodeId: z.number().int().positive(), + }), + /** 导入本地 XML 正文(服务端持久化);粗检含弹幕 v.includes("; /** 服务端是否已配置开放弹幕网络凭据(未配置时弹幕获取恒为空) */ openNetworkConfigured: boolean; };