19a3828b188185b7be69e9d25f80303ce32bc0b4
- gfetch 失败自动重试 2 次(1s 间隔),应对代理/节点瞬断 - 重试耗尽降级原生 fetch(CDN 带 ACAO:* 可直连) - 错误原因完整显示到面板,不再只报网络错误 - 背景: 17:25 mihomo 节点故障导致分片请求全部建连失败, Reqable 记录 remote.ip 为空 + mihomo 17:29 重启为证
huangguo-video
黄果(huangguo.me)短剧抓包下载工具。从 Reqable 抓包记录还原 AES-128 加密的 HLS 视频。
环境
- macOS + uv 包管理
ffmpeg(合并分片用,系统已装)- Reqable 3.2.7+(MCP 功能,可选)
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 |
油猴脚本:详情页一键全集下载,打包 ZIP 存到 ~/Downloads |
陪玩假女友_1.mp4 |
成品:123s / 720×1280 / H.264+AAC / 21.93 MB |
用法一:HLS 直接下载
# 整条流(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 封装了握手:
# 列出所有 MCP 工具
python3 reqable_mcp.py list
# 调用任意工具
python3 reqable_mcp.py call capture_live_get_by_id '{"id": 4289}'
# 快捷搜索
python3 reqable_mcp.py m3u8
典型还原流程:
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/<slug>/:
- 剧集区出现 「⬇ 全集下载」 按钮
- 点击 → 自动读取剧名、遍历全部集数、逐集下载解密转封装 → 打包
剧名.zip存入 ~/Downloads - ZIP 内文件名:
剧名_1.mp4、剧名_2.mp4…(按data-ep排序)
实现要点(全部在浏览器内完成,零后端):
| 环节 | 方案 |
|---|---|
| 跨域请求 | GM_xmlhttpRequest(@connect huangguo.me + *.rlkfus.cn) |
| 剧名/集数 | [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 仿真真实管线):
- 下载解密转封装:第 1 集 21.9MB/3.7s、第 2 集 14.5MB/2.4s,解压后文件名与时长均正确
- 打包:10 集 × 20MB = 200MB 压测,1 秒内完成,Python
testzip()CRC 逐字节全过,内存 RSS 294MB
踩坑记录
- Reqable MCP 端口:默认连
127.0.0.1:9000,但该端口被 adb 占了,Reqable 实际监听 9002,必须mcp-server --port 9002(reqable_mcp.py已写死)。 - TLS 证书:抓包环境有 MITM 证书,
uv/curl/裸urllib都可能报CERTIFICATE_VERIFY_FAILED。请求走hls_dl.http_get()(显式不走代理)或设system-certs = true。 - 无 IV ≠ 用 key 当 IV:HLS 规范是用 media sequence 号大端填充 16 字节,搞错会花屏。
- m3u8 地址短时效:
ticket/md签名会过期,分片 URL 同理;完整的列表和分片尽快落盘(Reqable 的本地缓存就是保险)。 - 裸
.ts报 Invalid data:十有八九是 AES 密文,先找#EXT-X-KEY。 - m3u8 相对路径前缀:play 页
data-url是v3/...m3u8,必须拼https://huangguo.me/h5/m3u8/,拼成根路径直接 404。 - 接口不校验 Referer:play 页 / m3u8 / 分片 / enkey 全链路去掉 Referer 照常(浏览器脚本本来也伪造不了),这是油猴方案可行的前提。
- m3u8 单次即全集:每次请求返回的就是该集完整列表(如第 1 集 18 段);之前遇到的"3 段广告"是另一个轮换位,别混淆。
mux.js全局名:浏览器 UMD 分支挂的是muxjs(不是mux,vm 实测),脚本里写window.muxjs || window.mux兼容;Node 加载需npm i global提供 shim,入口mux.mp4.Transmuxer。- JSZip
generateAsync卡 90%:Firefox 下多集大文件(每集 ~20MB×N)会永久挂死——所有Uint8Array压在 JS 堆上引发 GC 风暴。解法是弃用 JSZip,每集算完 CRC 立即new Blob([bytes])卸到堆外,ZIP 组装只拼 local header/central dir/EOCD 字节 + Blob 引用,200MB 1 秒完成。
Languages
JavaScript
55.3%
Python
44.7%