diff --git a/packages/types/package.json b/packages/types/package.json index 7cccd11..f5f7a4f 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -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" } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ec152b6..9ffdd71 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,2 +1,3 @@ /** `@app/types` 入口:统一导出 Zod 入参 schema 与跨层共享 DTO。 */ export * from "./schemas.js"; +export * from "./media-schemas.js"; diff --git a/packages/types/src/media-schemas.test.ts b/packages/types/src/media-schemas.test.ts new file mode 100644 index 0000000..8841623 --- /dev/null +++ b/packages/types/src/media-schemas.test.ts @@ -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); + }); +}); diff --git a/packages/types/src/media-schemas.ts b/packages/types/src/media-schemas.ts new file mode 100644 index 0000000..fd6c664 --- /dev/null +++ b/packages/types/src/media-schemas.ts @@ -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; +export type MountListDirInput = z.infer; +export type LibraryListInput = z.infer; +export type PlaybackReportInput = z.infer; +export type ScrapeSearchInput = z.infer; +export type DanmakuFetchOutput = { + ok: boolean; + source: "open-network" | "cache" | "none"; + /** Bilibili/弹弹play 兼容 XML 字符串;无弹幕则为 "" */ + xml: string; + message?: string; +}; diff --git a/yarn.lock b/yarn.lock index a5a9247..48870c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"