feat(types): media mount/library/playback/scrape/danmaku schemas

This commit is contained in:
noelorin 2026-09-25 02:35:52 +08:00
parent 3db8a61207
commit c0779dffdf
5 changed files with 303 additions and 5 deletions

View File

@ -14,12 +14,14 @@
"scripts": {
"build": "tsc --project tsconfig.build.json",
"typecheck": "tsc --noEmit",
"lint": "biome check ."
"lint": "biome check .",
"test": "vitest run"
},
"dependencies": {
"zod": "^4.6.5"
},
"devDependencies": {
"typescript": "^7.0.2"
"typescript": "^7.0.2",
"vitest": "^5.0.1"
}
}

View File

@ -1,2 +1,3 @@
/** `@app/types` 入口:统一导出 Zod 入参 schema 与跨层共享 DTO。 */
export * from "./schemas.js";
export * from "./media-schemas.js";

View File

@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { mountSchemas, playbackSchemas, scrapeSchemas } from "./media-schemas.js";
describe("mountSchemas.create", () => {
it("拒绝缺 baseUrl", () => {
const r = mountSchemas.create.safeParse({ name: "A", type: "webdav" });
expect(r.success).toBe(false);
});
it("接受合法 WebDAV", () => {
const r = mountSchemas.create.safeParse({
name: "OpenList",
type: "webdav",
baseUrl: "https://dav.example.com/dav",
username: "u",
password: "p",
rootPath: "/anime",
});
expect(r.success).toBe(true);
});
});
describe("scrapeSchemas.bind", () => {
it("需要 mediaItemId 与 bangumiId", () => {
const r = scrapeSchemas.bind.safeParse({ mediaItemId: "m1", bangumiId: 123 });
expect(r.success).toBe(true);
expect(scrapeSchemas.bind.safeParse({ mediaItemId: "m1" }).success).toBe(false);
});
});
describe("playbackSchemas.report", () => {
it("positionMs ≥ 0", () => {
expect(
playbackSchemas.report.safeParse({
mediaItemId: "x",
positionMs: -1,
durationMs: 1000,
}).success,
).toBe(false);
});
});

View File

@ -0,0 +1,96 @@
import { z } from "zod";
const id = z.string().min(1);
export const mountSchemas = {
create: z.object({
name: z.string().min(1, "名称不能为空").max(100),
type: z.literal("webdav"),
baseUrl: z.string().url("需要合法 URL"),
username: z.string().max(200).optional(),
password: z.string().max(500).optional(),
rootPath: z.string().max(1000).default("/"),
enabled: z.boolean().default(true),
}),
update: z.object({
id,
name: z.string().min(1).max(100).optional(),
baseUrl: z.string().url().optional(),
username: z.string().max(200).optional(),
password: z.string().max(500).optional(),
rootPath: z.string().max(1000).optional(),
enabled: z.boolean().optional(),
}),
delete: z.object({ id }),
list: z.object({}).optional(),
test: z.object({
baseUrl: z.string().url(),
username: z.string().max(200).optional(),
password: z.string().max(500).optional(),
rootPath: z.string().max(1000).default("/"),
}),
listDir: z.object({
mountId: id,
path: z.string().max(2000).default("/"),
}),
};
export const librarySchemas = {
list: z.object({
limit: z.number().min(1).max(100).default(24),
offset: z.number().min(0).default(0),
scrapeStatus: z.enum(["ok", "unmatched", "failed", "pending"]).optional(),
q: z.string().max(200).optional(),
}),
scan: z.object({ mountId: id }),
get: z.object({ id }),
};
export const playbackSchemas = {
report: z.object({
mediaItemId: id,
positionMs: z.number().min(0),
durationMs: z.number().min(0),
}),
get: z.object({ mediaItemId: id }),
};
export const scrapeSchemas = {
search: z.object({
q: z.string().min(1, "请输入搜索词").max(200),
}),
bind: z.object({
mediaItemId: id,
bangumiId: z.number().int().positive(),
}),
rescrape: z.object({ mediaItemId: id }),
};
export const danmakuSchemas = {
/** 开放网络:按媒体文件取弹幕 */
fetch: z.object({ mediaItemId: id }),
/** 本地 XML:仅登记导入元数据(正文在客户端喂给播放器) */
importMeta: z.object({
mediaItemId: id,
byteSize: z.number().min(0).max(20_000_000),
}),
settings: z.object({
enabled: z.boolean().default(true),
opacity: z.number().min(0).max(1).default(0.8),
density: z.number().min(0.1).max(3).default(1),
}),
getSettings: z.object({}).optional(),
};
export type MountCreateInput = z.infer<typeof mountSchemas.create>;
export type MountListDirInput = z.infer<typeof mountSchemas.listDir>;
export type LibraryListInput = z.infer<typeof librarySchemas.list>;
export type PlaybackReportInput = z.infer<typeof playbackSchemas.report>;
export type ScrapeSearchInput = z.infer<typeof scrapeSchemas.search>;
export type DanmakuFetchOutput = {
ok: boolean;
source: "open-network" | "cache" | "none";
/** Bilibili/弹弹play 兼容 XML 字符串;无弹幕则为 "" */
xml: string;
message?: string;
};

165
yarn.lock
View File

@ -114,6 +114,7 @@ __metadata:
resolution: "@app/types@workspace:packages/types"
dependencies:
typescript: "npm:^7.0.2"
vitest: "npm:^5.0.1"
zod: "npm:^4.6.5"
languageName: unknown
linkType: soft
@ -2231,7 +2232,7 @@ __metadata:
languageName: node
linkType: hard
"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28":
"@jridgewell/trace-mapping@npm:0.3.31, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28":
version: 0.3.31
resolution: "@jridgewell/trace-mapping@npm:0.3.31"
dependencies:
@ -4184,6 +4185,26 @@ __metadata:
languageName: node
linkType: hard
"@vitest/mocker@npm:5.0.1":
version: 5.0.1
resolution: "@vitest/mocker@npm:5.0.1"
dependencies:
"@jridgewell/trace-mapping": "npm:0.3.31"
"@vitest/spy": "npm:5.0.1"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^1.2.3"
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
checksum: 10c0/21f0456c7452d7f65449fc0f43f1ccbf222d353eb11fcf511b78b5b8c4ae38ac9ca286b56936d91856c483575cd86688f1b6dc00f02ffd85ed1108b12cf219cd
languageName: node
linkType: hard
"@vitest/pretty-format@npm:3.2.4":
version: 3.2.4
resolution: "@vitest/pretty-format@npm:3.2.4"
@ -4202,6 +4223,13 @@ __metadata:
languageName: node
linkType: hard
"@vitest/spy@npm:5.0.1":
version: 5.0.1
resolution: "@vitest/spy@npm:5.0.1"
checksum: 10c0/88838f6dda5237f0a40c365598ce7112922611e9157b390ca90c11760ae8697c37214f621263bfbe12e2bd27234df577cbbecc93de003ced366d23be24383c37
languageName: node
linkType: hard
"@vitest/utils@npm:3.2.4":
version: 3.2.4
resolution: "@vitest/utils@npm:3.2.4"
@ -4668,6 +4696,13 @@ __metadata:
languageName: node
linkType: hard
"chai@npm:^6.2.2":
version: 6.2.2
resolution: "chai@npm:6.2.2"
checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53
languageName: node
linkType: hard
"character-entities-html4@npm:^2.0.0":
version: 2.1.0
resolution: "character-entities-html4@npm:2.1.0"
@ -5350,7 +5385,7 @@ __metadata:
languageName: node
linkType: hard
"es-module-lexer@npm:^2.0.0":
"es-module-lexer@npm:^2.0.0, es-module-lexer@npm:^2.3.2":
version: 2.3.2
resolution: "es-module-lexer@npm:2.3.2"
checksum: 10c0/5e7389424c43478439f12f9a6aca1750f6f99afa384fc3de329f4a45f152ab156671055008adaafc74960877abf8cc338aeebf6bed8c21297b146fd6eb7a22f8
@ -5778,6 +5813,13 @@ __metadata:
languageName: node
linkType: hard
"expect-type@npm:^1.4.0":
version: 1.4.0
resolution: "expect-type@npm:1.4.0"
checksum: 10c0/d40d76b8570695d36587beb3cc28494da2ca3ec8f04e67f5622ed2d372d850e401a9adef19c6835e1a8173903f157c79540b34c7b3fbd7cd8ce726cc903c57b7
languageName: node
linkType: hard
"exponential-backoff@npm:^3.1.1":
version: 3.1.3
resolution: "exponential-backoff@npm:3.1.3"
@ -6849,6 +6891,15 @@ __metadata:
languageName: node
linkType: hard
"magic-string@npm:^1.2.3":
version: 1.4.2
resolution: "magic-string@npm:1.4.2"
dependencies:
"@jridgewell/sourcemap-codec": "npm:^1.6.0"
checksum: 10c0/d3e222c202f99ec4861e7316f473f9b1cfe335ebdf38d4f67335f842f3fe248f01ee00fd3e1a8206bcbe9855e0c41515d7c210300d8fcf6eefbb982d79e423c6
languageName: node
linkType: hard
"magicast@npm:^0.5.2":
version: 0.5.5
resolution: "magicast@npm:0.5.5"
@ -7188,7 +7239,7 @@ __metadata:
languageName: node
linkType: hard
"obug@npm:^2.1.1":
"obug@npm:^2.1.1, obug@npm:^2.1.4":
version: 2.2.1
resolution: "obug@npm:2.2.1"
checksum: 10c0/8a7c001847d880fa6ba83c7bbee6a80fd271ee628439cd508c8647d53b217abb1759e2d5e4aaeb09a35d0a274bdb225a0a5a78eded3213d63e656153cf411b63
@ -8128,6 +8179,13 @@ __metadata:
languageName: node
linkType: hard
"siginfo@npm:^2.0.0":
version: 2.0.0
resolution: "siginfo@npm:2.0.0"
checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34
languageName: node
linkType: hard
"signal-exit@npm:^4.0.1":
version: 4.1.0
resolution: "signal-exit@npm:4.1.0"
@ -8208,6 +8266,13 @@ __metadata:
languageName: node
linkType: hard
"stackback@npm:0.0.2":
version: 0.0.2
resolution: "stackback@npm:0.0.2"
checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983
languageName: node
linkType: hard
"statuses@npm:^2.0.2, statuses@npm:~2.0.2":
version: 2.0.2
resolution: "statuses@npm:2.0.2"
@ -8215,6 +8280,13 @@ __metadata:
languageName: node
linkType: hard
"std-env@npm:^4.2.0":
version: 4.2.0
resolution: "std-env@npm:4.2.0"
checksum: 10c0/40ac525ce7b7c556abc332a7376f14356eeb1a7f17f6ff9a003eb9f52326ff1f3745d3e1b43452675b1ec6fcc319f1b1d6f3b0d386cf3f91058479ad883cff69
languageName: node
linkType: hard
"storybook@npm:^9.1.10":
version: 9.1.20
resolution: "storybook@npm:9.1.20"
@ -8408,6 +8480,13 @@ __metadata:
languageName: node
linkType: hard
"tinybench@npm:6.1.4":
version: 6.1.4
resolution: "tinybench@npm:6.1.4"
checksum: 10c0/7a815318a02270a98247298ddd289b75c841725606c977f0b74749bc5ec811a44998267ccb60bd3deb23b551dbfaf4d979eb08e9b04fbc87ff571f9b39cc61c3
languageName: node
linkType: hard
"tinyclip@npm:^0.1.12":
version: 0.1.15
resolution: "tinyclip@npm:0.1.15"
@ -8415,6 +8494,13 @@ __metadata:
languageName: node
linkType: hard
"tinyexec@npm:1.3.0":
version: 1.3.0
resolution: "tinyexec@npm:1.3.0"
checksum: 10c0/e9b89f97489d2aab2cef408da279e6b32547e738d1275032ccb8fd0028a006d93eb70fc51c6cffd9fc2f5aca6c2a273d8b6f73b52d46ee5116da6b94969ef958
languageName: node
linkType: hard
"tinyexec@npm:^1.0.0, tinyexec@npm:^1.0.4":
version: 1.3.1
resolution: "tinyexec@npm:1.3.1"
@ -9218,6 +9304,67 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:^5.0.1":
version: 5.0.1
resolution: "vitest@npm:5.0.1"
dependencies:
"@types/chai": "npm:^5.2.2"
"@vitest/mocker": "npm:5.0.1"
chai: "npm:^6.2.2"
es-module-lexer: "npm:^2.3.2"
expect-type: "npm:^1.4.0"
magic-string: "npm:^1.2.3"
obug: "npm:^2.1.4"
picomatch: "npm:^4.0.7"
std-env: "npm:^4.2.0"
tinybench: "npm:6.1.4"
tinyexec: "npm:1.3.0"
tinyglobby: "npm:^0.2.17"
why-is-node-running: "npm:^2.3.0"
peerDependencies:
"@edge-runtime/vm": "*"
"@opentelemetry/api": ^1.9.0
"@types/node": ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 5.0.1
"@vitest/browser-preview": 5.0.1
"@vitest/browser-webdriverio": ^5.0.0-beta.5 || >=5.0.0
"@vitest/coverage-istanbul": 5.0.1
"@vitest/coverage-v8": 5.0.1
"@vitest/ui": 5.0.1
happy-dom: "*"
jsdom: "*"
vite: ^6.4.0 || ^7.0.0 || ^8.0.0
peerDependenciesMeta:
"@edge-runtime/vm":
optional: true
"@opentelemetry/api":
optional: true
"@types/node":
optional: true
"@vitest/browser-playwright":
optional: true
"@vitest/browser-preview":
optional: true
"@vitest/browser-webdriverio":
optional: true
"@vitest/coverage-istanbul":
optional: true
"@vitest/coverage-v8":
optional: true
"@vitest/ui":
optional: true
happy-dom:
optional: true
jsdom:
optional: true
vite:
optional: false
bin:
vitest: ./vitest.mjs
checksum: 10c0/1b49fe8eb7274ffd43ca5a29cccbcc942cf991488020a74114f3a725a1168c549d47820715b8ed3f521a0cd9ccc0013ab9511cc6e0f8e0a5bdfb751e3ddc4d2d
languageName: node
linkType: hard
"volar-service-css@npm:0.0.71":
version: 0.0.71
resolution: "volar-service-css@npm:0.0.71"
@ -9496,6 +9643,18 @@ __metadata:
languageName: node
linkType: hard
"why-is-node-running@npm:^2.3.0":
version: 2.3.0
resolution: "why-is-node-running@npm:2.3.0"
dependencies:
siginfo: "npm:^2.0.0"
stackback: "npm:0.0.2"
bin:
why-is-node-running: cli.js
checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054
languageName: node
linkType: hard
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version: 7.0.0
resolution: "wrap-ansi@npm:7.0.0"