🔑 重要なポイント
✦ AI·GEN2026年7月30日、Trakt は何の告知もないまま、無料アカウントでの API アプリ作成を VIP 限定に変更した。そして既存のアプリまで一緒に消えた——筆者の「いま観ているもの」ページは静かに更新を止め、バックエンドに残ったのは `client not found` の一行だけだった。本稿はまず検証から入る。trakt-web の PR #3057 が書いているのは「作成を塞ぐ」ことだけで、既存の一覧は「引き続き閲覧できる」と明記さえされているのに、実際に起きたのは削除だった。そして「タイムスタンプの秒をゼロにします」という程度の変更にも予告を出す trakt-api の Announcements カテゴリには、この件が一本もない。公式の回答は四日後、しかも issue を移送し、ディスカッション化し、ロックしたうえで、一分後に「一時的な措置」という三文だけだった。後半は移行の全過程。Yamtrack、Floppy、UTS の流用という三案がソースの確認でどう脱落したか、最終的に Simkl を選んだ理由(公式拡張、コールバック不要の PIN フロー、エピソード単位のタイムスタンプ、標準の差分同期)、そしてコードを一行も書く前に不確実性を潰す進め方。踏んだ穴も三つ収録した——docker-compose の environment が空文字で env_file を上書きする件、ドキュメント通りのエンドポイントが実アカウントで0件を返した件、そして筆者自身が TMDb の画像補完の道を塞いでポスターをぼやけさせた件。最後に UTS を fork してバックエンドだけ Simkl に差し替えた話(29 のサービス抽出器は一切触っていない)と、まだ繋いでいない部分——「いま観ている」というリアルタイム状態——を正直に並べている。
7月31日の夜、「いま観ているもの」のページが7月25日で止まっていることに気づいた。
エラーメッセージはない。サーバーは落ちていないし、ページも正常に描画される。アニメの側はいまも更新されている。映画とドラマの側だけが、止まっていた。
手動で入り直そうとTraktのサイトへ行くと、もっと妙なことになった。ログインは成功するのに、画面遷移のたびにトップページへ戻される。
一方で は完全に正常に見えた。視聴したものをきちんと拾い、マッチング結果も表示している。
どこもエラーを出さないまま、データだけが流れなくなる障害。あとで知ったが、あの夜、世界中の一定数の人がまったく同じ場所で止まっていた。
二つの文字列#
バックエンドのログの奥に、二つだけ残っていた。
client not found
session not foundclient not found の意味ははっきりしている——Trakt 側で私の OAuth アプリが見つからない。シークレットの期限切れでも、トークンの失効でもない。アプリそのものが存在しない。
Trakt の API 設定ページを確認すると、空だった。数か月前に作ったあのアプリが、消えていた。
誰が消したのか#
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)"——引き続き閲覧できる、と。
既存のアプリを削除するとは、一文字も書かれていない。
それでも私のアプリは消えていた。GitHub の issue のタイトルはそのまま "Can't see API apps"。報告者のアプリは 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 には 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日のドキュメント更新のままだ。一つの層のアプリをまとめて消した件については、一本もない。
つまり証明できるのは、仕組みはあり、普段は使っており、今回は使わなかったということ。証明できないのは動機だ——内部の記録は見えない。ただ、見えている行動はすべて同じ方向を指している。
四日後、公式が回答した#
8月3日、ようやく Trakt の人間が現れた。内容よりも、回答の届き方のほうが記録に値する——同じ一分のあいだに三つのことが起きている。
issue が trakt-web から trakt-api へ移された。
同じ秒に、issue は Discussion へ変換され、元のスレッドはロックされた。バグ追跡から Q&A へ——修正されるものから、回答されるものへ。
一分後、公式アカウントが三文だけ残した。
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 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 | 彼らのリポジトリ | 移送・ディスカッション化・ロック |
| 公式フォーラム | 彼らのもの | スレッドごと 404 |
| 彼らのものではない | 健在。102 票、60 コメント |
フォーラムのほうは URL の間違いでないことを確かめた。同じサーバーの検索 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、一度きりNetflix の 788 件は、自分で 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 アプリを作る必要がある——ちょうど私にとって閉ざされた道だ。
判断基準は、すでに構成の中に書かれていた#
一周してから気づいた。この機能を設計し直す必要はない。要件はすでに、自分が組んだものの中に書いてある。
差し替えるのは真ん中の一つだけ。基準もはっきりした。
なければバックエンドにデータが届かない。
できれば公式が保守している拡張機能がよい。切れる箇所が一つ減る。
これはたったいま、実地で学んだ。
なぜ Simkl なのか#
Simkl が最初に通したテストは単純だった。公開エンドポイントが、認証情報なしでデータを返す。
curl https://api.simkl.com/movies/trending # そのまま応答が返るTrakt は trending にすら api-key を求める。API に対する会社の姿勢が出るところだ。
そして構成がほぼ一対一で対応することが分かった。
| Trakt(壊れる前) | Simkl | |
|---|---|---|
| Netflix の取得 | UTS(サードパーティ拡張) | 公式拡張。Netflix + Crunchyroll に対応 |
| 認可の流れ | OAuth + redirect_uri | PIN フローもあり、サーバー側のコールバック不要 |
| エピソード単位の時刻 | 私の手元では全部 NULL | episode_watched_at=yes で実際のタイムスタンプ |
| 差分同期 | なし。毎回ページングで全件 | date_from を標準で用意 |
| 公開エンドポイント | api-key が必要 | 認証情報なし |
とくに PIN フロー(デバイスフロー)がありがたかった。トークンは /oauth/pin/{USER_CODE} をポーリングして取得するので、私のサーバーへコールバックが飛ぶことが一切ない。Trakt の redirect のために開けていた外向きのエンドポイントを、閉じられる。
ただし、はっきりさせておきたいことがある。
WARNING
Simkl も無料の第三者サービスであり、理屈のうえではまったく同じことが起こりうる。
「開発者ページに VIP の文字はなく、API は無料で開かれている」というのは今日の事実であって、約束ではない。7月30日より前の Trakt も、こう見えていた。
本当に免疫があるのは「データが自分の機械の上にある」ことだけだ。今回の移行はその問題を解決していない。いまのところ open に見える相手に、預け先を替えただけである。
コードを書く前に、不確実性を先に潰す#
一度焼かれた以上、また賭けたくはない。だからバックエンドのコードに一行も触れる前に、最小限の検証をした。
まさに Trakt が倒れたのがこの関門だ。 Simkl も塞いでいるなら、その場で計画は破棄になる。
作れた。一つ目の不確実性が消えた。
8回のポーリング、約40秒。そしてトークンの応答に思わぬ朗報があった。
{ "access_token": "…", "token_type": "bearer", "scope": "public" }expires_in も refresh_token もない。 Simkl のトークンは期限切れにならない。
つまり Trakt のトークン更新のために書いたミューテックス、アトミックな書き込み、二重チェック——すべて不要になる。そしてその機構こそが、そもそも私の Trakt 認可を焼き切った当のものだった。
Simkl 上で映画を一本「視聴済み」にし、それを取得して JSON の実物を見る。
既存のテーブルと突き合わせると、どの列にも対応があった。しかも ids は imdb / tmdb / tvdb / letterboxd をまとめて返してくれる。
ここまでで、まだコードは一行も書いていない。
実装方針を直接決めるルールが一つある#
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(新) | |
|---|---|---|
| トークン | 7日、更新が必要 | 期限切れなし |
| 競合対策 | ミューテックス + アトミック書き込み + 二重チェック | 不要 |
| トークンの置き場 | JSON ファイル | 環境変数 |
| カーソル | なし。毎回ページングで全件 | テーブルの一行、差分取得 |
| ポーリング | 無条件に全件 | まず activities、変化がなければ終了 |
カーソルの置き場にファイルをあえて使わなかったのは、Trakt のトークンファイルがまさに痛点だったからだ。ただ既存のカウンタ列は整数で 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 側を書いたとき、私は親切のつもりで Simkl の CDN の小さい画像をポスター列に入れた → 補完の判定が「もうポスターがある」と見なす → 飛ばす → 横長スチルは永遠に空のまま。
そしてバナーは「横長スチルがあれば使い、なければポスターで代替」する——スチルがないので、縦長のポスターを 880px まで引き伸ばす。ぼやけて当然だ。
直し方は、冪等なマイグレーションでその行のポスター列を空に戻し、補完ロジックに引き継がせること。
ついでに fork も作った#
Simkl の公式拡張は Netflix と Crunchyroll のみ。一方 UTS は 29 サービスに対応していて、Disney+ も HBO Max もそこに入っている。
中継を替えるためにそれらを捨てたくなかったので、UTS を Simkl 向けに直す工数を測った。予想より良かった。
書き換えが要る Trakt 関連 約 1,396 行(8 ファイル)
29 のサービス抽出器 一切触らない
disneyplus Trakt への言及 0 回
hbo-max Trakt への言及 0 回抽出器と Trakt は綺麗に分離されている。 各サービスのページ構造がどれだけ厄介でも、その痛みは上流がすでに引き受けており、その部分は送り先が誰かに依存しない。
だからこの fork の実体は「バックエンドを差し替えて、29 の抽出器を継ぐ」ことであって、書き直しではない。上流は MIT で、コードを複製するのではなく GitHub の fork を使った。コミット履歴と原作者のクレジットが残り、ライセンス関係も明確になるからだ。リンクは末尾に置いた。
過程で最も学びがあったのは、リネームで型チェックは全部通ったのに、ビルドが落ちたことだ。
リネームのスクリプトはコード側が参照するアイコンのファイル名を書き換えたが、実ファイルの名前は変わっていなかった。型チェックはアセットのパスを見ないので、バンドル時にはじめて落ちる。そこで CI に build を足した。それまでは型チェックと lint だけで、この種の問題はどちらも捕まえられない。
もう一つ、ビルドが落ちるよりずっと危険だと思うのが、静かに間違うものだ。上流にはコミュニティのデータベースから「照合の修正案」を取ってくる機能があり、その中身はすべて Trakt の ID である。Simkl に当てるとまったく無関係な作品に解決され、エラーは一切出ない。まるごと無効にした。
現在の状態#
8月4日、Netflix で映画を一本見終えた。他には何もしていない。
スケジュールが回ってきたとき、バックエンドはまず activities を一度尋ね、タイムスタンプが動いたのを見て差分だけを取得し、データベースに書き、画像を補完した。私が確認しようと思ったころには、もうページに出ていた。
管は繋がっている。
ただ一つ終わっていないことがあり、全部片付いたように書きたくはない。
「いま観ている」というリアルタイムの状態は、アニメの側しか生きていない。Trakt のリアルタイム状態はあの 377 行と一緒に消えたが、Simkl 側は調べたうえで繋がないことにした。拡張機能が API を叩くのは再生・一時停止・停止の三つの時点だけで、映画を通しで見ているあいだ、Simkl には何も届かないからだ。その「一時停止中」のデータを「いま観ている」として出せば、二日前に 1% で止めたものが表示されることになる。
正しいやり方は、Simkl を経由せず拡張機能から自分のサーバーへ直接ハートビートを送ることだ——アニメの側がまさにそうしている。ただしそれには、誰でもダウンロードできる形で公開している拡張機能の中に、自分のサーバーの認証情報を入れる必要がある。これは未解決の設計問題であって、そのまま真似して済む話ではない。
このページをそもそもどう作ったかは、こちらに書いた。
まだコメントがありません
✨ 最初のコメントを残しませんか