WangQiFei

Keefer


思想提纯
  • 首页
  • 归档
  • 标签
  • 关于
  •     

© 2026  by  Wangqifei

Stremio Addon:懂片帝插件开发实录

发布于 2026-10-09 22:10 stremio nuvio 

把一次完整的技术项目从需求到落地的过程沉淀下来。不炫技,只记录真实的取舍、踩过的坑和可复用的经验。


一、起点:为什么造这个轮子

事情的起因很简单:自己在用 Stremio 看片,但常用的几个中文资源站没有对应的 Addon。市场上能找到的几个要么维护停滞,要么接口失效,要么把简单的事情做得过于复杂。

我手上有两样东西:

  1. 一个已经能跑的 TVBox Spider(Python),包含完整的签名算法、接口调用和业务逻辑
  2. 一份从 GitHub 上找到的参考项目(jpyyStremio),跑在 Vercel Edge Functions 上,架构清晰

想法很直接:把 Python Spider 的接口能力,用 jpyy 的架构重新实现成 Stremio Addon。

于是有了这个项目。过程比预想的长,也比我预想的有意思。


二、技术全景

目标站点的接口特点

懂片帝是一个 Next.js 应用,所有 /v1/* 接口都需要 HMAC-SHA256 签名。签名算法:

1
2
3
4
sign = HMAC-SHA256(
key = "8b9a908a05eac640e1ee06f52acaa741bfe4ba9e004eeffdbeb635e532e06666",
msg = "METHOD\n/path?query\n毫秒时间戳\n32位hex随机数"
)

请求头需要 7 个 x-ai-movie-* 字段,包括时间戳、nonce、签名。

关键接口:

接口 用途
/v1/browse/catalog 分类浏览 + 关键词搜索
/v1/catalog/{id} 影片详情
/v1/catalog/{id}/episodes 剧集列表
/v1/playback/resolve/{token} 解析播放线路
/v1/playback/resolve-line 解析官方票据(需登录)

Addon 架构

选 Vercel Edge Functions 作为运行环境,原因:

  • 免费额度充足:个人自用绰绰有余
  • 全球边缘节点:用户就近响应
  • 无状态天然适配:Stremio Addon 本来就是无状态服务
  • CDN 缓存开箱即用:Cache-Control 响应头直接生效

技术栈刻意保持极简:

  • 语言:纯 JavaScript(ES Module)
  • 运行时依赖:零第三方库,只用原生 fetch + WebCrypto
  • 缓存:Upstash Redis(可选)+ Vercel CDN

不用 node-fetch、不用 axios、不用 express,是因为 Edge Runtime 本身提供了这些能力,引入库反而增加冷启动开销和版本管理成本。


三、五个真实的技术挑战

挑战 1:签名算法的跨语言对拍

Python 用 hmac + hashlib,JS 用 WebCrypto。算法逻辑一样,但编码细节极易出错。

我写了两个最小脚本独立对拍:

1
2
3
// verify-sign.mjs
const MSG = `GET\n/v1/browse/catalog?kind=movie&page=1&limit=20\n1700000000000\n${'0'.repeat(32)}`;
// HMAC-SHA256 → hex
1
2
3
# dpd.py
MSG = 'GET\n/v1/browse/catalog?kind=movie&page=1&limit=20\n1700000000000\n' + '0'*32
print(hmac.new(KEY, MSG.encode(), hashlib.sha256).hexdigest())

两个输出必须逐字节一致。这是后续所有请求的基础,值得花时间验证。

踩过的坑:

  • 消息分隔符是 \n(换行),不是空格
  • nonce 是 32 位 hex 小写,不是 UUID
  • 时间戳是 13 位毫秒字符串,不是秒

挑战 2:resolve:// 票据的定位

站点对同一个影片会下发 20~30 条”线路”:

1
2
url_kind=m3u8          resolved=true   匿名可用    → 直接播放
url_kind=resolve_ticket resolved=false 需登录 → 二次解析

官方高清线路走的是第二种。流程:

1
2
3
4
5
6
GET /v1/playback/resolve/{token}
→ 返回 line_options[],每条含 url=resolve://<ticket>

POST /v1/playback/resolve-line
body: {"ticket": "<去掉 resolve:// 前缀的字符串>"}
→ 返回 line.url = https://xxx.toutiaovod.com/...mp4

关键洞察:官方线路画质最高但需 Cookie,采集 m3u8 线路画质一般但匿名可用。两条腿都要有,且要能自动切换。

我的策略:

  • 无 Cookie:只用 m3u8
  • 有 Cookie:优先解析官方票据,全部失败则回退 m3u8

挑战 3:请求限流与 4xx 语义

11 条票据线路并发请求,站点直接返回 429。

解决方案:串行 + 250ms 间隔。11 条从”17 秒 + 全部失败”变成”5 秒 + 3 条成功”。

另一个发现:4xx 错误重试毫无意义。

状态码 含义 重试
401 登录态失效 ❌ 立即失败
402 需付费会员 ❌ 立即失败
404 线路不可用 ❌ 立即失败
429 站点限流 ❌ 越重试越糟
5xx 服务端错误 ✅ 可重试
网络异常 偶发 ✅ 可重试

改成 /HTTP 4\d\d/ 统一判断后,失败场景的耗时从 30 秒降到 8 秒。

挑战 4:缓存分层与 negative 缓存

三级缓存设计:

层级 存储 命中耗时 生命周期
L1 内存 Map < 1ms 60 秒
L2 Upstash Redis < 10ms 5 分钟 ~ 7 天
L3 Vercel CDN < 100ms 全局 TTL

negative 缓存是后期加的优化:

某片的官方线路一旦全部失败(例如”复仇者联盟”全部 402/404),5 分钟内不再尝试。用户第二次进入时直接走 m3u8 回退分支。

效果对比:

场景 优化前 优化后
第二次请求 8.3 秒 2.0 秒
用户感受 明显等待 秒开

关键设计点:negative 缓存的 key 包含 cookie 的短 hash。用户切换账号后能立即重试,不被旧缓存阻挡。

挑战 5:IMDb 多语言匹配

这是最复杂的模块。问题背景:TMDB 返回英文原名(如 3 Body Problem),站点收录的是中文译名(如 三体),直接搜索匹配不到。

四步方案:

  1. 拉取所有别名:/tv/{id}/alternative_titles 返回 9 种语言
  2. 按优先级排序:纯中文 > 中文混合 > 原始标题 > 其他
  3. 多标题搜索:逐个搜索站点,合并去重
  4. 相似度匹配:Levenshtein 距离 + 年份加分 + 精确匹配加分

实测效果:

1
2
3
4
5
[IMDbResolve] resolve: tt13016388 type=series season=1
[IMDbResolve] 候选标题: 三体 | 3 Body Problem | 3体 | Задача трёх тел | ...
[IMDbResolve] 搜索合并:原始 65 条 → 过滤后 19 条
[IMDbResolve] season 1 命中: "三体 第1季"
[IMDbResolve] 解析成功并缓存: tt13016388 → vodId=av_gRw-...

一个反直觉的发现:/v1/browse/catalog?query_mode=fast_v3&q=三体 比 /v1/suggest?q=三体 快 3 倍且结果更全。

  • suggest:需要额外补 N 次 detail,共 N+1 次请求
  • fast_v3:**一次请求返回完整 cards[]**,字段齐全

从 7 次 HTTP 降到 1 次,这是搜索体验的关键优化。


四、配置分层的设计哲学

初期配置散落在三个位置(代码、环境变量、URL 参数),混乱且维护成本高。重构后形成清晰的三层:

层级 位置 生命周期 典型项
部署级 环境变量 一次部署 UPSTASH_*、APP_PREFIX
用户级 URL ?cfg= 每请求 bd、bv、blockedLines、cookie
代码默认 config.js 代码版本 BASE_DOMAIN、DEFAULT_BLOCKED_LINES

关键原则:

  • 用户级配置只存用户主动改过的字段。没改的不写,服务端用代码默认值兜底。这样开发者更新默认值时所有用户自动获益。
  • 代码默认 + 用户增量,不是”覆盖”。比如黑名单:DEFAULT_BLOCKED_LINES ∪ user.blockedLines。开发者发现某线路长期失效,加到代码里所有用户立即受益。
  • cfg 用 base64url 编码。URL 里不能直接放 JSON,base64url 编码后全是 URL 安全字符。Stremio 客户端会自动附加这个参数到每次请求。

这套设计的核心价值:用户永远不需要手动同步默认值,但又有完全的覆盖能力。


五、黑名单:从被动到主动

30 条 m3u8 线路展示给用户,体验很差。很多是同一内容的重复源,还有一批长期 404。

黑名单功能的设计:

  1. 配置入口:代码默认(DEFAULT_BLOCKED_LINES)+ 配置页面追加
  2. 生效位置:
    • 官方票据线路:请求前过滤,省 HTTP 请求
    • 直连 m3u8 线路:响应前过滤,响应瘦身
  3. 维护工具:health.js 脚本自动跑多部片,统计每个线路名的成功率

关键差异:

分支 过滤时机 收益
官方票据 请求前 省 HTTP(黑名单里的不发请求)
直连 m3u8 响应前 响应瘦身(url 已经在响应里)

这个区别很微妙,但影响实际性能。早期我把两个都做成响应前过滤,白白发了不少必失败的请求。


六、调试方法论

这个项目调试量很大。沉淀下来的方法论:

1. 独立探针脚本先行

改业务代码之前,先用独立脚本验证接口行为:

  • verify-sign.mjs:签名对拍
  • probe.mjs:遍历所有接口,输出字段结构
  • test-resolve-line.mjs:验证 Cookie 解析票据
  • test-threads.mjs:验证新发现的搜索协议

收益:接口行为弄清楚后再写业务代码,避免在 addon 里反复调试。

2. 结构化日志

分级 + 模块前缀:

1
2
3
4
[14:52:37.444] ℹ️ [Handler] stream source=dpd id=av_5LAgCLxw...
[14:52:40.358] ℹ️ [Adapter] 解析 11 条票据线路(已配置 Cookie)
[14:52:40.676] ℹ️ [HTTP] 402 (317ms) .../resolve-line
[14:52:40.677] 🐛 [Adapter] 票据解析失败 (高清-官方B): 需付费会员

允许 URL 参数临时调级:?debug=1 打开 debug 级别,不用改代码重启。

3. 失败也要有语义

早期日志:

1
[Adapter] 票据解析失败: HTTP 402

后期日志:

1
[Adapter] 票据解析失败 (高清-官方B): 需付费会员

后者一眼就能看出是”付费墙问题”,无需查资料。

4. 时间线思维

调试性能问题时,看日志时间戳,把每一步的耗时拆出来。

比如 stream 请求的 2 秒,实际是:

  • Edge 冷启动:300ms
  • episodes 缓存:200ms
  • negative 缓存:200ms
  • direct 缓存:200ms
  • detail 缓存:200ms
  • jq 处理 + 传输:900ms

知道时间花在哪里,才有优化方向。


七、踩过的典型坑

坑 1:if (hasCookie) 花括号位置

1
2
3
4
5
6
7
8
9
if (hasCookie) {
// 官方线路处理
if (ticketLines.length > 0) { ... }
else { ... }

// 直连分支被错误地放在这里 ← 错误
const direct = ...;
return lines;
}

当 hasCookie = false 时整个块跳过,函数隐式返回 undefined,上层 lines.length 报错。

教训:if 块内的 return 一定要确认它是否在正确的条件分支下。

坑 2:Cookie 缺前缀

用户从浏览器复制的 ai_movie_session 值形如 ums2_2026-08.eyJ2...,但发送时必须补上前缀:

1
2
Cookie: ai_movie_session=ums2_2026-08.eyJ2...
^^^^^^^^^^^^^^^^^ 前缀不能少

代码里做兼容:用户只填值就自动补前缀,填完整 Cookie 就用原样。

坑 3:build-version 会变

站点前端发版后,x-ai-movie-build-version 头必须同步更新,否则签名校验失败返回 401。

当前:默认值写在代码里,配置页面允许用户覆盖。

未来:可以做一个自动化探测——从站点的 JS bundle 里提取最新版本号。

坑 4:jq 语法

1
2
3
4
5
# ❌ 错
jq '.metas | length, .metas[0].name'

# ✅ 对
jq '{ count: (.metas | length), first: .metas[0].name }'

前者会让 jq 先返回 length 的数字,再把数字当数组索引 .metas[0],报错。


八、数值对比:优化效果一览

场景 优化前 优化后 提升
搜索耗时 7 次 HTTP,约 3~7 秒 1 次 HTTP,约 1 秒 ~5 倍
首次 stream 30 秒(含票据解析全部失败) 30 秒 —
二次 stream(缓存命中) 8.3 秒 2.0 秒 ~4 倍
官方失败分支耗时 30 秒(含 4xx 重试) 8 秒 ~4 倍
黑名单省 HTTP 无 请求前过滤 视黑名单大小
用户可见线路数 30 条(混杂失效线路) 15~25 条(精选) 体验提升

九、设计原则沉淀

回头看,这个项目里几条值得记住的原则:

1. 接口行为先于业务代码

接口返回什么字段、怎么分页、错误码语义——这些必须用独立脚本验证清楚再动业务代码。

2. 缓存要有分层,更要有”负缓存”

不仅缓存成功结果,也缓存”短期失败”。避免用户反复踩同一个坑。

3. 配置分层,但要有明确边界

部署级、用户级、代码默认——三层的职责不能混。

4. 日志不只是 debug 用,更是产品的一部分

好的日志在故障时是救命稻草,在日常运行时是性能分析的依据。

5. 失败要”优雅”而不是”消失”

官方票据全部失败不是”删除线路”,而是”回退到 m3u8”。用户永远有东西可看。

6. 优化要看数据,不靠直觉

“多一个 resolve 请求”听起来没多少,实际占 40% 耗时。”250ms 间隔”听起来拖慢速度,实际把失败率从 100% 降到 70%。


十、未来可以做的

  • 多季支持:动漫/综艺的多 variant 结构,可以按 season_label 分组
  • HLS 代理:如果 m3u8 分片某天需要 Referer,本地代理模块可以补上
  • 自动化 build-version 探测:从站点 JS bundle 里提取最新版本号
  • CDN 预热:热门 catalog 定时刷新 CDN 缓存

十一、结语

这个项目从”我想有个自己用的 Stremio 插件”开始,到”完整的、可部署的、有维护工具的开源项目”,花了大概两周的业余时间。

最耗时的不是写代码,而是理解站点行为和设计正确的抽象。签名算法、票据解析、缓存分层、配置分层——每个问题的”正确答案”都不是一次想出来的,是先用独立脚本验证事实,再写业务代码逐步逼近的。

如果你也在做类似的事,希望这篇文章能帮你少走几步弯路。真正的经验往往不在于”怎么做对了”,而在于”哪些路是死路”。


项目地址:[dpdStremio](https://github.com/fqw000/dongpiandiStremio)

技术栈:JavaScript / Vercel Edge Functions / Upstash Redis / Stremio Addon SDK

License:MIT

 

下一篇: 一个IP绑定流媒体stream源的完整开发记录-jpyy- 

© 2026  by  Wangqifei