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爬虫, 反向代理, 流媒体工具, 自定义脚本, 视频解析