出発点は実は「視聴」とは関係ありませんでした。欲しかったのは Innei のようなつぶやきノートブログ——何かを見終えたら、リンクを一つ放り、一言添える。でも彼と私には重要な分岐がある:彼は手で維持し、私は面倒くさがり。彼の「見終わってからつぶやく、リンクだけ置く」あのリズムは真似できないし、したくない——私はそれが自分で更新してほしい。

そうして「手動のつぶやき」は別物へ育った:自動で「最近何を見てる?」と問うページ。私はそれを「いま何を見てる」と呼びました。

繋ぐだけに聞こえます。でもこの道で本当に私をつまずかせたのは二つ:一つ、アニメのデータ源は基本自分で造るしかなかった;二つ、小さすぎること——ポスター、同じ一枚が五通りに顔を取り違える。

十のプラットフォーム、一つのリスト#

一行もコードを書く前に、決めるべき一つ:何で追跡するか。これが思いのほか難しい、私の視聴習慣がバラバラだから——アニメは動畫瘋(バハムートのアニメ配信)、映像は Netflix・Disney+・HBO Max に散らばり、たまに YouTube・Bilibili;しかも私は大半をアプリかテレビで見て、ブラウザはめったに開かない。

思いつく限りのツールを並べて一巡評価しました:

  • TMDb:メタデータの王(ポスター、年、ジャンル、多言語タイトル)、Innei が使うのもこれ。だが「これがどんな作品か」に答えるだけで、「私が何を見たか」には答えない。だからメタデータ層にしかなれず、追跡源にはなれない。
  • Trakt:映画も、ドラマも、ライブも一度に食う——最も「視聴版 Spotify」に近く、これを選ぶ。だが残酷な前提がある——ブラウザ内の web 再生しか見えず、アプリやテレビは一切拾えない。ざっと見て、私へのカバー率は二〜三割。
  • Letterboxd:UI は Trakt より滑らか、コミュニティ感が強く、公式 CSV もある。一度は本当に採用した——が Trakt とほぼ完全に重なる(ただ映画しか取らない)と分かり、残すのは純粋に余計。
  • アニメは? Trakt のアニメ対応は貧弱。MyAnimeList、AniList(その API は確かに MAL より綺麗)も見たが、どちらも自分で手打刻が要る;そして動畫瘋は台湾の地域サービス、誰も Trakt に繋いでくれない。ならば全自動を自分で逆解析ぎゃくかいせきするほうがよく、打刻すら要らない。
  • 旧作や履歴はどう補う? ここが一番詰まる。Netflix は仏心、公式 CSV エクスポートがある(映画 95・ドラマ 78 を一気に流し込んだ);だが Disney+ も HBO もエクスポート口が一切ない——GDPR データ請求を真面目に調べたが、結論は台湾ユーザーに法的強制力なし、しかも数週間待つ;「続けて観る」をスクレイプするのは不安定でアカウント BAN の恐れも。最後は諦めた:その二つの履歴はまるごと放棄、Trakt を入れた日から先へ少しずつ積み、手で数件補うだけ。
  • もっと綺麗な解も「私が持ってない」で軒並のきなみ脱落:Plex 自前ホストの scrobble 統合が最も完璧だが Plex がない;デスクトップの trakt-scrobbler は VLC / MPV しか取らず、ブラウザ配信は食わない。

途中、AI が非常に有用なリアリティチェックを一言:あなたは本当に Trakt、AniList、Letterboxd を使ってる? 答えが「いや、でもこれから使う」なら——高確率で半年後も使わない。この言葉は後に的中し、Letterboxd は本当に切られた。

こうして塵は落ち着き、分担が明確に:

  • アニメ → 動畫瘋(自作 SDK、全自動)
  • 映画 + ドラマ → Trakt(以後の新規視聴 + ライブ)、加えて Netflix の CSV を凍結した歴史ベースに
  • すべてのリンク → 一律 TMDb(訪問者を私の私的な動畫瘋履歴ページへ導かない)
  • Letterboxd、Disney+ / HBO の履歴 → 野に放つ

選んだ源をデータフローに描くと、だいたいこう:

NOTE

三社を合わせると、ちょうど各社が一角を欠く:動畫瘋は画像をくれるが tmdb_id をくれない、Trakt は id をくれるが画像をくれない、TMDb は縦型ポスターをくれるが私は横型 banner が欲しい。後の落とし穴のほぼすべては「どこかがくれなかった一角」を補うこと——とりわけポスター。

動畫瘋 SDK を自分で逆解析する#

映像には Trakt のエコシステム一式があり、アニメにはない。私のアニメは全部バハムートの ani.gamer にあり、npm を隅々すみずみまで探した——存在するのは全部ダウンローダー、プログラムが「ユーザーが何を見たか」を取る API クライアントは一つもない。誰もやってないなら、自分で逆解析してついでに開源:anigamer という TypeScript SDK。

(後にバックエンドをまるごと Rust へ移した際、Rust 版 も揃えて足した、テストスイートは TS 版と一対一で揃えて。)

付随してブラウザ拡張「Cookie Pusher」も作った:

動畫瘋にログインして一度クリックすれば、cookie をバックエンドへ送り同期をホット更新——長い cookie を毎回 env に貼り、docker build して再起動、が本当に嫌だったから。同時にライブ検出も兼務、後の scrobbler の段で再登場します。

この線には落とし穴が少なくなく、どれも「バハムートらしい」。死んだ session に対しステータス 200、なのに body には 401を詰めたソフトエラーを返す——私の同期は静かに三日壊れ、監視は正常だと思っていた。そしてあの鍵の BAHARUNE cookie は 、拡張は最初どう読んでも非 HttpOnly の五つしか取れず——

——最後はブラウザのデバッグインターフェース(CDP)でやっと回避した。データ自体も地雷を踏んだ:最初は各アニメが「1 話」しか表示されず、タイムスタンプも同じ瞬間に固まっていた;追うと、SDK もバックエンドも両層とも「最新の一話」しか取っていない——nested history 全体を展開したら、DB は百数十行から九百数十行へ伸びた。

だがこれら——逆解析の攻防、cookie の綱引つなひき、200 に隠れた 401、SDK のゼロからリリースまで——は実は別の一本分で、単独で深く書きます。このページは、アニメのデータは自分で造ったパイプラインで引き込んだ、それだけ知っていれば十分。

汚れた履歴#

アニメの線を片付け、映像側へ。

画像が壊れたナポレオン#

「最近見てる」に NAPOLEON のポスターが一枚壊れていた。第一直感は Letterboxd を疑い、いっそ源を Trakt だけに切り詰めよう、だった。だがデータを並べて数えると、真相は印象とちょうど逆:

映画ドラマ壊れた日付(1970)ポスター欠
Netflix CSV9578 本 / 693 話00
Trakt03 本 / 151 話151(全壊)
Letterboxd1(Napoleon)001

ナポレオンは確かに Letterboxd 由来——だが**tmdb_id(753342)はあり、ただ poster_urlnull**。そして私の enrich は「tmdb_id IS NULL」のものしか補わないので、ナポレオンを一瞥もせず飛ばした。画像の破損は源が悪いのではなく、補う条件を狭く書きすぎた。直し:「tmdb_id はあるが poster_urlnull」も補うよう広げる(id で detail を直接取り、再検索しない、手滑りで tmdb_id を壊さないため)。ナポレオンの顔はその場で生え戻った。

まさにその瞬間、Letterboxd を正式に畳んだ——選定の時から Trakt との重複を嫌っていたし、今その唯一の貢献が画像破損の元凶。cron をコメントアウトし関数は残した:Trakt の web UI は本当に使いづらく、手動追加が痛いので、いつか戻りたくなるかもしれない。

全部 1970 の日付#

あの表はもう一つ地雷を隠していた:Breaking Bad、Game of Thrones、Itaewon Class の三本、日付が全部 1970-01-01。全部 Trakt 由来で、151 件の watched_at が全部 に壊れていた。根本原因はコードでなく私自身——これらは何年も前に見て、日付を本当に覚えておらず、当時 Trakt に「不明」と入れた。Trakt は「不明」を Unix epoch として保存し、通しで 1970 と表示する。

直し:cleanWatchedDate を足し、同期時に epoch / 1970 の日付を NULL として保存(1970 のふりをさせず);フロントは日付なしを「日付不明」と表示し、並び順は常に最後。Trakt へ戻って直す必要は一切ない。

劇場版は同じ作品ではない#

一部のアニメは Netflix でも動畫瘋でも見ていて、二度出る。欲しい重複排除ルールは「話数最多の行を残す」で、名前だけには頼れない——名前が近い作品や劇場版を巻き添えにするのが怖い。だから重複排除は tmdb_id で:同 id は話数最多を残す;movie と tv は名前空間が分かれ、劇場版(movie id)は生来ドラマ(tv id)と衝突しない;tmdb_id がないものはいっそ重複排除しない、重複してでも誤殺しない。名前というものは、主キーに使えば必ずいつか爆発する。

視聴中、最近見たではなく#

欲しいのは「最後に見たもの」でなく「いま視聴中」——ドラマ、映画、アニメを問わず。私のサイトには元々 Spotify の now-playing(再生なしなら 204)があり、その同じリズムで作った:

  • 映画 / ドラマ:既製の Universal Trakt Scrobbler(UTS)拡張を入れ、Netflix・Disney+・HBO の再生をライブで Trakt へ scrobble、バックエンドは Trakt の /users/{slug}/watching をポーリング。私側は一行も変えない。
  • 動畫瘋:Trakt エコシステムにないので自分で——これがあの Cookie Pusher 拡張の二つ目の仕事:animeVideo.php に掛けた content script が、<video> 再生中は 30 秒ごとに heartbeat、一時停止やタブ閉じで止まる。純 push、動畫瘋へのポーリングはゼロ。

検出は三つの安定した原語だけに頼る:URL の ?sn=(video_sn)、<video> の再生/一時停止イベント、そして document.title(バックアップのみ)。本当のタイトル・カバー・tmdb_id は壊れやすいサイト内 selector からけずらず、バックエンドが video_sn で anime_history を逆引きして補う——手で刻まずに済むなら刻まない。バックエンドはメモリ状態を一つ、90 秒 TTL;フロントはポーリングし、誰かが再生中なら「● 視聴中」を灯し、なければ正直に「最近見終わった」へ退く。

TIP

当時一番心配したのはポーリングが問題を起こさないか。だからルールをこうした:誰かが実際にこのページを開いた時だけ Trakt に問い、最大 25 秒に一度、そして動畫瘋が再生中は Trakt に一切問わない。アイドル時は 0 回;最悪でも Trakt 枠の約 1% を使う程度。

唯一私の制御外なのは、あのサードパーティ UTS 拡張自体が少し気難きむずかしいこと:たまに再生を拾わない。実測では、Netflix でトップから入って観る場合、時々入った後にその再生ページを手動で一度リロードしないと scrobble されない——別のレンダー層に切り替わって拡張が掛からなかった、というのが私の推測。正直、根本原因は本当に証せなかった、言えるのは:少なくともこうすれば、素直になる。

ライブカードの連続やらかし#

番を借り違えた顔#

ライブ取得を組んで、いさんでページを開いた。Trakt は確かにライブで拾った——文字は明白に「視聴中 100 METERS、映画」なのに、大画像は私が前に動畫瘋で見た番(転生したらスライムだった件 第 4 期)。タイトルは合ってる、顔は別人。

またあの「id をくれて画像をくれない」Trakt。/api/watch/now の Trakt 分岐は cover を直接 null にハードコードし、フロントの hero はこうポスターを選ぶ:

ts
// Watch.tsx — cover が null の時は now?.poster に fallback
poster: liveNow.cover ?? now?.poster

nowアニメ集約から出た「最近の一本」、つまりスライム。だから covernull になった途端、視聴中の映画が前の番の顔を借りる。

IMPORTANT

これは先の画像が壊れたナポレオンと、同じ落とし穴の二つの顔。一つは「id はあるが画像がない時、補う条件が狭すぎる」(静的リスト);もう一つは「画像がない時、fallback の対象を選び間違える」(ライブカード)。根は同じ一文——TMDb / Trakt が id をくれて画像をくれない時、その fallback チェーンが設計されていない、と画像は隣から借りられる。

直しはバックエンドとフロントに一刀ずつ。バックエンド:ポーリングが Trakt 視聴中を拾ったら、くれた tmdbId で TMDb からポスターを取り cover に(キャッシュ済み detail を再利用、取れなければ null)。フロント:hero ポスターを liveNow.cover ?? null に変え、now へ fallback しない:

ts
// フェイルセーフ:視聴中のポスターはそれ自身のものだけ、取れなければ空、決して他人のを借りない
poster: liveNow.cover ?? null

ぼやけて、そして切られて#

ポスターはやっとそれ自身になった。だが次は、手を変え品を変え壊してくる。

まずぼやけ——大画像は明らかにサムネの拡大。補う時 w342(342px)を取り、大 banner に詰めて三倍近く拡大していた。original に変えると naturalWidth は 342 から 1433 へ——くっきり。

くっきりしたら、次は切られる——原寸は明らかに縦型、載せると中央の一筋だけ。TMDb のポスターは縦型(1433×2048)、私は横型 banner(880×587)に詰め、 で切ると中央だけ。

直し:TMDb detail に URL も吐かせる——16:9 の横型スチル——視聴中の大画像はそれを優先、なければポスターへ退く。同じ一枚、まず番を借り違え、次にぼやけ、次に切られ、三巡直してやっと落ち着いた。

当てにならない進捗バー#

進捗バーは最初まるで動かない。endsAt を足し、クライアント補間で押し動かした;すると今度は狂う——100 METERS を見終えたばかりなのにバーは満たず、明らかにエンドロールなのに 30% くらいに見える。

本当のお化けは、Trakt の started_at / expires_at が跳ぶこと。同じ 100 METERS で、duration が 68 分に見えたり 30 分に見えたり(プレイヤーが再 scrobble)。跳ぶ起点で進捗を計算すれば、当然エンドロールで始まったばかりと思う。しゃくは Trakt に問うべきでなく、起点も跳ぶ started_at を使うべきでない。

直し:TMDb detail に runtime_min も吐かせ、本当の尺で計算(100 METERS = 106 分、安定);アンカーを expires_at に変え逆算;フロント補間を「バックエンドスナップショットの進捗をアンカーに、経過時間の delta を足す」に変え、時計ずれに免疫。実測レート 1.57×10⁻² %/秒、ちょうど 100 ÷(106×60)——完全一致。

WARNING

ここは 100% 直せず、直したふりもしない:絶対進捗は依然として Trakt の跳ぶ started_at の影響を受ける——データに「本当に何秒まで再生したか」が存在しないから。できるのはレートを正しくし、30 秒ポーリングで自己補正させること。scrobbler がくれるのは「何を見てるか」、決して「何秒目か」ではない。

消えたポスター#

最後の症状は最も基本的:画像が消え、上部の「最近見終わった」がまだ前のアニメを掛けている。見終えたばかりの 100 METERS はリストに入ったが、カチンコのプレースホルダーだけ。

二つの根本原因が重なる。

一つ、Trakt 履歴同期のあの INSERT が poster_url を全く保存しない:

sql
-- poster_url が抜けている、100 METERS がリストに入って顔なしなのも道理
INSERT OR IGNORE INTO film_history (title, watched_date, source, tmdb_id, release_year) ...

二つ、hero の「最近見終わった」が常にアニメ集約(= スライム)を使い、種別を跨いだ本当に最新の一本ではない。直し:リストの画像欠けの Trakt 映画は、tmdb_id で TMDb からライブ補填;hero は「種別を跨いだ最新の一本」を計算(視聴中 / 最近の映画 / 最近のドラマ、最新を取る)。

もう一つ「遅すぎる」落とし穴:見終えてプレイヤーを閉じた後、Trakt 側は見終わったと表示するのに、私のサイトは同期しない——履歴同期の worker が 6 時間に一度しか走らないから。直しは「視聴中 → 非視聴」の遷移(= 見終えて閉じた)を検出したら、すぐ一度追加で履歴同期を走らせる(idempotent)、約 90 秒で更新される。

合わない形#

全体を収束させると、あの繰り返す落とし穴は実に単純:三つの API が各々半分をくれ、痛みはすべて「欠けた半分を繋ぐ」ぎ目で起きる。動畫瘋は API クライアントすらなく、自分で逆解析するしかなかった;履歴を一続きくれたのに私は一マスしか取らなかった;Trakt は id をくれて画像をくれず、隣の顔を借りさせた;TMDb は縦型ポスターをくれ、私は横枠に無理に詰めた。どのバグも、底まで解けば同じ一文——源がくれる形と、あなたが欲しい形が、合わない。

あのポスターの fallback チェーンに、最後に一つルールを立てた:id があれば TMDb からライブで画像を補い、取れなければ明示的に null、正直に空けておき、決して他人の画像を借りない。画像が壊れたナポレオンと、顔を取り違えたライブカードは、同じルールの二つの書き方。

源が id をくれて画像をくれない時、隣の一枚を急いで借りるな。取り違えるより空けておくほうがいい——取り違えたポスターは、空きよりも見苦みぐるしい。