183 lines
8.4 KiB
Markdown
183 lines
8.4 KiB
Markdown
# 对齐弹弹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 · xml · byteSize · createdAt`
|
||
唯一 `(userId, mediaItemId, source)`(同源仅留最新)。`userId` 级隔离。
|
||
说明:`withRelated` 关联弹幕在拉取时**并入** open-network 正文,不单独落 `related` 源。
|
||
|
||
### `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`)→ 拉弹幕
|
||
**高置信定义**:`matches` 仅 1 条,或最高分领先第二名 ≥ 阈值(默认 0.15,常量可调);否则进手选
|
||
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 |
|
||
| 验收口径 | 真实链路可跑通 |
|
||
| 范围 | 协议/数据兼容 + 核心四块全选 |
|