From d0f310d8fc8568b3378be70c7b1e76b125e7ed1e Mon Sep 17 00:00:00 2001 From: noelorin Date: Sat, 26 Sep 2026 17:54:13 +0800 Subject: [PATCH] docs(specs): dandanplay capability alignment (core chain + open API) --- ...-dandanplay-capability-alignment-design.md | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-26-dandanplay-capability-alignment-design.md diff --git a/docs/superpowers/specs/2026-09-26-dandanplay-capability-alignment-design.md b/docs/superpowers/specs/2026-09-26-dandanplay-capability-alignment-design.md new file mode 100644 index 0000000..e4874ce --- /dev/null +++ b/docs/superpowers/specs/2026-09-26-dandanplay-capability-alignment-design.md @@ -0,0 +1,180 @@ +# 对齐弹弹play能力(核心链路 + 协议/数据格式)— 设计规格 + +- **日期**:2026-09-26 +- **状态**:设计已获用户批准,待规格审查 → writing-plans +- **仓库**:本产品仓(业务进入现有分层 + 新增 `@app/danmaku`) +- **前置**:`2026-09-25-dandanplay-web-design.md`(Web 版弹弹play 主体)已实现主链路 + +## 1. 目标与范围 + +在已有「挂载 → 找片 → 播放 → 弹幕 → 进度」之上,把弹幕能力与弹弹play **开放弹幕网络**在体验与协议上对齐。 + +**In scope** + +1. **协议/数据兼容** + - 开放 API:`/api/v2/match`、`/api/v2/comment/{episodeId}`(含 `withRelated`)、AppId 签名 + - 数据格式:文件识别 hash(前 16MB MD5)、B 站/弹弹play 兼容弹幕 XML、匹配元数据 +2. **核心链路深对齐** + - 候选集匹配与手选(不盲取第一条) + - 弹幕持久化与同目录自动关联(修 P1-3) + - 关联弹幕与渲染体验(`withRelated`、prefs 服务端同步) + - 弹幕过滤与屏蔽(关键词、类型) + +**Out of scope(YAGNI)** + +- 远程访问协议(弹弹play 连帆幕) +- protobuf 弹幕、多弹幕站聚合 +- BT/RSS、AI 字幕、投屏、Emby/Jellyfin 直连(见产品文档「还没有」) + +**验收口径**:真实链路可跑通(文件 → 匹配候选 → 手选 → 拉弹幕 → 过滤 → 持久化,刷新/多设备可恢复)。无凭证时降级路径也要可用。 + +**实现路径(用户选定)**:**方案 B — 独立 `@app/danmaku` 协议包**(非垂直增强、非先契约后功能)。 + +## 2. 架构与包边界 + +新增纯协议包 `@app/danmaku`:无 DB、无 tRPC;HTTP 仅用全局 `fetch`。 + +| 模块 | 职责 | +|------|------| +| `signature` | `base64(sha256(AppId + Timestamp + Path + AppSecret))` | +| `hash` | 文件头 16MB MD5(流式,禁止整文件缓冲) | +| `match` | `POST /api/v2/match` 入参/出参、`hashAndFileName` / `fileNameOnly`、候选排序纯函数 | +| `comment` | `GET /api/v2/comment/{episodeId}`、`withRelated`、comments 结构化 | +| `xml` | B 站/弹弹play 兼容 XML 解析与序列化(`p` 字段双向) | +| `types` | `EpisodeId`、`MatchCandidate`、`DanmakuComment` 等纯类型 | +| `filter` | `filterComments`(关键词、类型、防重叠可选) | + +依赖方向(只新增向下边,禁止循环): + +``` +web ──► trpc ──► dao ──► db ──► models + │ │ + │ ├──► types + │ └──► danmaku(协议包) + └──► danmaku(XML 解析复用) +``` + +- **trpc** `danmaku.service`:鉴权、落库、缓存、绑定编排 + 调 `@app/danmaku` +- **web**:`danmaku-render.ts` 格式解析改为引用 `@app/danmaku` 的 `xml`(单一解析实现) +- **包位置**:`packages/danmaku`,构建方式对齐 `@app/types`(vite + tsc dts) + +## 3. 数据模型 + +Schema 源仍为 `@app/models`;查询进 `@app/dao`。 + +### `media_items` 增列 + +| 字段 | 说明 | +|------|------| +| `dandanEpisodeId` | 开放网络 episodeId;空 = 未绑定 | +| `danmakuMatchSource` | `auto` \| `manual` \| `none` | +| `danmakuMatchedAt` | 上次匹配时间 | + +### `danmaku_prefs`(用户级,新表) + +`userId · enabled · opacity · density · blockKeywords(JSON) · blockTypes(JSON) · updatedAt` +唯一 `(userId)`。blockTypes:`scroll` / `top` / `bottom`。 + +### `danmaku_docs`(正文持久化,新表,修 P1-3) + +`id · userId · mediaItemId · source: open-network|local-xml|related · xml · byteSize · createdAt` +唯一 `(userId, mediaItemId, source)`(同源仅留最新)。`userId` 级隔离。 + +### `danmaku_cache`(保留,全局共享) + +未绑定时 `matchKey = sha256(path:size)`;**绑定后**缓存键改为 `episodeId`(集身份比文件身份稳定)。 + +### Zod(`@app/types`) + +- `danmakuMatchSelectSchema`(mediaItemId + episodeId) +- `danmakuImportSchema`(替代仅 `byteSize` 的 `importMeta`,可含 fileName) +- `danmakuPrefsSchema`(含 blockKeywords / blockTypes) +- 输出:`MatchCandidate[]`、`DanmakuFetchOutput`(source 扩展 `local-xml` 等) + +## 4. 协议/数据格式对齐 + +| 对齐点 | 规范 | +|--------|------| +| 文件识别 | 前 16MB MD5 + 文件名;有 hash 用 `hashAndFileName`,否则 `fileNameOnly` | +| 匹配 | `POST /api/v2/match` → `matches[]`;按置信/用户选择写入 `dandanEpisodeId` | +| 弹幕 | `GET /api/v2/comment/{id}?withRelated=true` | +| 鉴权 | `X-AppId` / `X-Timestamp` / `X-Signature`(现有算法保留 + 单测向量) | +| XML | `text`;round-trip 测试 | +| 降级 | 无凭证/失败 → 本地 XML 仍可用 | + +环境变量沿用:`OPEN_DANMAKU_APP_ID` / `OPEN_DANMAKU_APP_SECRET` / `OPEN_DANMAKU_API_BASE`。 + +## 5. 页面与交互 + +路由不变;只增强 `/watch`、`/danmaku` 等组件。 + +### 候选集匹配与手选 + +1. 打开 `/watch`:无 `dandanEpisodeId` → 自动 match → 高置信绑定(`source=auto`)→ 拉弹幕 +2. 未匹配/低置信 → 「重新匹配」→ 候选列表(标题、集数、类型、置信)→ 点选绑定(`source=manual`)→ 立即拉弹幕 +3. 已绑定可「更换匹配」;弹幕集绑定与 Bangumi 绑定**相互独立** + +### 弹幕持久化与自动关联 + +- 导入 XML → 服务端 `danmaku_docs(source=local-xml)`(≤20MB)→ 刷新/换端可恢复 +- 播放优先级:**本地已导入 > 开放网络** +- 浏览/扫描发现同目录 `同名.xml` → 登记 `local-xml`(正文按需从 WebDAV 拉取)→ 播放页提示「发现同名弹幕」 + +### 关联弹幕与渲染体验 + +- `withRelated` 并入;过滤面板可显示本集/关联计数 +- 继续用 artplayer-plugin-danmuku;`danmaku_prefs` 服务端同步(开关/透明度/密度),localStorage 仅作无网络回退 +- 密度 `thinDanmaku` 仍在客户端 + +### 弹幕过滤与屏蔽 + +- 播放页过滤面板:关键词、类型(滚动/顶部/底部)、防重叠(插件已有) +- 设置页:默认值 + 屏蔽词编辑 → `danmaku_prefs` +- 过滤在 XML → comments 之后、喂播放器之前执行(`filterComments` 纯函数) + +## 6. 错误处理 + +| 场景 | 策略 | +|------|------| +| 无开放网络凭证 | 明确提示「未配置弹幕网络」;本地 XML 全功能可用 | +| match 401/签名失败 | Toast 鉴权失败;不重试风暴 | +| match 无候选/低置信 | 进手选候选面板,不静默 | +| comment 失败 | 保留绑定;可重试;本地 XML 优先 | +| XML 解析失败 | 导入报错拒绝;自动关联静默跳过 | +| prefs 失败 | 回退 localStorage 会话级;不阻断播放 | +| 权限 | docs / prefs / 绑定均强制 `userId` 隔离 | + +## 7. 测试与验证 + +**`@app/danmaku` 单测**:签名向量、16MB MD5、XML round-trip、`filterComments`、候选排序;match/comment 用 fixture 锁字段名。 + +**service 测**:绑定持久化、导入落库、prefs upsert、缓存键(path:size → episodeId)切换。 + +**真实链路清单(验收)** + +1. 配置凭证 → 未绑定视频自动匹配 → 弹幕上屏 +2. 低置信 → 手选 → 绑定刷新仍有效 +3. 导入 XML → 刷新/换端登录仍在 +4. 同目录同名 XML 自动关联 +5. 关联弹幕计入;屏蔽词/类型即时生效且持久 +6. 断网/无凭证降级:可播、本地 XML 可用 + +**命令**:`yarn typecheck` · `yarn lint` · `yarn test` · `yarn build:web` + +## 8. 风险与开放问题 + +1. **凭证**:当前 `.env` 的 `OPEN_DANMAKU_APP_ID` 为空。真实链路需提供 AppId/AppSecret,或先用 `OPEN_DANMAKU_API_BASE` 指向 mock 跑通再切官方。 +2. **开放 API 字段**:实现时以 `doc.dandanplay.com/open` 实测为准;fixture 与适配器单点收口在 `@app/danmaku`。 +3. **同目录 XML 自动关联**:避免扫描时全量拉 WebDAV 正文;按需拉取 + 缓存。 +4. **协议包勿膨胀**:若未来要多端复用再抽;本规格不做远程访问协议。 + +## 9. 批准记录 + +| 节 | 结果 | +|----|------| +| §1 架构与包边界 | OK(方案 B) | +| §2 数据模型 + 协议/数据格式 | OK | +| §3 页面与交互(四块) | OK | +| §4 错误处理 + 测试验证 | OK | +| 验收口径 | 真实链路可跑通 | +| 范围 | 协议/数据兼容 + 核心四块全选 |