Skip to content

Repository files navigation

nico2dandan

在本地将 Niconico(ニコニコ動画)弹幕 抓取并转换为 弹弹play(dandanplay)兼容格式 的 API 服务,可作为 danmaku-anywhere(弹幕任何地方)扩展的本地 Niconico 弹幕源,也可供其他 dandanplay 兼容的播放程序调用。

快速开始(Docker)

docker compose up -d --build

Docker 方式下服务默认映射到宿主机 http://localhost:3012(容器内监听 3000,见下)。也可以不使用 Docker 直接运行:

npm install
npm run build
npm start          # 开发模式: npm run dev

接入 danmaku-anywhere

danmaku-anywhere 内置了弹弹play 弹幕源(manifest),支持将 API 根地址指向自建服务。接入步骤:

  1. 启动本服务(docker compose up -d);
  2. 打开 danmaku-anywhere 扩展弹窗 → 弹幕源(Danmaku Providers)页面;
  3. 点击添加 弹弹play 兼容源(Add DanDanPlay Compatible Provider);
  4. 基础地址(Base URL)设置为 http://localhost:3012,保存;
  5. 播放视频时触发弹幕搜索/匹配,即可从 Niconico 获取弹幕。

说明:扩展请求本地地址需要「允许访问私有/回环主机」的选项,danmaku-anywhere 的 manifest 运行选项已默认开启(allowPrivateHosts: true),且扩展的 host_permissions 覆盖 http://*/*,无需额外配置。

使用流程

每个 Niconico 官方频道(一般即一部动画)在弹幕源中表现为一个「作品」,内含全部分集:

搜索/匹配 ──▶ GET /api/v2/search/anime?keyword=关键词   (SnapShot 搜索,按频道合并)
选择作品 ──▶ GET /api/v2/bangumi/ch{频道ID}              (频道分集列表)
获取弹幕 ──▶ GET /api/v2/comment/{数字编码episodeId}      (单集弹幕)

搜索时输入动漫名/频道名(如「とある魔術の禁書目録」)即可命中对应作品。

API 端点

所有端点与 dandanplay API v2 兼容。bangumiId 使用频道 ID(ch{频道ID})或 Niconico 视频 ID,episodeId 使用数字编码 ID(见下文)。

路径前缀:所有接口同时挂载于 /api/v2/.../v2/... 两种路径。新版 danmaku-anywhere(manifest 引擎)使用 /api/v2/,旧版扩展使用 /v2/,两种均可正常使用。

1. 获取弹幕

GET /api/v2/comment/:episodeId
curl "http://localhost:3012/api/v2/comment/sm9"
{
  "count": 938,
  "comments": [
    {
      "p": "0.00,4,65535,d2d9baa9eda7c175ff53537fa734c765",
      "m": "ニコニコに入り浸って15年という自分に震える"
    }
  ],
  "related": []
}

p 字段为逗号分隔的 4 项(dandanplay 兼容生态的标准格式):

# 字段 含义 转换自
0 time 弹幕出现时间(秒,两位小数) vposMs / 1000
1 mode 1=滚动,4=底部,5=顶部 mail 命令 ue/shita
2 color 十进制 RGB 颜色 mail 命令命名色 / 高级会员 #rrggbb
3 uid 用户 ID 的 md5 哈希(可省略) userId

说明:

  • 弹幕集合与 Niconico Web 端一致:弹幕线程接口按视频时间轴返回各时间段的"直近弹幕"(约数百至上千条),不支持历史全量分页。
  • 弹幕 ID 超过 JS 安全整数范围(Niconico 使用 64 位 ID),因此不输出 cid 字段(dandanplay 格式中该字段可选)。
  • 结果在服务端缓存(默认 1 小时,CACHE_TTL_SECONDS 可调)。

2. 搜索

GET /api/v2/search/anime?keyword=<关键词>&page=<页码>

底层使用 Niconico SnapShot Search API,仅按标题匹配并按播放量排序。

搜索结果会自动筛选并合并

  • 只保留动漫番剧:按内容 ID 前缀过滤,仅保留 so(官方频道番剧正片)/ na / niconico 开头的视频,排除 sm / nm / ss 开头的普通用户投稿(MAD、剪辑、音乐、CM 等杂音);
  • 去除 dアニメストア 重复版本:同一集若同时存在 dアニメストア 频道(默认 2632720「dアニメストア ニコニコ支店」)与其他频道的版本,只保留主站版本;仅在 dアニメストア 存在的作品仍会保留;
  • 按频道合并为作品:同一官方频道的多个分集合并为一个选项(如搜索一部番只出现一个结果),选项名为频道名(一般即动画名),点进去后分集按集数排序、分集名为具体视频标题。

合并后的作品通过 /api/v2/bangumi/ch{频道ID}(如 ch2650196)获取分集列表,分集直接抓取 Niconico 频道视频页面(准确、完整,不受搜索噪声影响),抓取失败时自动降级为按频道名搜索。

curl "http://localhost:3012/api/v2/search/anime?keyword=%E3%81%A8%E3%81%82%E3%82%8B"
{
  "success": true,
  "errorCode": 0,
  "errorMessage": "",
  "animes": [
    {
      "animeId": 641,
      "bangumiId": "ch641",
      "animeTitle": "とある魔術の禁書目録<インデックス>",
      "type": "other",
      "typeDescription": "Niconico",
      "imageUrl": "",
      "startDate": "",
      "episodeCount": 1,
      "rating": 0,
      "isFavorited": false
    }
  ]
}

合并作品的 animeId 即频道 ID,bangumiIdch{频道ID};搜索页显示的 episodeCount 为当页搜到的集数,完整分集以 /api/v2/bangumi/ch{频道ID} 返回为准。

3. 作品详情

GET /api/v2/bangumi/:bangumiId

bangumiId 支持两种形式:

  • 频道 ID(ch{频道ID},如 ch2650196):返回该频道(一般即一部动画)的所有分集,作品名为频道名,分集按集数排序;
  • 单个 niconico 视频 ID(如 sm9):返回单视频作品(含单集)。

episodes[].episodeId数字编码 ID(dandanplay 格式要求 episodeId 为数字):由视频 ID 确定性编码、可无状态反解(如 so465259032000046525903),弹幕接口可直接用该数字请求。

频道模式示例(/api/v2/bangumi/ch641):

{
  "success": true,
  "errorCode": 0,
  "errorMessage": "",
  "bangumi": {
    "animeId": 641,
    "bangumiId": "ch641",
    "animeTitle": "とある魔術の禁書目録<インデックス>",
    "episodes": [
      { "episodeId": 10000064101, "episodeTitle": "とある魔術の禁書目録 第1話「学園都市」", "episodeNumber": "1" },
      { "episodeId": 10000064102, "episodeTitle": "とある魔術の禁書目録 第2話「魔女狩りの王」", "episodeNumber": "2" }
    ]
  }
}

10000064101 等为演示用的数字编码 ID,实际以接口返回为准。)

单视频模式(/api/v2/bangumi/sm9)返回含单集的同名作品,用于直接按视频 ID 调用。

4. 视频匹配(供弹弹play 桌面客户端等使用)

POST /api/v2/match

从请求体 fileName 中提取 Niconico 视频 ID(把本地视频文件命名为 sm12345678.mp4 即可命中),也可通过 nicoId 字段直接指定:

{ "fileName": "sm15630734.mp4", "fileHash": "...", "fileSize": 123456, "videoDuration": 300 }

5. 健康检查

GET /healthz

配置(环境变量)

变量 默认值 说明
PORT 3000 监听端口
HOST 0.0.0.0 监听地址
CACHE_TTL_SECONDS 3600 弹幕结果缓存时间(秒)
NICO_HTTP_TIMEOUT_MS 15000 请求 Niconico 的超时(毫秒)
NICO_USER_SESSION Niconico 登录态 user_session cookie,用于抓取需要登录才能观看的视频
SEARCH_LIMIT 20 搜索每页返回条数
DANIME_CHANNEL_IDS 2632720 dアニメストア 在 niconico 的频道 ID(逗号分隔)。搜索到同一标题时,若同时存在 dアニメストア 版本与其他版本,删除 dアニメストア 版本

抓取需要登录的视频

部分视频(会员限定、部分频道视频)需要登录态。在浏览器中登录 Niconico 后,从 Cookie 中取出 user_session 的值注入环境变量:

docker run -d -p 3012:3000 -e NICO_USER_SESSION="你的user_session" nico2dandan:latest

或在 docker-compose.yml 中设置 NICO_USER_SESSION。该 cookie 有有效期,过期后需更新。

集成测试

项目包含三个测试,均使用扩展真实使用的引擎/格式对本服务做端到端验证:

  • test/integration.mjs:使用 danmaku-anywhere 同款引擎(@mr-quin/dangoManifestRunner)+ 真实弹弹play manifest,走 搜索 → 作品详情 → 弹幕 流水线;
  • test/legacy-provider.mjs:模拟旧版扩展(manifest 引擎之前的 @danmaku-anywhere/danmaku-provider),用其 zod schema 原样校验 /v2/... 路径的 搜索 → 作品 → 弹幕 → 关联 流程;
  • test/filter.test.mjs:搜索筛选与合并逻辑(动漫前缀过滤 + dアニメストア 去重 + 频道合并/集数提取/排序)单元测试。
npm run build && npm start &        # 先启动服务
node test/integration.mjs            # 默认 http://localhost:3012
node test/legacy-provider.mjs        # 默认 http://localhost:3012
BASE_URL=http://localhost:3001 node test/integration.mjs   # 指定服务地址

工作原理

弹幕获取分两步(与 Niconico 官方 Web 端一致):

  1. GET https://www.nicovideo.jp/watch/{videoId}?responseType=json 获取视频信息(含 nvComment 弹幕配置);
  2. POST {nvComment.server}/v1/threads,携带 threadKeyparams.targets,请求头使用 x-client-os-type: othersx-frontend-id: 6x-frontend-version: 0 获取弹幕线程数据。

获取到的弹幕(vposMs 毫秒时间戳 + commands mail 命令 + 颜色/字号/位置)随后转换为 dandanplay 的 4 字段 p 格式。

License

GPL-3.0(见 LICENSE)。

About

将 Niconico 弹幕格式转换为 弹弹play 兼容格式的 API 服务

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages