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

6.6 KiB

弹弹play 弹幕获取 + 刮削能力打通 — 设计规格

  • 日期:2026-09-26
  • 状态:设计已定(用户未在线,按最佳判断推进,详见批准记录)
  • 上游规格:2026-09-25-dandanplay-web-design.md(开放弹幕网络中继 + Bangumi 刮削已批准)

1. 问题

上一版实现后能力不可用/不完整,用户反馈「没有实现接入弹弹play 的弹幕获取和刮削能力」:

  1. 弹幕获取实际拿不到:danmaku.service.ts 客户端已写(match/签名/16MB head MD5/24h 缓存),但
    • 根 .env 中 OPEN_DANMAKU_APP_ID/APP_SECRET 为空 → 运行时恒走「未配置开放网络」降级,零弹幕;
    • match 请求的 fileName 传了含扩展名的完整文件名,官方 swagger 明确要求「不包含文件夹名称和扩展名」,降低识别率;
    • HTTP 非 200 / errorCode≠0 全部吞成 null,排障无从下手;
    • match 成功得到的 episodeId 不落库,每次播放都重新 16MB 读流 + match。
  2. 刮削没有弹弹play 源:只有 Bangumi 文件名搜索;match 响应中的 animeTitle/episodeTitle/imageUrl/animeId 元数据被丢弃。

2. 方案选型

方案 说明 结论
A. 双源互补(推荐) dandanplay 做「文件级识别 + 弹幕身份」源,Bangumi 保留做「文件名级刮削」源;识别结果落库复用 ✅ 采纳
B. 全面换 dandanplay 扫描期每文件要读 16MB 算 hash,大库不可行;凭据未申请时连展示元数据都没有 ❌
C. 只修弹幕、刮削维持现状 不满足「刮削能力」诉求 ❌

凭据配置:维持 env-only(OPEN_DANMAKU_APP_ID/SECRET/API_BASE),不做 DB 存凭据的设置 UI(YAGNI,单实例自托管);弹幕设置页明示配置状态,消除「静默降级」体感。

3. 官方 API 事实依据(2026-09-26 swagger 实读,api.dandanplay.net/swagger/v2/swagger.json)

  • 签名:X-Signature = base64(sha256(AppId + Timestamp + Path + AppSecret)),头 X-AppId/X-Timestamp/X-Signature(与现实现一致)。
  • POST /api/v2/match:请求 {fileName(无扩展名), fileHash(前16MB MD5,可空), fileSize, matchMode(hashAndFileName|fileNameOnly|hashOnly)};响应 MatchResponseV2 = ResponseBase + {isMatched, matches: MatchResultV2[]};MatchResultV2 = {episodeId, animeId, animeTitle, episodeTitle, type, typeDescription, shift, imageUrl};ResponseBase = {errorCode, success, errorMessage}。
  • GET /api/v2/comment/{episodeId}?withRelated=true:响应 {count, comments: [{cid, p: "time,mode,color,uid", m}]}(与现 XML 转换兼容)。
  • GET /api/v2/bangumi/bgmtv/{bgmtvSubjectId}:Bangumi subjectId → 弹弹play 番剧详情(BangumiDetails.episodes[] = {episodeId, episodeTitle, episodeNumber(短标题可排序), airDate}),免 hash 桥。

4. 设计

4.1 数据模型(@app/models)

media_items 新增一列:

字段 说明
dandanplayEpisodeId integer 可空;弹弹play 弹幕库编号(识别成功后落库,弹幕拉取直达)

matchedHash(原预留列)开始写入:成功 hash 识别时存「前 16MB MD5」。迁移:yarn db:generate + yarn db:push。

4.2 服务层(@app/trpc)

danmaku.service = 开放 API 客户端单点(scrape 复用其导出):

  • matchFileName(path):去扩展名修 fileName(新增,含点文件名边界)。
  • openMatch(fileName, size, hash) → {ok, errorCode, errorMessage, isMatched, candidates: MatchResultV2[]}:非 200/errorCode≠0 透出 errorMessage,不再吞 null。
  • openCommentXml(episodeId) → XML(原逻辑拆出)。
  • remoteFileHash() 导出供 scrape 用;openHeaders 恒带 Content-Type。
  • fetch() 流程改为:
    1. 无凭据 → source:"none" + 明确文案(含「需配置 OPEN_DANMAKU_APP_ID/SECRET」);
    2. item.dandanplayEpisodeId 已存在 → 跳过 match 直接 comment;
    3. 否则 hash + match;精确命中(isMatched)时 setDanmakuMatch 落库 episodeId+matchedHash(仅身份,不动标题/海报——改标题属刮削语义,由识别入口显式做)。

scrape.service.matchDandanplay(userId, mediaItemId)(新增,手动入口):

  1. 无凭据 → 报错提示配置;已有 dandanplayEpisodeId 且未要求重识别 → 直接返回已识别信息;
  2. hash + match:isMatched 或有候选 → 取首条,写 dandanplayEpisodeId/matchedHash/title=animeTitle/posterUrl=imageUrl/scrapeStatus=ok;
  3. match 未中且 bangumiId 存在 → bgmtv 桥:pickDandanEpisode(episodes, epNumber) 按集数择集,只补 dandanplayEpisodeId;
  4. 输出 DanmakuMatchOutput {ok, matched, message?, result?},UI toast 展示。

pickDandanEpisode 为纯函数:episodeNumber 数值相等优先,否则同位序回退(第 N 条)。

4.3 tRPC 路由

  • media.scrapeDandanMatch(mutation):入参 {mediaItemId},调 scrapeService.matchDandanplay。
  • media.danmakuGetSettings:响应增加 openNetworkConfigured: boolean(其余仍为本地偏移默认值,持久化仍走 localStorage,不在本次范围)。
  • media.danmakuFetch 行为如 4.2,签名不变。

4.4 前端(@app/web + @app/i18n)

  • 库页匹配弹窗顶部加「弹弹play 文件识别」按钮:调 scrapeDandanMatch,成功 toast 展示 animeTitle/episodeTitle,失败透出 message;弹窗仍保留 Bangumi 搜索绑定。
  • 弹幕设置页顶部状态条:「开放弹幕网络:已配置 / 未配置(服务端 .env 设置 OPEN_DANMAKU_APP_ID/APP_SECRET 后重启)」。
  • i18n:zh-CN 权威,en 同构新增 ~8 key。

4.5 错误处理

场景 策略
凭据未配置 明确文案 + 设置页状态条;不阻断播放/本地 XML
match/comment HTTP 错误或 errorCode≠0 message 透传 errorMessage;返回 source:"none"
hash 读流失败 降级 fileNameOnly 匹配
识别无候选 matched=false + 文案;Bangumi 已绑走桥接兜底
桥接无对应集数 只提示,不写库

4.6 测试与验证

  • 纯函数单测(沿用 vitest 模式):matchFileName(去扩展名/点文件名)、match 响应解析(isMatched/候选/errorCode)、pickDandanEpisode(集数命中/回退)。
  • 命令:yarn workspace @app/trpc test、yarn typecheck、yarn lint、yarn build:web。
  • 真实弹幕需官方凭据:设置页状态条 + 文档说明申请入口,凭据就绪后播放页冒烟即可验证。

5. 不做(本次)

弹幕设置服务端持久化 · 弹幕发送(POST comment)· match 候选多选 UI · shift 弹幕偏移应用 · chConvert 简繁参数 · 扫描期批量 hash 识别 · DB 存凭据。

6. 批准记录

用户提出能力缺口后未在线答复澄清(问询超时),按最佳判断采纳方案 A 推进;如与预期不符,数据模型/service 边界已隔离,可低成本调整。