🔑 关键洞察
✦ AI·GEN2026 年 7 月 30 日,Trakt 在没有任何公告的情况下把免费帐号建立 API app 的功能改成 VIP 限定,而既有的 app 也一并消失——作者的「在看什么」页面因此静静地停止更新,后端只留下 `client not found` 这一句话。这篇从查证开始:trakt-web 的 PR #3057 只写了「挡住建立」、还注明既有清单「仍然可读」,但真正发生的是删除;而 trakt-api 那个平常连「时间戳秒数归零」都会预告的 Announcements 分类,这次一则都没有。官方在四天后才回应,回应方式是先把 issue 转移、转成讨论串、上锁,一分钟后才留下三句「暂时措施」。后半是完整的迁移过程:Yamtrack、Floppy、沿用 UTS 三个方案怎么被原始码查证刷掉,为什么最后选 Simkl(官方扩充、PIN 流程免 callback、逐集时间戳、原生增量),以及在写任何一行程式码之前先把三个未知打掉的做法。附三个踩坑——docker-compose 的 environment 会用空字串盖掉 env_file、文件写的端点在实际帐号回 0 笔、以及作者自己把 TMDb 补图的路堵死导致海报糊掉。最后顺手 fork 了 UTS 换成 Simkl 后端,29 支平台撷取器一支未动。文末诚实列出还没接的部分:「现在正在看」那个即时状态。
7 月 31 日晚上,我发现「在看什么」那页停在 7 月 25 日。
没有错误讯息。伺服器没挂、页面正常渲染、动画那半边还在更新——只有电影和影集那半边,停住了。
去 Trakt 官网想手动重登,结果更怪:明明登入成功,每次跳转都被踢回首页。
而 那边看起来完全正常——它照样抓到我看了什么,照样显示配对结果。
一个「没有任何一处报错,但资料就是不再流动」的故障。后来才知道,那天全世界有一批人跟我卡在同一个地方。
两个字串#
翻到后端日志深处,只有两个东西:
client not found
session not foundclient not found 这句话的意思很明确——Trakt 那边查不到我的 OAuth app 了。不是密钥过期、不是 token 失效,是那个 app 本身不存在。
去 Trakt 的 API 设定页确认:空的。我几个月前建的那个 app,不见了。
它是被谁删的#
Trakt 的网页前端是开源的,所以这件事不需要猜。
trakt/trakt-web 的 PR #3057,标题 feat(settings): make api application creation vip-only,作者是 Trakt 自己的开发者 vladjerca,2026 年 7 月 30 日建立,同一天合并。
改动内容包括这几条新的介面字串:
"Creating new apps requires Trakt VIP"
"VIP members can register unlimited OAuth applications."以及把建立按钮包进 <RenderFor audience="vip">。
到这里为止都还合理——一家公司决定某个功能只给付费会员,那是它的权利。
但这个 PR 只做了「挡住建立」。 它的说明里写得清清楚楚:免费使用者会在应用程式清单上方看到一张升级提示卡,而那份清单本身是 "(still readable)"——仍然可读。
从头到尾,没有任何一个字提到会删掉既有的 app。
而我的 app 不见了。GitHub 上的 issue #3061 标题就叫 "Can't see API apps",开票的人说他的 app 是「v3 出现之前」就建的,现在整份清单是空的,而他的整合开始收到 403。底下四则留言,全是同一件事:
my API app was deleted without warning. Not a VIP here either, but i feed them data and now get nothing in return. Is this intentional?
The lack of notice just plain sucks. Whoever took that decision needs to hire a comms team...
「没公告」这件事,可以用他们自己的纪录证明#
这不是我的主观感受,是可以查的。
trakt/trakt-api 这个 repo 有一个 Announcements 分类,而且他们平常真的在用:
| 编号 | 日期 | 标题 |
|---|---|---|
| #775 | 2026-04-29 | ‼️ Upcoming API Changes: Watched Endpoints Pagination & Extended Defaults |
| #694 | 2026-01-29 | 🔔 Upcoming Change: Seconds and Milliseconds Will Be Zeroed Out |
| #681 | 2026-01-20 | 📣 Upcoming API Changes: Pagination & Sorting Updates |
| #655 | 2025-12-01 | ⚠️ Reminder: Set a proper User-Agent header |
连「时间戳的秒数之后会归零」这种等级的改动,都会事先发一则预告。
而截至本文写作,那个分类最新一则公告仍停在 6 月 11 日的文件更新。删掉一票人 API app 这件事,一则都没有。
所以能证明的是:他们有这个机制、平常会用、这次没用。 不能证明的是动机——我看不到任何内部纪录。但目前所有看得到的行为,都指向同一个方向。
四天后,官方回应了#
8 月 3 日,Trakt 的人终于出现。回应的方式比内容更值得记录——同一分钟内发生了三件事:
把那张 issue 从 trakt-web 转移到 trakt-api。
同一秒,issue 被转换成 Discussion、原本的讨论串被锁定。从「bug 追踪」变成了「问答区」——一个不需要被修掉的东西。
一分钟后,官方帐号留下三句话。
This is just a temporary measure to prevent abuse. Our intention is to keep the API accessible for developers. We'll have more to announce soon.
下一则留言来自另一位开发者,他的 app 上架在 App Store:
Why don't you communicate about it BEFORE deleting API apps then? Give us some notice and some delay before doing such moves, you shouldn't cut access like that, some of us depend on those for production apps. ... You've lost my trust as well.
而那句 "temporary",到今天为止:PR #3057 没有被回退,官方公告仍然没有,期间倒是又上线了一个新的 VIP 功能。
同一时间,这件事在三个地方被讨论,而它们现在的状态刚好跟「谁拥有那个平台」一致:
| 平台 | 谁的地盘 | 现在 |
|---|---|---|
| GitHub | 他们的 repo | 转移、转成讨论串、上锁 |
| 官方论坛 | 他们的 | 整串回 404 |
| 不是他们的 | 还在,102 赞、60 则留言 |
论坛那串我特地确认过不是网址写错:同一台伺服器的搜寻 API 回应正常,只有那个主题不存在了。至于是谁删的,我没有证据,不写。
Reddit 那串倒是有个容易误读的地方——原始贴文显示 [deleted],但那是发文者自己删的,页面上写得很清楚,留言全都还在。我一开始差点把它算成「又一个被消失的讨论」,查清楚之后发现不是。
那 60 则留言里,最高票的一句是:
Trakt is speedrunning the dumbest path to monetization.
而另一则留言提醒了我一件事——对那里的人来说,这不是单一事件:
they redesigned their UI which everyone hates, got rid of the option to use the old version of the site, increased pricing for VIP, and now cut off their API for free users. I'm sure there's more that I'm forgetting, but it's all anti-user.
我只用了 Trakt 两个月,没有立场评论前面那几件。但这解释了为什么那串的情绪比「一个 API 坏掉」该有的强度高得多。
先搞清楚我到底失去了什么#
在找替代品之前,我先查了一件更基本的事:我的资料到底是怎么进来的?
结果跟我以为的不一样:
netflix 788 笔 全部在 2026-05-28 同一秒写入 ← 一次性历史汇入
trakt 155 笔 电影更新到 7/25、影集停在 5/29
letterboxd 1 笔 2026-05-28 一次性那 788 笔 Netflix 是我自己下载 CSV 汇进去的,一次性,之后再也没动过。Trakt 只有 155 笔,看起来微不足道——
但它是唯一还在流动的来源。它占比小只是因为它才跑了两个月。
也就是说:Trakt 一断,我的电影与影集纪录就完全没有自动来源了。(动画那条走的是我自己写的动画疯同步,不受影响。)
顺带查到一个一直没发现的问题:那 151 笔影集纪录,watched_date 全部是 NULL。Trakt 从来没给过我逐集的观看时间。这件事等一下会变成选择的关键。
三个看起来可行的方案,查完全部出局#
我试着找能接上原本架构的东西。判准只有一个:它必须能让我的后端把资料拉回来。
两个自架的追踪站,GitHub 上都很活跃,乍看都能取代 Trakt 的位置。
我没有只读 README,直接搜原始码:
Yamtrack rest_framework / api/urls → 0 个结果
Floppy rest_framework / APIView → 0 个结果两个都没有给第三方拉资料的 REST API。 它们的定位是「给人看的追踪站」,不是给别的服务当资料源。
而且 Floppy 的 Trakt 汇入功能需要你自己建一个 Trakt API app——那条路对我来说刚好死掉。
判准其实早就写在架构里#
绕了一圈之后我意识到:我不需要重新设计这个功能。需求就写在我已经盖好的东西里。
要换的只是中间那一颗。所以判准变得很明确:
不然我的后端拿不到资料。
最好是官方自己维护的扩充功能,少一层断点。
这一条我刚刚才学到,而且是硬学的。
为什么是 Simkl#
Simkl 第一个通过的测试很单纯:它的公开端点不用任何凭证就回资料。
curl https://api.simkl.com/movies/trending # 直接就有回应Trakt 连 trending 都要 api-key。这是一个关于「这家公司怎么看待 API」的讯号。
然后我发现架构几乎是一比一对应:
| Trakt(坏掉前) | Simkl | |
|---|---|---|
| Netflix 撷取 | UTS(第三方扩充) | 官方扩充,原生支援 Netflix + Crunchyroll |
| 授权流程 | OAuth + redirect_uri | 多一个 PIN 流程,伺服器免 callback |
| 每集观看时间 | 我拿到的全是 NULL | episode_watched_at=yes 给真时间戳 |
| 增量同步 | 没有,每次分页硬拉 | 原生 date_from |
| 公开端点 | 要 api-key | 完全不用凭证 |
那个 PIN 流程(device flow)对我特别有价值:token 是靠轮询 /oauth/pin/{USER_CODE} 拿的,整个过程没有任何 callback 打回我的伺服器。我当初为了 Trakt 那套 redirect 开的对外端点,可以省掉。
但我要把话说清楚——
WARNING
Simkl 一样是免费的第三方服务,理论上可以重演一模一样的事。
我查到的「开发者页没有 VIP 字样、API 免费开放」是今天的事实,不是承诺。Trakt 在 7 月 30 日之前也长这样。
真正能免疫的只有「资料落在自己机器上」。这次迁移没有解决那个问题,只是换了一个目前看起来比较开放的对象。
写程式之前,先把不确定的都打掉#
被烧过一次之后,我不想再赌。所以在动任何一行后端程式码之前,先做最小验证。
这正是 Trakt 倒掉的那一关。 如果 Simkl 也挡,整个方案当场放弃。
结果:建得出来。第一个未知清掉。
轮询 8 次、约 40 秒拿到。而 token 的回应有个意外的好消息:
{ "access_token": "…", "token_type": "bearer", "scope": "public" }没有 expires_in,也没有 refresh_token。 Simkl 的 token 不会过期。
我为了 Trakt 的 token 轮替写的那套互斥锁、原子写档、double-check——整组都不需要了。而那套机制,正是当初烧掉我 Trakt 授权的东西。
在 Simkl 上把一部电影标成已看,然后拉回来看 JSON 到底长怎样。
对照我现有的资料表,每一栏都有对应,而且 ids 一次给 imdb / tmdb / tvdb / letterboxd 四种 ID。
到这一步为止,我一行程式码都还没改。
有一条规则会直接决定实作方式#
Simkl 的文件里有一段警告,粗体是他们自己标的:
Never run unconditional background polling timers without active user interaction. Ensure you always use
date_fromto avoid overloading the API server. If you don't follow these rules, your client_id will be suspended.
我原本 Trakt 那套是每 6 小时无条件全量拉。照搬过去,client_id 会被停权——那等于自己动手重演一次 Trakt。
正确做法是他们文件写的三段式:
每 N 小时:
1. GET /sync/activities ← 很轻,只回一堆时间戳
2. 跟本地存的游标比对,一样就结束 ← 绝大多数的轮询停在这里
3. 有变才 GET /sync/all-items/?date_from=<上次的时间戳>这比 Trakt 那套分页硬拉干净太多,而且大部分时候根本不会产生第二个请求。
实际迁移#
后端的新模组大约 300 行,取代原本 450 行的 Trakt 逻辑。差异不只是行数:
| Trakt(旧) | Simkl(新) | |
|---|---|---|
| token | 7 天、要轮替 | 不过期 |
| 并发保护 | 互斥锁 + 原子写档 + double-check | 不需要 |
| token 存哪 | 一个 JSON 档 | 环境变数 |
| 游标 | 没有,每次全量分页 | 资料表里的一列,增量 |
| 轮询 | 无条件全量 | 先问 activities,没变就结束 |
游标存哪这件事,我特地没有沿用档案——Trakt 那个 token 档就是痛点。但既有的计数表栏位是整数,存不了 ISO 时间戳,所以另外开了一张很小的 sync_state 表。
三个坑#
① docker-compose 的 environment 会盖掉 env_file
凭证明明写在 .env.backend 里,worker 却说缺少 client id。
原因是我在 docker-compose.yml 的 environment 底下多写了一行:
- SIMKL_CLIENT_ID=${SIMKL_CLIENT_ID:-}${VAR:-} 读的是 compose 自己的 .env,而那里没有这个键 → 展开成空字串 → environment 优先于 env_file,于是空字串盖掉了真值。
把那行删掉就好。凭证本来就是靠 env_file 整份载入的,不需要在 environment 里逐条列。
② 文件说的端点,在我的情境下回 0 笔
Simkl 文件的第一阶段写「分别呼叫 /sync/movies」。实测:
/sync/movies → 0 笔
/sync/all-items/movies → 1 笔同一个帐号、同一部电影。要用 /sync/all-items/{type} 才拿得到。
如果没有先做那个「打真实 API 看形状」的步骤,我会在接完后端之后才发现,并且第一个念头一定是「我程式写错了」。
③ 我自己把补图的路堵死了,结果海报糊掉
这个最有意思,因为它是我修好一件事的同时弄坏了另一件。
同步跑通之后,首页那张横幅图糊得很明显。第一个假设是「Simkl 没有高清图」——错的。
真相是:我的后端本来就有一段逻辑,会对「海报栏位是空的」那些资料去 TMDb 补图,而且补的不只海报,还有原始尺寸的横式剧照。
Trakt 那条同步只写标题、日期、TMDb ID,刚好把这条路留着。而我写 Simkl 的时候,顺手把它家 CDN 的小图填进了海报栏位 → 补图判断「已经有海报了」→ 跳过 → 横式剧照永远是空的。
而页面的横幅是「有横式剧照就用,没有就退回海报」——没有横式剧照,就拿直式海报拉宽撑满,当然糊。
修法是加一段幂等的资料库迁移,把那笔的海报栏位清成空值,让补图逻辑重新接手。
顺手做了一个 fork#
Simkl 的官方扩充只做 Netflix 和 Crunchyroll,而 UTS 支援 29 个平台——Disney+、HBO Max 那些都在里面。
我不想为了换 hub 而放弃那些平台,所以去量了一下把 UTS 改成送 Simkl 要多少工。答案比预期好:
要改写的 Trakt 相关 约 1,396 行(8 支档案)
29 支平台撷取器 完全不用动
disneyplus 提到 Trakt 0 次
hbo-max 提到 Trakt 0 次撷取器跟 Trakt 是干净解耦的。 那些平台各自的网页结构有多难搞,上游已经替我踩过了,而那部分跟后端是谁完全无关。
所以这个 fork 的本质是「换掉后端,继承 29 支撷取器」,不是重写。上游是 MIT 授权,我用 GitHub 的 fork 而不是复制程式码——保留 commit 历史与原作者的贡献纪录,授权关系也最清楚。连结放在文末。
过程里最值得记的一件事:改名让型别检查全绿,建置却炸了。
改名脚本把程式码里引用的图示档名改了,实际的档案没改。型别检查看不到资产路径,是打包阶段才解析失败的。所以我在 CI 里把 build 补了上去——原本只跑型别检查跟 lint,那类问题两个都抓不到。
另外有一个会静默出错的东西,我觉得比建置炸掉危险得多:上游有个功能会去社群资料库抓「配对修正」的建议,而那里面全是 Trakt 的 ID。在 Simkl 上套用会配到毫不相干的条目,而且不会有任何错误讯息。整个停用了。
现在的状态#
8 月 4 日,我在 Netflix 看完一部电影。没有做任何事。
排程到点的时候,后端先问了一次 activities,发现时间戳变了,才去拉那一段增量,写进资料库,补了 TMDb 的图。等我想到要去看的时候,它已经在页面上了。
整条管线是通的:
但有一件事我没有做完,不想写得像全部搞定了。
「现在正在看」那个即时状态,目前只有动画那条还活着。Trakt 的即时状态随着那 377 行程式码一起被移除了,而 Simkl 这条我查过之后决定先不接——因为扩充功能只在播放、暂停、停止三个时机才打 API,连续播放一整部片的期间,Simkl 那边收不到任何更新。拿它的「暂停中」资料当「正在看」,显示出来的会是两天前停在 1% 的东西。
正确的做法是让扩充直接把心跳送到我自己的伺服器,绕过 Simkl——动画那条就是这样做的。但那需要在一个公开发布的扩充里带着我伺服器的凭证,而发布出去的安装档任何人都能下载。这是个还没解决的设计问题,不是照抄就能做完的事。
这一页本来是怎么盖起来的,写在这篇:
还没有留言
✨ 成为第一个留言的人吧