WangQiFei

Keefer


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

© 2026  by  Wangqifei

TryBox Stremio Addon 开发全记录

发布于 2026-10-13 01:10 stremio 

TryBox Stremio Addon 开发全记录

一篇从探针到生产部署的完整技术复盘。包含协议逆向、架构设计、缓存分层、ID 兼容、踩坑与技巧。既是本项目的技术沉淀,也可作为 Stremio Addon 开发的参考。

一、项目背景与目标

1.1 缘起

TryBox(trybox.app)是一个影视聚合站点,提供影视元数据、多源播放链接、中英文字幕。它没有公开 API,所有请求走 /_serverFn/ 下的加密端点,payload 是自定义的 t/p/k/v/a 结构。

目标:把 TryBox 的数据封装成 Stremio Addon,让用户能在 Stremio 客户端里浏览、搜索、看详情、加载字幕、播放流。同时开发一个独立的 Nuvio Provider,让 Nuvio 用户也能访问 TryBox 的流。

1.2 约束

约束 说明
国内访问 用户主要在国内,必须考虑网络可达性
Vercel 部署 选择 Vercel 而非 Cloudflare Workers
Edge Runtime 必须使用 Vercel Edge Function,零第三方运行时依赖
Upstash 共享 多个项目共用一个 Upstash Redis,必须用 APP_PREFIX 隔离
成本 免费层额度内运行
规范 遵守已有的项目开发规范(配置分层、缓存 TTL、日志分级等)

二、协议逆向:探针的设计与实施

2.1 为什么先写探针

TryBox 的 API 是自定义协议。直接写 addon 会面临:

  • 不知道请求 payload 的确切结构
  • 不知道响应的数据字段和类型
  • 不知道 stream 链接是否带鉴权
  • 不知道字幕接口的参数组合

探针的本质:一个独立的诊断脚本,**只做”发请求 + 打印结果”**,不参与业务逻辑。它的职责是:

  1. 验证 API 可用性
  2. 探明请求/响应结构
  3. 评估数据完整性和响应时间
  4. 为后续开发提供”事实依据”

探针原则:不猜、不想当然、以实际返回为准。

2.2 探针的整体设计

我实现的探针脚本(probe.js)覆盖 5 个核心接口:

探针 用途 关键发现
catalog 首页各板块数据 一次请求返回全部板块(713KB),可复用
search 关键词搜索 支持 multi / movie / tv 三种类型
meta 影片详情 TV 类型只返回 seasons 列表,不含 episodes
subtitle 字幕列表 需要 imdb_id,支持语言过滤
stream 播放链接 POST 请求(不是 GET),需要 Bearer Token

2.3 探针的核心技术:decodeNode

TryBox 的响应是加密的 t/p/k/v/a 结构,需要解码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function decodeNode(node) {
if (node === null || node === undefined) return node;
if (typeof node !== 'object') return node;

switch (node.t) {
case 0: // 数字
case 1: // 字符串
return node.s;
case 2: // null
return null;
case 9: // 数组
return (node.a || []).map(decodeNode);
case 10: { // 对象
const obj = {};
const keys = node.p?.k || [];
const values = node.p?.v || [];
for (let i = 0; i < keys.length; i++) {
obj[keys[i]] = decodeNode(values[i]);
}
return obj;
}
case 11: // context 等特殊节点
return null;
default:
return node.s !== undefined ? node.s : node;
}
}

关键洞察:

  • t: 0 表示数字,t: 1 表示字符串,值在 s 字段
  • t: 10 表示对象,键在 p.k,值在 p.v,需要递归解码
  • t: 9 表示数组,元素在 a

这个解码器是后续所有 handler 的基础。

2.4 探针的三大产出

产出 1:请求 payload 模板

1
2
3
4
5
6
7
// Catalog 请求
{
t: { t: 10, i: 0, p: { k: ['data'], v: [
{ t: 10, i: 1, p: { k: ['page'], v: [{ t: 1, s: 'home' }] }, o: 0 }
]}, o: 0 },
f: 127, m: []
}

产出 2:响应结构地图

接口 顶层字段 数组板块
Catalog trending, popular, topRated, secondary, tertiary, presets.* 12 个数组
Meta id, imdb_id, seasons, cast, videos, … 5 个数组
Stream meta, links, mbp_links, beta_links 3 个数组

产出 3:关键事实

  1. Catalog 一次请求返回全部板块(713KB),所有 catalog 可共享同一份缓存
  2. Meta 的 TV 类型需要额外请求 episodes 接口(每季一次)
  3. Stream 是 POST 请求,必须带 Authorization: Bearer header
  4. Stream 链接部分带 IP 查询参数(IP 校验),部分不带
  5. 字幕接口用 imdb_id,不用 tmdb_id

2.5 探针的补充:episodes 探针

第一轮探针没覆盖 episodes(剧集列表),导致后面写 meta 时遇到问题。补充探针后确认:

  • episodes 接口独立于 meta
  • 每季一次请求(不能一次拿全部季)
  • 响应包含 episode_number、name、air_date、still_path 等

教训:探针要覆盖所有下游接口,不能只测主接口。

2.6 探针的最大价值

**避免”想当然”**。举例:

  • 如果没跑探针,会以为 stream 是 GET 请求 → 405 错误
  • 如果没跑探针,会以为 catalog 每个板块需要单独请求 → 浪费 11 倍请求
  • 如果没跑探针,会以为 stream 链接都能用 → 用户播放失败率极高
  • 如果没跑探针,会以为 subtitle 用 tmdb_id → 永远查不到字幕

探针是事实来源,代码是对事实的翻译。


三、架构设计

3.1 分层架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
┌─────────────────────────────────────────────────────┐
│ 客户端 (Stremio / Nuvio) │
└────────────────────┬────────────────────────────────┘
│ HTTP
▼
┌─────────────────────────────────────────────────────┐
│ Vercel Edge Function (api/index.js) │
│ ├─ 路由分发 (handler.js) │
│ └─ 响应构造 (jsonResponse + Cache-Control) │
└────────────────────┬────────────────────────────────┘
│
┌────────────┼────────────┬────────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│catalog │ │ meta │ │subtitle│ │ stream │
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
│ │ │ │
└───────────┴────────────┴────────────┘
│
┌────────────┼────────────┬────────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ decode │ │payload │ │headers │ │ http │
└────────┘ └────────┘ └────────┘ └────────┘
│
┌────────────┼────────────┬────────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│cache │ │ token │ │ tmdb │ │config │
└────────┘ └────────┘ └────────┘ └────────┘
│
▼
┌─────────────────────────────────────┐
│ Upstash Redis (L2 持久化) │
│ TryBox API (上游数据源) │
│ TMDB API (ID 转换) │
│ Supabase (Token 刷新) │
└─────────────────────────────────────┘

3.2 模块职责

模块 职责 是否纯函数
decode.js TryBox 协议解码 是
payload.js 请求 payload 构建 是
headers.js 请求头构建(API / Stream 分离) 是
http.js HTTP 客户端(4xx 不重试、5xx 重试) 否(有网络)
cache.js 三级缓存 + 冷却降级 否
token.js Token 三级刷新 + Bark 告警 否
tmdb.js tt → tmdb 转换 否
config.js 配置合并(默认 + env + user) 是
logger.js 分级日志 是
manifest.js Manifest 动态生成 是
handler/*.js 各资源处理逻辑 否

3.3 关键设计决策

决策 1:为什么用 trybox_{tmdbId} 而不是 tt{imdbId}

  • Catalog 返回的 id 必须是 TryBox 能直接用的 id
  • TryBox 的 catalog 只返回 TMDB ID
  • 如果 catalog 用 tt ID,就需要把 125 个 TMDB ID 都转成 IMDb ID(要么批量查 TMDB,要么逐条查 meta,都很慢)
  • 保留 trybox_ 前缀做内部链路,额外支持 tt 做外部联动,两全其美

决策 2:为什么单独设计 token 管理

  • TryBox 的 stream 接口需要 Bearer Token
  • Token 来自 Supabase,refresh_token 是一次性的(用一次就失效)
  • 必须持久化(内存 + Redis),否则每次请求都拿旧 token → 报 refresh_token_already_used
  • 三级刷新:内存 → 用户配置 → Redis → refresh 刷新 → access 刷新 → Bark 告警

决策 3:为什么 Token 要同时存内存和 Redis

  • 内存:同一 isolate 内快速复用,省 Upstash 请求
  • Redis:跨 isolate、跨请求持久化
  • 写入时同时写两份,读取时先查内存、再查 Redis
  • **access_token 检查 JWT exp**,未过期直接用,避免浪费 refresh_token

四、ID 兼容与联动:Stremio 生态的关键技巧

4.1 Stremio 的 ID 机制

Stremio 的资源请求格式是 /{resource}/{type}/{id}.json。

请求链:

1
Catalog 返回 id → Stremio 用这个 id 请求 Meta → Stremio 用 Meta 的 id 请求 Stream

idPrefixes 的作用:manifest 中声明 idPrefixes 后,Stremio 只会把匹配前缀的请求发给你的 addon。

1
2
3
resources: [
{ name: 'stream', types: ['movie', 'series'], idPrefixes: ['trybox_', 'tt'] },
]

关键规则:

  • idPrefixes 对 catalog 无效(catalog 是特例)
  • 同一个资源可以声明多个前缀
  • 不同资源可以有不同的 idPrefixes

4.2 双 ID 兼容的实现

链路 1:站内 ID(trybox_)

1
2
3
Catalog → 返回 trybox_76331
Meta → 收到 trybox_76331,返回 trybox_76331 + imdb_id
Stream → 收到 trybox_76331,直接提取 tmdbId=76331,调 TryBox stream

链路 2:外部 IMDb ID(tt)

1
2
3
Cinemeta → 返回 tt7660850
Stream → 收到 tt7660850,调 TMDB find API 转成 tmdbId=76331,调 TryBox stream
Subtitle → 收到 tt7660850,直接用 imdbId=tt7660850 调 TryBox subtitle

统一入口:parseAnyId(env, id, type, tmdbKey)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export async function parseAnyId(env, id, type, tmdbKey) {
// 1. trybox_{tmdbId}[:season:episode]
const tryboxMatch = id.match(/^trybox_(\d+)(?::(\d+):(\d+))?$/);
if (tryboxMatch) {
return { tmdbId, kind: type === 'series' ? 'tv' : 'movie', season, episode };
}

// 2. tt{imdbId}[:season:episode]
const imdbMatch = id.match(/^(tt\d+)(?::(\d+):(\d+))?$/);
if (imdbMatch) {
const resolved = await resolveImdbId(env, ttId, tmdbKey);
return { tmdbId: String(resolved.tmdbId), kind: resolved.type, season, episode };
}

return null;
}

4.3 联动效果

当用户从 Cinemeta 进入播放页面时,Stremio 会用 tt7660850 请求所有支持 tt 前缀的 addon。你的 addon 也是其中之一。用户能看到你的流,和其他 addon 的流一起展示。

这就实现了 addon 生态的联动。

4.4 tt → tmdb 转换的缓存

转换结果缓存 7 天(规范里 IMDb 映射的 TTL):

1
2
3
4
5
6
7
8
9
10
11
12
export async function resolveImdbId(env, ttId, tmdbKey) {
const cacheKey = `imdb_to_tmdb:${ttId}`;
const { value, fromCache } = await withCache({
env,
key: cacheKey,
l1Ttl: 86400, // 1 天(内存)
redisTtl: 604800, // 7 天(Redis)
fetcher: () => fetchTmdbFind(ttId, tmdbKey),
shouldCache: (v) => v !== null, // null 不缓存
});
return value;
}

收益:第二次请求同一个 tt ID 时,0 次 TMDB 请求。


五、缓存分层:三级缓存的工程实践

5.1 缓存分层设计

层级 存储 生命周期 命中耗时
L1 内存 Map isolate 生命周期 <1ms
L2 Upstash Redis TTL 控制 200-1500ms
CDN Vercel 边缘 TTL 控制 100-400ms

缓存 key 结构:{APP_PREFIX}:cache:{业务key}

统一的 withCache 包装器:

1
2
3
4
5
6
7
8
9
10
11
12
13
export async function withCache({ env, key, l1Ttl, redisTtl, fetcher, shouldCache }) {
const cached = await getCache(env, key, { l1Ttl });
if (cached !== null) {
return { value: cached, fromCache: true };
}

const value = await fetcher();
const allow = shouldCache ? shouldCache(value) : true;
if (allow) {
await setCache(env, key, value, { l1Ttl, redisTtl });
}
return { value, fromCache: false };
}

5.2 各资源的缓存 TTL

数据 L1 Redis CDN
Catalog 60s 5 分钟 5 分钟
Meta 60s 30 分钟 30 分钟
Episodes 60s 30 分钟 —
Search 60s 10 分钟 5 分钟
Subtitle 60s 10 分钟 5 分钟
Stream(成功) 60s 3 分钟 1 分钟
Stream(负面) — 5 分钟 —
IMDb 映射 1 天 7 天 —

5.3 缓存 key 设计

关键决策:

  • **Catalog 用同一个 key catalog:home**:因为 TryBox 一次返回全部板块,所有 catalog 共享
  • **Episodes 用单个 key episodes:{tvId}**:包含全部季,而不是每季一个 key
  • **tt 转换用 imdb_to_tmdb:{ttId}**:全站共享

收益对比(Meta series):

方案 冷启动请求 热启动请求
每季一个 key 1 次 meta + N 次 episodes + 2(N+1) 次 Upstash 1 + N 次 Upstash
单个 key 1 次 meta + N 次 episodes + 4 次 Upstash 2 次 Upstash

继承之战 4 季:从 5 次 Upstash 降到 2 次。

5.4 Redis 冷却降级

问题:Redis 挂掉时,每次请求都等超时,用户体验极差。

方案:连续 3 次失败进入 60 秒冷却,期间只用 L1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let redisFailCount = 0;
let redisCooldownUntil = 0;

function recordRedisFail() {
redisFailCount++;
if (redisFailCount >= 3) {
redisCooldownUntil = Date.now() + 60000;
redisFailCount = 0;
}
}

function isRedisAvailable(env) {
if (!env?.UPSTASH_REDIS_REST_URL) return false;
if (Date.now() < redisCooldownUntil) return false;
return true;
}

5.5 Upstash REST API 的正确用法

踩坑:我一开始用 URL 路径拼接多参数命令:

1
2
// ❌ 错误
const url = `${UPSTASH_URL}/SET/${key}/${value}/EX/3600`;

Upstash 不识别这种格式,会静默失败(返回空值,但 HTTP 200)。

正确做法:用 /pipeline 端点,发送 JSON 数组:

1
2
3
4
5
6
7
8
9
10
// ✅ 正确
const res = await fetch(`${UPSTASH_URL}/pipeline`, {
method: 'POST',
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify([['SET', key, value, 'EX', '3600']]),
});
// 返回 [{ result: 'OK' }, ...]

教训:第三方 API 必须核对官方文档的调用格式,不能凭直觉拼 URL。

5.6 生产环境 CDN 缓存

Vercel Edge Function 的响应可以加 Cache-Control 头,让 Vercel CDN 缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
function jsonResponse(data, status = 200, { cacheControl } = {}) {
const headers = {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*',
};
if (cacheControl) headers['Cache-Control'] = cacheControl;
return new Response(JSON.stringify(data), { status, headers });
}

// Catalog 返回
return jsonResponse(result, 200, {
cacheControl: 'public, s-maxage=300, stale-while-revalidate=60',
});

效果:

  • 第一次请求:x-vercel-cache: MISS
  • 第二次请求:x-vercel-cache: HIT(不触发 Edge Function)

注意:Vercel 会把响应头的 Cache-Control 重写为 public,但内部的 s-maxage 仍然生效。


六、Token 管理的完整方案

6.1 需求分析

TryBox 的 stream 接口需要 Bearer Token:

  • Token 来自 Supabase Auth
  • refresh_token 是一次性的(用一次就返回新的,旧的立即失效)
  • access_token 有效期 1 小时
  • 必须持久化,否则每次请求都拿旧 token 撞 already_used
  • 失效时要告警给管理员

6.2 三级刷新流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
请求
├─ 1. 读内存 access_token → 检查 JWT exp
│ ├─ 有效 → 直接用
│ └─ 无效 → 往下
├─ 2. 读用户配置的 access_token → 检查 JWT exp
│ ├─ 有效 → 缓存到内存 + 用
│ └─ 无效 → 往下
├─ 3. 读 Redis access_token → 检查 JWT exp
│ ├─ 有效 → 缓存到内存 + 用
│ └─ 无效 → 往下
├─ 4. 用 refresh_token 刷新 → 检查响应
│ ├─ 成功 → 写内存 + 写 Redis + 用
│ └─ 失败 → 往下
├─ 5. 用旧 access_token 刷新(降级)
│ ├─ 成功 → 写内存 + 写 Redis + 用
│ └─ 失败 → 往下
└─ 6. Bark 告警 → 返回空 token → 触发诊断 stream

关键设计:

  • **先检查 JWT exp**,未过期就用,避免浪费 refresh_token
  • 同时写内存和 Redis,跨请求持久化
  • Bark 告警:token 全失效时通知管理员

6.3 JWT 过期检查

access_token 是 JWT,可以 base64 解码 payload 拿 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function decodeJwtExpiry(token) {
try {
const [, payload] = token.split('.');
let b64 = payload.replace(/-/g, '+').replace(/_/g, '/');
while (b64.length % 4) b64 += '=';
const json = JSON.parse(atob(b64));
return json.exp ? json.exp * 1000 : null;
} catch {
return null;
}
}

function isAccessValid(accessToken) {
const exp = decodeJwtExpiry(accessToken);
if (!exp) return true; // 解析失败假设有效
return exp > Date.now() + 60_000; // 留 60s 余量
}

6.4 环境隔离

用 APP_PREFIX 区分环境:

环境 APP_PREFIX
Production tryboxStream:prod
Preview tryboxStream:preview
Development tryboxStream:dev

收益:本地/预览的脏数据不会污染生产缓存。


七、部署与运维

7.1 Vercel 部署

1
npx vercel --prod

环境变量(Vercel Dashboard):

变量 必填
UPSTASH_REDIS_REST_URL 是
UPSTASH_REDIS_REST_TOKEN 是
APP_PREFIX 是
BARK_KEY 否
TMDB_KEY 否
LOG_LEVEL 否

注意:*.vercel.app 在国内访问不稳定,必须绑定自定义域名。

7.2 国内访问优化

问题:*.vercel.app 国内访问延迟 1-4 秒。

方案:

  1. 绑定自定义域名(必须)
  2. 优选 CNAME:vercel-cname.xingpingcn.top(社区维护的优选 IP)
  3. Cloudflare 代理不推荐(会剥掉 x-vercel-cache 头)

效果:国内延迟从 1-4 秒降到 200-500ms。

7.3 health.js 健康检测

34 项测试,覆盖:

分组 测试项 说明
manifest 5 默认配置、资源完整性、功能开关
catalog 10 8 个 catalog × 各自的 type
search 3 中文关键词
genre 4 movie/series × 各 genre
meta 2 movie + series
subtitle 2 movie + series
stream 2 movie + series
tt-id 5 tt 格式 + manifest 声明
debug 1 健康端点

关键设计:

  • 上游波动不判失败:no_valid_url 视为 warning(TryBox 上游偶发返回空)
  • 真实失败才判失败:token_failed、http_failed、decode_failed
  • 支持跳过慢测试:SKIP_SLOW=1

7.4 日志系统

分级:debug / info / warn / error

格式:[HH:mm:ss.SSS][模块][级别] 消息

示例:

1
2
3
[22:54:08.157][TMDB] [INFO] TMDB find: tt0109830
[22:54:08.893][Cache] [DEBUG] [L1 SET] imdb_to_tmdb:tt0109830 ttl=86400s
[22:54:09.275][TMDB] [INFO] resolveImdbId 回源: tt0109830 → {"tmdbId":13,"type":"movie"}

模块前缀:Edge / Router / Catalog / Meta / Subtitle / Stream / Token / Cache / TMDB


八、踩坑与技巧

8.1 探针阶段

坑 1:以为 stream 是 GET

  • 实际是 POST,且必须带 Content-Type: application/json + Bearer Token
  • 教训:探针必须打印实际请求方法

坑 2:以为 catalog 每个板块单独请求

  • 实际一次请求返回全部(713KB)
  • 教训:探针要打印响应的完整结构,不能只看长度

坑 3:以为 meta 就够用

  • TV 类型需要额外请求 episodes
  • 教训:探针要覆盖所有下游接口

8.2 开发阶段

坑 4:idPrefixes 混用

Stremio 允许顶层声明和资源级声明两种方式。资源级覆盖顶层。

正确配置:

1
2
3
4
5
6
resources: [
{ name: 'catalog', types: [...], idPrefixes: ['trybox_'] }, // catalog 的 idPrefixes 无效
{ name: 'meta', types: [...], idPrefixes: ['trybox_'] }, // meta 只用站内 ID
{ name: 'subtitles', types: [...], idPrefixes: ['trybox_', 'tt'] },
{ name: 'stream', types: [...], idPrefixes: ['trybox_', 'tt'] },
]

坑 5:vercel dev 和生产的差异

差异 本地 vercel dev 生产 Vercel
L1 缓存 每次新 isolate 热 isolate 保留
CDN 缓存 不生效 生效
fetch 走代理 不走系统代理 不需要(直连)
Cache-Control 不重写 重写为 public

教训:本地能跑 ≠ 生产能跑。关键功能必须在生产验证。

坑 6:Upstash URL 路径拼接多参数命令失败

SET key value EX 3600 用 URL 路径拼静默失败。**必须用 /pipeline**。

坑 7:诊断 stream 的可读性

错误做法:诊断信息放 title 字段,会被 Stremio 截断。

正确做法:诊断信息放 url 字段,中文原文不编码,用户长按可复制。

1
2
3
4
5
// ✅ 正确
{
name: 'TryBox · 诊断',
url: `trybox|no_valid_url|原始 0 条,全部含 IP 校验或无 URL`,
}

8.3 技巧与妙用

技巧 1:用 bingeGroup 提升剧集体验

1
2
3
4
{
url: '...',
bingeGroup: `trybox-${quality}`, // 如 'trybox-1080p'
}

Stremio 会记住用户在同一 bingeGroup 里选择的质量,下一集自动匹配相同质量。

技巧 2:stremio:// 协议安装

配置页面的安装按钮用 stremio:// 协议:

1
window.location.href = 'stremio://' + url.replace(/^https?:\/\//, '');

比让用户手动复制粘贴 URL 更友好。

技巧 3:Catalog 支持 Search + Genre

不需要单独的 search handler。在 catalog 的 extra 里声明:

1
2
3
4
5
extra: [
{ name: 'search', isRequired: false },
{ name: 'genre', isRequired: false, options: ['动作', '喜剧', ...] },
{ name: 'skip', isRequired: false },
]

Catalog handler 里根据 extra.search 和 extra.genre 分流。

技巧 4:Negative cache 避免反复回源

上游返回空结果时,缓存 5 分钟:

1
2
3
4
if (!validLinks.length) {
await setCache(env, negKey, { detail: '...' }, { redisTtl: 300 });
return { streams: [buildDiagnostic('no_valid_url', detail)] };
}

收益:同一失败请求在 5 分钟内不再回源。

技巧 5:?cfg= 纯前端编码

配置参数用 base64url 编码的 JSON 传递,addon 后端直接解码,无需服务端参与:

1
2
3
4
5
6
7
8
// 前端
const cfg = { rt: '...', cats: ['movie'], feats: ['catalog', 'meta'] };
const encoded = base64urlEncode(JSON.stringify(cfg));
const url = `/manifest.json?cfg=${encoded}`;

// 后端
const cfgParam = url.searchParams.get('cfg');
const user = decodeCfg(cfgParam);

技巧 6:Stremio 客户端缓存

Stremio 客户端会缓存 catalog 和 meta 数据。如果你在开发时改了代码,客户端可能仍显示旧数据。解决办法:

  • 重启 Stremio 客户端
  • 清除缓存(设置里)
  • 换一个新的 manifest URL(加随机 query 参数)

九、项目总结与展望

9.1 关键成果

指标 结果
本地 health.js 34/34 通过
生产 health.js 34/34 通过
Catalog 缓存命中 ~430ms(本地 1.3s)
Meta series 缓存命中 ~800ms(原 7.9s)
tt 路径 完整支持 movie + series + subtitle
CDN 缓存 x-vercel-cache: HIT
第三方依赖 零(纯 Web 标准 API)

9.2 架构亮点

  1. 三级缓存:L1 内存 + Upstash Redis + Vercel CDN,每层独立降级
  2. 双 ID 兼容:trybox_ 站内链路 + tt 外部联动,统一入口 parseAnyId
  3. Token 三级刷新:内存 + 用户配置 + Redis,滚动刷新 + Bark 告警
  4. Episodes 单 key 合并:从 N 次 Upstash 请求降到 1 次
  5. Negative cache:上游失败不反复回源

9.3 后续优化方向

方向 说明
Nuvio Provider 独立项目,Promise 链式写法,不剔除 IP 校验链接
Catalog 按 section 拆分 单 catalog 用户拉取量从 713KB 降到 60KB
TMDB 备用 API 主 API 失败时切换备用(如 api.tmdb.org 镜像)
BingeGroup 优化 剧集播放时自动匹配质量
自定义域名 国内访问优化(优选 CNAME)
监控告警 Vercel 用量告警 + Bark 推送

9.4 经验教训

关于探针:

  • 探针是事实来源,不是可选步骤
  • 探针要覆盖所有下游接口
  • 探针的输出要保留(作为后续开发的参考)

关于协议:

  • 第三方 API 必须核对官方文档
  • 不能凭直觉拼 URL / 猜请求方法

关于缓存:

  • 缓存分层要明确 TTL
  • 缓存 key 要统一前缀
  • Redis 挂掉要降级

关于 Stremio:

  • idPrefixes 是资源级配置,不是全局
  • Catalog 是特例,idPrefixes 无效
  • 诊断信息放 url,不放 title
  • bingeGroup 提升剧集体验

关于部署:

  • 本地能跑 ≠ 生产能跑
  • *.vercel.app 国内访问不稳定
  • 关键功能必须在生产验证

十、附录:Stremio Addon 开发注意事项清单

10.1 Manifest 规范

  • id 必须全局唯一,建议用反向域名(如 community.trybox)
  • version 语义化版本
  • resources 用对象数组声明,每个资源单独配 types 和 idPrefixes
  • catalogs 里的 extra 声明 search / genre / skip
  • idPrefixes 不能包含冒号(Stremio 用 : 分隔 season/episode)
  • behaviorHints.configurable: true 开启配置页

10.2 Catalog 规范

  • metas 数组里的每一项必须有 id、type、name
  • poster 用 https://image.tmdb.org/t/p/w500{path}
  • background 用 https://image.tmdb.org/t/p/original{path}
  • releaseInfo 建议只放年份
  • imdbRating 是字符串(不是数字)
  • 一个 catalog 可以同时支持 movie 和 series(在 manifest 里声明两次)

10.3 Meta 规范

  • meta.id 必须与请求的 id 一致(关键)
  • meta.type 是 movie 或 series
  • series 类型必须有 videos 数组
  • videos[].id 格式:{metaId}:{season}:{episode}
  • videos[].season 和 videos[].episode 是数字
  • meta 失败时返回 { meta: null },不要返回 debug 信息

10.4 Stream 规范

  • 每个 stream 必须有 url / externalUrl / infoHash / ytId 之一
  • name 是分类名(如 “TryBox · MovieBox”)
  • title 是流描述(如 “1080P · 文件名”)
  • quality 是清晰度(1080p / 720p / 4K / ORG)
  • bingeGroup 用于剧集质量记忆
  • 诊断信息放 url,中文原文不编码

10.5 Subtitle 规范

  • subtitles[].id 必须唯一
  • subtitles[].url 是字幕下载地址
  • subtitles[].lang 是 ISO 639-1 两位码(如 zh、en)
  • 不要返回没有 lang 的字幕

10.6 ID 规范

  • ID 前缀不含冒号
  • ID 前缀短、小写、有意义
  • Catalog / Meta / Stream 的 ID 必须一致
  • tt 前缀保留给 IMDb ID

10.7 部署规范

  • Edge Runtime 零第三方运行时依赖
  • 用 APP_PREFIX 隔离环境
  • 缓存 key 统一前缀
  • 绑定自定义域名(国内)
  • 加 Cache-Control 响应头(CDN 缓存)

本文档最后更新于 2026-10-13,项目地址:fqw000/tryboxStremio

Visitors

 上一篇: JWT 与 Supabase 认证机制全解析 

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

© 2026  by  Wangqifei