app-template/docs/superpowers/specs/2026-09-26-dandanplay-capab...

181 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 对齐弹弹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 | `<d p="time,mode,size,color,timestamp,pool,uid,rowid">text</d>`;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 |
| 验收口径 | 真实链路可跑通 |
| 范围 | 协议/数据兼容 + 核心四块全选 |