🔑 關鍵洞察
✦ 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——動畫那條就是這樣做的。但那需要在一個公開發布的擴充裡帶著我伺服器的憑證,而發布出去的安裝檔任何人都能下載。這是個還沒解決的設計問題,不是照抄就能做完的事。
這一頁本來是怎麼蓋起來的,寫在這篇:
還沒有留言
✨ 成為第一個留言的人吧