# huangguo-video 黄果(huangguo.me)短剧抓包下载工具。从 Reqable 抓包记录还原 AES-128 加密的 HLS 视频。 ## 环境 - macOS + [uv](https://docs.astral.sh/uv/) 包管理 - `ffmpeg`(合并分片用,系统已装) - Reqable 3.2.7+(MCP 功能,可选) ```bash cd huangguo-video uv sync # 自动建 .venv 并装依赖(pycryptodome) ``` > `pyproject.toml` 里固定了 `system-certs = true`:本机走 MITM 代理抓包,uv 自带证书库不认系统 CA,没这条 `uv sync` 会报 TLS 错。 ## 文件说明 | 文件 | 用途 | |---|---| | `hls_dl.py` | 通用 HLS 下载器:拉 m3u8 → 并发下分片 → AES-128 解密 → ffmpeg 合并 | | `reqable_mcp.py` | Reqable MCP 客户端:命令行翻抓包记录、调用 Reqable 的 50+ 个工具 | | `huangguo_video_dl.user.js` | **油猴脚本 v1.1.3**:详情页一键全集下载,打包 ZIP 存到 ~/Downloads | > 成品视频(`*.mp4`/`*.zip`)由 `.gitignore` 挡在库外,不入库。 ## 用法一:HLS 直接下载 ```bash # 整条流(m3u8 地址,从 Reqable 复制完整 URL) uv run python hls_dl.py "https://xxx.m3u8?id=..&ticket=.." \ --referer "https://huangguo.me/" -o 视频.mp4 # 单个 .ts 分片 uv run python hls_dl.py x --ts-url "https://xxx/seg.ts?md=.." -o test.ts # 需要走代理时(默认直连) uv run python hls_dl.py "..." --proxy http://127.0.0.1:7890 ``` 要点: - **自动识别** master/media 多级播放列表,选最高码率 - **AES-128 自动解密**,无 `IV` 时按 HLS 规范用 `EXT-X-MEDIA-SEQUENCE` 序号做 IV(0,1,2,…) - 合并优先 `ffmpeg -f concat -c copy`(无损),没有 ffmpeg 回退二进制拼接 - 单次 m3u8 可能只返回部分分片(分页/轮换),拿不到完整列表时走用法二 ## 用法二:从 Reqable 记录还原视频 MCP 是 stdio 型子进程,`reqable_mcp.py` 封装了握手: ```bash # 列出所有 MCP 工具 python3 reqable_mcp.py list # 调用任意工具 python3 reqable_mcp.py call capture_live_get_by_id '{"id": 4289}' # 快捷搜索 python3 reqable_mcp.py m3u8 ``` 典型还原流程: ```python from reqable_mcp import McpClient c = McpClient(); c.initialize() # 1. 按关键词搜记录,拿 ID 列表 r = c.call('capture_live_filter', {'filters': [{'type': 'keyword', 'pattern': 'm3u8'}]}) ids = json.loads(r['content'][0]['text']) # 2. 取详情:m3u8 响应体在 response.body.text,enkey 的 key 是 base64 # 分片密文被 Reqable 落盘,response.body.text 是本地文件路径 d = json.loads(c.call('capture_live_get_by_id', {'id': 4289})['content'][0]['text']) # 3. key + 序号 IV 解密 → ffmpeg 合并(见 git log 里的一次性脚本) ``` 三件套:完整 **m3u8**(记录的响应体) + **AES key**(`/enkey` 响应,base64) + **分片密文**(Reqable 本地缓存文件,缺失的段用记录里的 URL 在线补)。 ## 用法三:油猴脚本一键全集下载 `huangguo_video_dl.user.js` — 安装到 Tampermonkey/Violentmonkey 后,打开任意详情页 `https://huangguo.me/detail//`: 1. 剧集区出现 **「⬇ 全集下载」** 按钮 2. 点击 → 自动读取剧名、遍历全部集数、逐集下载解密转封装 → 打包 `剧名.zip` 存入 **~/Downloads** 3. ZIP 内文件名:`剧名_1.mp4`、`剧名_2.mp4` …(按 `data-ep` 排序) **实现要点**(全部在浏览器内完成,零后端): | 环节 | 方案 | |---|---| | 跨域请求 | `GM_xmlhttpRequest`(`@connect huangguo.me` + `*.rlkfus.cn` + `*.cloudfront.net` + `*` 兜底) | | 网络容错 | 失败重试 2 次(1s)→ 降级原生 `fetch`(CDN 带 `ACAO: *`)→ 完整错误显示到面板 | | 剧名/集数 | `[data-drama-title]` / `data-total-episode` / `a.detail-episode[data-ep]` | | m3u8 地址 | play 页 `.dplayer-video` 的 `data-url`,**拼 `/h5/m3u8/` 前缀** | | AES-128 解密 | WebCrypto `subtle.decrypt`,IV = media sequence 大端 | | TS → MP4 | `mux.js` `mp4.Transmuxer`(输出 fMP4) | | 打包 | **手写 STORE ZIP**(每集 CRC32 后转 `Blob` 卸到堆外,只拼头尾字节) | | 付费集 | 识别 `data-coin-dialog-open`,自动跳过并在完成提示里列出 | **已验证**: - Node 仿真管线:下载解密转封装 21.9MB/3.7s;200MB 打包 **1 秒**,`testzip()` CRC 全过,RSS 294MB - **实战验收**(2026-10-08):《高三爱情故事》全集一键下载成功,ZIP 落入 ~/Downloads ✅ ## 踩坑记录 1. **Reqable MCP 端口**:默认连 `127.0.0.1:9000`,但该端口被 adb 占了,Reqable 实际监听 **9002**,必须 `mcp-server --port 9002`(`reqable_mcp.py` 已写死)。 2. **TLS 证书**:抓包环境有 MITM 证书,`uv`/`curl`/裸 `urllib` 都可能报 `CERTIFICATE_VERIFY_FAILED`。请求走 `hls_dl.http_get()`(显式不走代理)或设 `system-certs = true`。 3. **无 IV ≠ 用 key 当 IV**:HLS 规范是用 media sequence 号大端填充 16 字节,搞错会花屏。 4. **m3u8 地址短时效**:`ticket`/`md` 签名会过期,分片 URL 同理;完整的列表和分片**尽快落盘**(Reqable 的本地缓存就是保险)。 5. **裸 `.ts` 报 Invalid data**:十有八九是 AES 密文,先找 `#EXT-X-KEY`。 6. **m3u8 相对路径前缀**:play 页 `data-url` 是 `v3/...m3u8`,必须拼 `https://huangguo.me/h5/m3u8/`,拼成根路径直接 404。 7. **接口不校验 Referer**:play 页 / m3u8 / 分片 / enkey 全链路去掉 Referer 照常(浏览器脚本本来也伪造不了),这是油猴方案可行的前提。 8. **m3u8 单次即全集**:每次请求返回的就是该集完整列表(如第 1 集 18 段);之前遇到的"3 段广告"是另一个轮换位,别混淆。 9. **`mux.js` 全局名**:浏览器 UMD 分支挂的是 **`muxjs`**(不是 `mux`,vm 实测),脚本里写 `window.muxjs || window.mux` 兼容;Node 加载需 `npm i global` 提供 shim,入口 `mux.mp4.Transmuxer`。 10. **JSZip `generateAsync` 卡 90%**:Firefox 下多集大文件(每集 ~20MB×N)会永久挂死——所有 `Uint8Array` 压在 JS 堆上引发 GC 风暴。解法是弃用 JSZip,每集算完 CRC 立即 `new Blob([bytes])` 卸到堆外,ZIP 组装只拼 local header/central dir/EOCD 字节 + Blob 引用,200MB 1 秒完成。 11. **`@connect` 漏域名 = 油猴静默拦截**:同一站点不同剧集可能走不同 CDN(《陪玩假女友》→ `rlkfus.cn`,《高三爱情故事》→ `cloudfront.net`),白名单没盖住时请求**不发也不报权限错**,只是 GM `onerror` 网络错误。特征:Reqable 里 key 请求有、分片零记录。解法:`@connect *` 兜底。 12. **代理节点瞬断的指纹**:Reqable 记录 `connection.remote.ip` **为空** = 请求进了代理但上游没建连(网络层问题,不是脚本问题);拿记录时间戳对 `ps -o lstart -p ` 看重启时间即可破案。此时段国内域名 200、国外 CDN 全挂,正是分流节点故障的典型特征。