sharoon7171/anime-nexus-hls-stream-resolver
GitHub: sharoon7171/anime-nexus-hls-stream-resolver
一款自托管的 Node.js 工具,用于将 anime.nexus 的观看 URL 解析为带签名的 HLS 流,并提供 CLI 导出、本地播放网关和 Web UI。
Stars: 0 | Forks: 0
# Anime Nexus HLS 流解析器
一款自托管的 Node.js 工具,接收 `anime.nexus` 的观看 URL,执行与网站播放器相同的 clearance、socket 和 CDN 签名步骤,并通过 CLI JSON 导出、本地 HLS 网关以及一个小型 Web UI 来输出结果。
## 目录
- [概述](#overview)
- [入口点](#entry-points)
- [解析流程](#resolve-flow)
- [Cloudflare clearance](#cloudflare-clearance)
- [流元数据和 attestation](#stream-metadata-and-attestation)
- [VideoSocket](#videosocket)
- [CDN 签名](#cdn-signing)
- [播放网关](#playback-gateway)
- [浏览器 UI](#browser-ui)
- [技术栈](#stack)
- [项目结构](#project-layout)
- [使用方法](#usage)
- [HTTP 路由](#http-routes)
- [免责声明](#disclaimer)
## 概述
**输入** — `https://anime.nexus/watch/{episodeId}[/{slug}]`,由 `lib/nexus/watch.mjs` 解析。
**输出** 取决于您使用的入口点:
| 入口 | 函数 | 解析后的 socket | 主要输出 |
| --- | --- | --- | --- |
| CLI (`cli.mjs`) | `resolve()` | 关闭 | JSON: `signedMasterUrl`, `headers`, 字幕, `videoMeta` |
| 服务器解析 | `openSession()` | 打开 (30 分钟) | JSON: 指向本地网关的 `playUrl` |
| 网关路由 | `serveMaster` / `servePlaylist` / `pipeCdn` | 打开 | 重写后的 M3U8 + 通过 `/play/{playId}/…` 流式传输的 `.m4s` |
未签名的 HLS master 位于流 API 响应 (`data.hls`) 中。每次 CDN 请求仍需要从 `VideoSocket.getToken()` 获取新的 token,以及来自 `cdnHeaders()` 的 header 集。
**源站** (`lib/nexus/site.mjs`):
| 常量 | 主机 |
| --- | --- |
| `SITE_ORIGIN` | `https://anime.nexus` |
| `API_ORIGIN` | `https://api.anime.nexus` |
| `SOCKET_WS` | `wss://prd-socket.anime.nexus` |
| CDN | 嵌入在 `data.hls` 中的主机名 (例如 `video.anime.delivery`) |
## 入口点
```
cli.mjs → resolve(watchUrl) → stdout JSON, exit
server.mjs → GET /api/resolve → { playUrl, signedMasterUrl, … }
→ GET /play/{id}/master.m3u8 → proxied HLS
→ GET /play/{id}/x/{host}/… → signed CDN fetch
public/ui.js → fetch /api/resolve → hls.js on playUrl
```
`server.mjs` 根据请求的 `Host` header (`requestOrigin`) 构建 `playUrl` 和所有重写后的播放列表行 — 源码中没有固定的绑定地址。
## 解析流程
`lib/core/resolve.mjs` — 为 `resolve()` 和 `resolvePlayback()` 共享 `openStream()`:
1. `parseWatchUrl(input)` → `{ episodeId, watchUrl }`
2. `randomUUID()` → 客户端指纹
3. `getCfSession({ episodeId })` → 带有 `cf_clearance` 的 `curl-cffi-node` 会话
4. 并行执行:
- `fetchStreamMeta(episodeId, fingerprint, client)` → API JSON
- `fetchAttestation(watchUrl, client)` → 来自观看 HTML 的 `{ ref, secret }`
5. `pickStream(meta)` → 来自 `data.hls` 的 `{ url, videoId, subtitles, videoMeta }`
6. `VideoSocket.connect()` 带有 episode id、指纹、HLS URL 和 attestation
7. `manifestToken(manifestPath(stream.url))` → 第一个 CDN token
8. `signUrl(stream.url, token, sessionId, 'manifest')` → 签名后的 master
`resolve()` 返回导出字段并在 `finally` 中关闭 socket。
`resolvePlayback()` 为网关返回 `{ socket, http, episodeId, videoId, watchUrl, masterUrl }`。
## Cloudflare clearance
`lib/net/session.mjs` 通过 `httpClient()` (`lib/net/fetch.mjs`,Chrome 131 模拟) 创建一个共享的 API 会话,并调用 `solveCloudflare(streamApiUrl(episodeId), client)`。
遇到挑战时,`lib/cf/solve.mjs` 会:
1. 从 HTML 读取 `__CF$cv$params` (`lib/cf/challenge.mjs`)
2. 获取 `/cdn-cgi/challenge-platform/scripts/jsd/main.js`,使用 `lib/cf/jsdctl.mjs` 解析
3. 将压缩后的浏览器 payload (`lib/cf/browser.mjs` + `lib/cf/lz.mjs`) POST 到 JSD oneshot URL
4. 在导出的 cookie 中确认 `cf_clearance`
缓存 20 分钟 (`session.mjs` 中的 `CF_TTL`),随后重新清除。
## 流元数据和 attestation
**元数据** — `lib/nexus/stream.mjs`
```
GET {API_ORIGIN}/api/anime/details/episode/stream?id={episodeId}&fillers=true&recaps=true
```
Headers 包括 `X-Client-Fingerprint` 和 `X-Fingerprint`。响应 `data.hls` 是 master URL;`videoId` 从该 URL 中的 `/video/{uuid}/stream/` 解析。
**Attestation** — `lib/nexus/attest.mjs`
```
GET {watchUrl}
```
从 HTML 正则提取:
- `attestRef:"…"`
- `wireSecret:"…"`
作为 HMAC-SHA256 证明输入到 socket `auth` 中 (`lib/net/socket.mjs` 中的 `wireProof`)。
## VideoSocket
`lib/net/socket.mjs` — Socket.IO v4,命名空间 `/video`。
连接 URL:
```
wss://prd-socket.anime.nexus/api/socket/?videoId={episodeId}&fingerprint={uuid}&m3u8Url={encoded-hls}&EIO=4&transport=websocket
```
握手:Engine.IO ping/pong → `40/video,` → `auth` packet → 带有 `sessionId` 的 `connected`。
`getToken` payload:
| `requestType` | 字段 | 签名资源 |
| --- | --- | --- |
| `manifest` | `manifestUrl`, `videoId`, `prevToken` | Master、媒体播放列表、初始化 `.mp4` |
| `segment` | `variant`, `segIdx`, `track`, `videoId` | 匹配 `.{variant}_{segIdx}_{track}.m4s` 的 `.m4s` |
通过 `tokenInflight` map 进行在途去重。`manifestToken` 通过 `lastManifestToken` 链接 `prevToken`。
身份验证后的挑战(如果存在)将 `X-Challenge` 和 `X-Encrypted-Secret` 添加到 CDN headers — 按原样转发,不在本地解密。
## CDN 签名
`lib/nexus/stream.mjs` — `signUrl(base, token, sessionId, requestType, segmentPath?)`:
```
{base}?token=…&requestType=…&sessionId=…[&segmentPath=/…]
```
`lib/hls/playlist.mjs` — `tokenizeCdnUrl(socket, videoId, absoluteUrl)`:
- 由 `parseSegment()` 解析的分片路径 → 分片 token + `segmentPath` 参数
- 初始化分片 (`_init-{n}.mp4`) 和播放列表 → manifest token
`lib/cdn/gateway.mjs` — `cdnHeaders(session)` 将 socket headers 与 `Origin: SITE_ORIGIN` 和 `Referer: watchUrl` 合并。
## 播放网关
`lib/core/proxy.mjs` — 内存中的 `sessions` map 以 `playId` (`randomUUID()`) 为键,TTL 为 30 分钟。
**`openSession(watchUrl)`**
1. `resolvePlayback(watchUrl)`
2. 签名并获取 master;存储 `masterBody`
3. 返回 `{ playId, watchUrl, signedMasterUrl, videoId, sessionId, fingerprint, headers }`
**服务**
| 路由 | 模块调用 | 备注 |
| --- | --- | --- |
| `/play/{playId}/master.m3u8` | `serveMaster` | 对缓存的 master 执行 `rewritePlaylist` |
| `/play/{playId}/x/{host}{path}` (`.m3u8`) | `servePlaylist` | 上游获取,重写,为 `#EXT-X-MAP` 执行 `warmInit` |
| `/play/{playId}/x/{host}{path}` (二进制) | `pipeCdn` | `streamCdn` 将响应 body 通过管道传输给客户端 |
重写模式 (`lib/hls/playlist.mjs`):
```
{request-origin}/play/{playId}/x/{cdn-host}{cdn-path}
```
播放列表 body 按上游 URL 缓存在 `session.playlistBodies` 中。初始化 URL 在 `session.inits` 中跟踪,以避免重复预热。
过期或未知的 `playId` → 错误,socket 已关闭。
## 浏览器 UI
`public/index.html` + `ui.js`:
1. 提交观看 URL → `GET /api/resolve?url=…`
2. 在导出字段中显示 `watchUrl` 和 `playUrl`(复制按钮)
3. 将 `playUrl` 加载到 hls.js 1.6.16 中(从 jsDelivr 动态导入)
4. 通过 `performance.now()` 执行解析和播放计时器
播放使用网关 URL,以便在每次分片请求时在服务端应用 token 和 headers。Chrome/Firefox 需要 hls.js;如果指向 `playUrl`,Safari 可以原生播放 HLS。
## 技术栈
| 组件 | 实现 |
| --- | --- |
| HTTP + WebSocket 客户端 | `curl-cffi-node` (`impersonate: chrome131`) |
| Cloudflare JSD | `lib/cf/*` |
| Attestation HMAC | `node:crypto` `subtle.sign` |
| HTTP 服务器 | `node:http` |
| 模块 | ES modules (`.mjs`) |
| UI 播放 | hls.js 1.6.16 |
| 环境变量 | 默认值 | 用于 |
| --- | --- | --- |
| `PORT` | `3000` | `server.mjs` 监听 |
## 项目结构
```
cli.mjs
server.mjs
lib/core/resolve.mjs resolve(), resolvePlayback()
lib/core/proxy.mjs openSession(), serveMaster(), servePlaylist(), pipeCdn()
lib/nexus/site.mjs origins, streamApiUrl(), watchUrl()
lib/nexus/watch.mjs parseWatchUrl()
lib/nexus/stream.mjs fetchStreamMeta(), pickStream(), signUrl()
lib/nexus/attest.mjs fetchAttestation(), manifestPath()
lib/net/session.mjs getCfSession(), cookiesFor()
lib/net/fetch.mjs httpClient(), fetchBytes(), fetchJson()
lib/net/socket.mjs VideoSocket
lib/net/userAgent.mjs
lib/cf/solve.mjs solveCloudflare()
lib/cf/challenge.mjs extractChallengeParams()
lib/cf/browser.mjs JSD payload builder
lib/cf/jsdctl.mjs challenge script parser
lib/cf/lz.mjs LZ compress for JSD POST
lib/hls/playlist.mjs rewritePlaylist(), tokenizeCdnUrl()
lib/cdn/gateway.mjs cdnHeaders(), warmInit(), streamCdn()
public/index.html
public/ui.css
public/ui.js
```
## 使用方法
```
npm install
npm start
```
打开日志中打印的 URL(默认为 `http://localhost:3000`),粘贴观看链接,点击解析 (Resolve)。
**CLI**
```
node cli.mjs "https://anime.nexus/watch/{episodeId}/episode-12"
anime-nexus-hls-resolve "https://anime.nexus/watch/…"
```
用于脚本的一次性导出。如需播放,请运行服务器并使用 `playUrl` — 网关需要打开的 socket 来生成 segment token。
## HTTP 路由
### `GET /api/resolve?url={watchUrl}`
打开一个网关会话并返回:
```
{
"watchUrl": "https://anime.nexus/watch/…",
"playUrl": "http://localhost:3000/play/{playId}/master.m3u8",
"signedMasterUrl": "https://…/master.m3u8?token=…",
"videoId": "…",
"sessionId": "…",
"fingerprint": "…",
"headers": { }
}
```
如果缺少 `url` 则返回 `400`。失败时返回 `500` `{ "error": "…" }`。
### `GET /play/{playId}/master.m3u8`
重写后的 master 播放列表。`Content-Type: application/vnd.apple.mpegurl`。CORS `*`。
### `GET /play/{playId}/x/{host}/{path…}`
代理的 CDN 资源。M3U8 响应被重写;其他内容以原始的 `Content-Type` 流式传输。查询字符串被转发。
### 静态资源
| 路径 | 文件 |
| --- | --- |
| `/`, `/index.html` | `public/index.html` |
| `/ui.css` | `public/ui.css` |
| `/ui.js` | `public/ui.js` |
### CLI stdout
来自 `lib/core/resolve.mjs` 中 `resolve()` 的所有字段:`episodeId`、`watchUrl`、`videoId`、`fingerprint`、`sessionId`、`masterUrl`、`signedMasterUrl`、`subtitles`、`videoMeta`、`headers`。
## 免责声明
本项目不托管、存储或分发媒体。anime.nexus 及其 CDN 是独立的服务。解析器读取公开的观看页面,并以与浏览器相同的方式调用其 API。
您有责任遵守版权法、网站服务条款和当地法规。不提供任何担保。仅在您有权访问的内容上使用。
标签:GNU通用公共许可证, HLS, MITM代理, Node.js, Web爬虫, 反向代理, 流媒体工具, 自定义脚本, 视频解析