把一次完整的技术项目从需求到落地的过程沉淀下来。不炫技,只记录真实的取舍、踩过的坑和可复用的经验。
一、起点:为什么造这个轮子
事情的起因很简单:自己在用 Stremio 看片,但常用的几个中文资源站没有对应的 Addon。市场上能找到的几个要么维护停滞,要么接口失效,要么把简单的事情做得过于复杂。
我手上有两样东西:
- 一个已经能跑的 TVBox Spider(Python),包含完整的签名算法、接口调用和业务逻辑
- 一份从 GitHub 上找到的参考项目(jpyyStremio),跑在 Vercel Edge Functions 上,架构清晰
想法很直接:把 Python Spider 的接口能力,用 jpyy 的架构重新实现成 Stremio Addon。
于是有了这个项目。过程比预想的长,也比我预想的有意思。
二、技术全景
目标站点的接口特点
懂片帝是一个 Next.js 应用,所有 /v1/* 接口都需要 HMAC-SHA256 签名。签名算法:
1 | sign = HMAC-SHA256( |
请求头需要 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 | // verify-sign.mjs |
1 | # dpd.py |
两个输出必须逐字节一致。这是后续所有请求的基础,值得花时间验证。
踩过的坑:
- 消息分隔符是
\n(换行),不是空格 - nonce 是 32 位 hex 小写,不是 UUID
- 时间戳是 13 位毫秒字符串,不是秒
挑战 2:resolve:// 票据的定位
站点对同一个影片会下发 20~30 条”线路”:
1 | url_kind=m3u8 resolved=true 匿名可用 → 直接播放 |
官方高清线路走的是第二种。流程:
1 | GET /v1/playback/resolve/{token} |
关键洞察:官方线路画质最高但需 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),站点收录的是中文译名(如 三体),直接搜索匹配不到。
四步方案:
- 拉取所有别名:
/tv/{id}/alternative_titles返回 9 种语言 - 按优先级排序:纯中文 > 中文混合 > 原始标题 > 其他
- 多标题搜索:逐个搜索站点,合并去重
- 相似度匹配:Levenshtein 距离 + 年份加分 + 精确匹配加分
实测效果:
1 | [IMDbResolve] resolve: tt13016388 type=series season=1 |
一个反直觉的发现:/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。
黑名单功能的设计:
- 配置入口:代码默认(
DEFAULT_BLOCKED_LINES)+ 配置页面追加 - 生效位置:
- 官方票据线路:请求前过滤,省 HTTP 请求
- 直连 m3u8 线路:响应前过滤,响应瘦身
- 维护工具:
health.js脚本自动跑多部片,统计每个线路名的成功率
关键差异:
| 分支 | 过滤时机 | 收益 |
|---|---|---|
| 官方票据 | 请求前 | 省 HTTP(黑名单里的不发请求) |
| 直连 m3u8 | 响应前 | 响应瘦身(url 已经在响应里) |
这个区别很微妙,但影响实际性能。早期我把两个都做成响应前过滤,白白发了不少必失败的请求。
六、调试方法论
这个项目调试量很大。沉淀下来的方法论:
1. 独立探针脚本先行
改业务代码之前,先用独立脚本验证接口行为:
verify-sign.mjs:签名对拍probe.mjs:遍历所有接口,输出字段结构test-resolve-line.mjs:验证 Cookie 解析票据test-threads.mjs:验证新发现的搜索协议
收益:接口行为弄清楚后再写业务代码,避免在 addon 里反复调试。
2. 结构化日志
分级 + 模块前缀:
1 | [14:52:37.444] ℹ️ [Handler] stream source=dpd id=av_5LAgCLxw... |
允许 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 | if (hasCookie) { |
当 hasCookie = false 时整个块跳过,函数隐式返回 undefined,上层 lines.length 报错。
教训:if 块内的 return 一定要确认它是否在正确的条件分支下。
坑 2:Cookie 缺前缀
用户从浏览器复制的 ai_movie_session 值形如 ums2_2026-08.eyJ2...,但发送时必须补上前缀:
1 | Cookie: ai_movie_session=ums2_2026-08.eyJ2... |
代码里做兼容:用户只填值就自动补前缀,填完整 Cookie 就用原样。
坑 3:build-version 会变
站点前端发版后,x-ai-movie-build-version 头必须同步更新,否则签名校验失败返回 401。
当前:默认值写在代码里,配置页面允许用户覆盖。
未来:可以做一个自动化探测——从站点的 JS bundle 里提取最新版本号。
坑 4:jq 语法
1 | # ❌ 错 |
前者会让 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
