Files
tver/PROJECT.md
T

20 KiB
Raw Blame History

title, tags, status
title tags status
tver の現状
homelab
tver
worker の登録番組確認を実装中

tver の現状

[!info] このファイルの位置づけ tver プロジェクトの現状(決定事項・要件・構成・課題)を積み上げていく。運用規約は AGENTS を参照。

今の状態

  • Go の HTTP サーバー、TVer API クライアント、ドラマ・バラエティの新着表示、テスト、web・worker コンテナの定義がある
  • worker のダウンロード本体と登録番組の定期確認を実装中。 待ち行列からの取得に加え、有効な登録番組の新しい回を確認して自動登録する
  • リポジトリ: https://gitea.kazu634.com/kazu634/tver.git

目的

TVer(https://tver.jp/)で公開中の動画情報を取得し、選んだ動画を yt-dlp でダウンロードし、アプリ内で再生する Web アプリを作る。

決定事項

項目 決定
TVer 利用規約との関係 鑑賞を前提とするダウンロードのため、問題無しと判断(ユーザー判断)
利用者・公開範囲 自分のみ・LAN 内だけ。EC2 nginx では公開しない
保存先 NAS(ds923p)
自動ダウンロード 作る(番組を登録し、新しい回を自動でダウンロードする)
再生 アプリの中で再生する
ダウンロード yt-dlp を使う
デプロイ Dockge で管理する複数コンテナ構成。dockage VM の /opt/stacks/tver
開発言語 Go(web・worker とも)。yt-dlp は外部コマンドとして呼び、進捗・結果は yt-dlp の出力から読み取る
DB NAS 上の tver 専用 Postgres 17 コンテナ(Gitea と同じ方式)。バックアップは規約どおり pg_dump → NAS。公開ポートは未定(候補 15434、実機で確認する)
DB ドライバ github.com/jackc/pgx/v5 の pgxpool
DB テスト github.com/fergusstrange/embedded-postgres v1.34.0 の Postgres 17 を起動する。Apple Silicon では Rosetta 2 が必要
定期実行 worker 内のスケジューラ
NAS のマウント NFS。DSM で 192.168.10.155 のみ許可する
保持期間 削除は手動のみ。自動削除はしない
待ち行列 Postgres のテーブルだけで実装する。専用のキュー基盤は持たない
定期確認の間隔 登録番組の新しい回を 24 時間ごとに確認する
定期確認の走査 worker のスケジューラが起動直後と1時間ごとに、最終確認から24時間以上経過した有効な番組を探す。番組は1件ずつ確認し、間に数秒空ける
定期確認の予告 180秒以下の回は自動登録せず、既知としてだけ記録する。新着画面の予告判定と同じしきい値を使う
定期確認の失敗 last_checked_at を更新せず、失敗日時と秘匿値を伏せた短いメッセージを保存する。次の1時間走査で再試行し、成功時にエラーを消す
登録時の既存回 登録の都度「公開中の回もダウンロードする」かを選ぶ。選ばない場合も公開中の回を既知として記録する
番組の一時停止 登録は残し、自動ダウンロードだけ止める。再開後は停止中に公開された未確認の回も対象にする
動画のマウント先 web と worker は VIDEO_DIR(既定 /videos)に同じ NAS 共有をマウントする。download_jobs.file_path は VIDEO_DIR からの相対パスを保存する
ファイル名 <番組名>/<番組名> <回タイトル> [<エピソードID>].mp4。禁止文字と制御文字を _ に置換し、空の要素は 名称未設定、各入力要素は UTF-8 で60バイトまでに切り詰める。エピソード ID で同名衝突を避ける
同名ファイル 移動先に同名ファイルがあれば上書きする。ファイル名にエピソード ID が入り episode_id は一意なので、同名は同じ回の前回処理の残りに限られる。再実行で同じ結果になることを優先する
動画の削除 ファイルを先に削除してからジョブ記録を削除する。ファイルが既に無い場合も記録は削除する。登録番組の known_episodes は残す
同時ダウンロード数 既定は 1。WORKER_CONCURRENCY で 2まで増やせる
ダウンロードの再試行 自動では再試行しない。失敗を failed で残し、画面から手動で再試行する
実行中の取り消し web で running を canceled にし、worker が検知して yt-dlp のプロセスグループを停止する
yt-dlp の作業場所 VIDEO_DIR/.incoming/<job id>/ に %(id)s.%(ext)s で保存し、検査後に Go が media.RelativeVideoPath の場所へ同一ファイルシステム内で移動する。終了状態にかかわらず作業場所を片付ける

要件

機能要件

# 要件 補足
F1 ドラマ・バラエティの新着一覧を表示する 番組名・回タイトル・放送局・長さ・配信終了日時・サムネイル
F2 一覧を絞り込む 予告(「60秒PR」等)を除外できる。ダウンロード済み・待ち行列中を区別して表示する
F3 選んだ動画をダウンロードの待ち行列に入れる 複数選択できる
F4 ダウンロードの進捗・結果を表示する 失敗理由(DRM・地域制限・TVer 側の変更による抽出失敗)を区別して表示する
F5 同じ回を二重にダウンロードしない 手動・自動の両方で共通
F6 番組(シリーズ)を登録し、新しい回を自動でダウンロードする 登録・解除・一覧。定期的に新しい回を確認する
F7 ダウンロード済みの動画をアプリ内で再生する シーク(HTTP Range)に対応する
F8 ダウンロード済みの動画を一覧・削除する NAS の容量管理のため
F9 保存ファイルの名前と置き場所を規則で決める 例: <番組名>/<番組名> <回タイトル> [<エピソードID>].mp4

非機能要件

  • yt-dlp を素早く更新できること。 TVer 側の変更で抽出が壊れやすいので、worker のイメージを再ビルドするだけで更新できるようにする
  • TVer への負荷を抑える。 一覧取得はキャッシュし、同時ダウンロード数は 1〜2 に制限する
  • ブラウザで再生できる形式で保存する。 映像 H.264 + 音声を MP4 に結合する(ffmpeg が必要)
  • 認証は持たない(LAN 内限定のため)
  • homelab の規約に従う(homelab/docs/conventions): Compose v2、秘匿情報は .env、TZ=Asia/Tokyo

構成(案)

コンテナ 役割
web UI・API。新着一覧の取得、待ち行列への登録、番組登録、動画の配信(再生)
worker Go + yt-dlp + ffmpeg。待ち行列からダウンロードし、登録番組の新しい回を定期的に確認する
  • 現在の web は /healthz、新着(/)、待ち行列(/queue)、登録番組(/subscriptions)、完了済みライブラリ(/library)、再生ページ(/library/{id})、Range 対応の MP4 配信(/videos/{id})を提供する
  • 動画配信と削除はクライアントからジョブ ID だけを受け取り、DB の相対 file_path を使う。絶対パス・..・VIDEO_DIR 外を指すシンボリックリンクを拒否する。削除後は新着から手動で再登録できるが、登録番組の既知記録は残るため自動再登録しない
  • 新着の「番組を登録」から、公開中の回をダウンロードするか選ぶ。web はサーバー側の新着キャッシュから番組 ID と名前を引き、TVer の season ごとの公開中エピソードを取得する。取得失敗時は登録しない
  • 登録と公開中の回の既知記録・任意の待ち行列投入は1トランザクションで行い、登録時刻を最終確認日時に設定する。known_episodes は番組解除時に連動して削除する。一時停止中は投入を拒否する
  • worker のスケジューラは起動直後と1時間ごとに、enabled かつ最終確認から24時間以上の番組を抽出する。TVer から公開中の回を順次取得し、未確認の180秒超の回だけを source=subscription で投入する。180秒以下も既知にし、成功時の最終確認更新・エラー消去まで1トランザクションで行う
  • TVer の番組取得に失敗した場合は最終確認日時を変えず、失敗日時と伏字済みの短いメッセージを subscriptions に記録して画面に表示する。番組間には3秒空け、SIGTERM では取得中または待機中の確認を中断する
  • 新着の登録ボタンはエピソード ID とジャンルだけを送る。web が有効な新着キャッシュから番組情報を引き、episode_id の一意制約で重複登録を防ぐ。新着一覧はジョブ状態を一括取得し、失敗・取り消しには再試行を表示する
  • 待ち行列は実行中・待機中・失敗・取り消し・完了を状態別に直近50件ずつ表示する。段階別の進捗と失敗理由を表示し、待機中の取り消しと失敗・取り消しの再試行ができる。実行中または待機中のジョブがある間だけ5秒ごとに一覧を更新する
  • 実行中のジョブにも取り消しボタンを表示する。worker は DB の canceled を定期確認し、yt-dlp をプロセスグループごと停止する
  • 新着はジャンルごとにメモリへ 10 分間キャッシュする。キャッシュ中は TVer セッションの再発行と一覧の再取得を行わない。TVer の取得に失敗した場合、一覧の場所にエラーを表示して詳細をログへ記録する
  • 「予告を隠す」は既定でオンとし、180 秒以下の件を非表示にする。長さは秒から分へ切り上げて表示し、配信終了日時は Asia/Tokyo で表示する。ジャンルと予告表示の状態は URL に残す
  • 画面は html/template と htmx 2.0.11 を使い、htmx は Go の embed から /static/ でローカル配信する
  • compose.yaml は web と worker を定義する。web は dockage VM の LAN アドレス 192.168.10.155:8080 で公開し、待ち受けは WEB_ADDR で変更できる。両方へ DATABASE_URL と同じ動画ボリュームを渡し、worker の並行数は WORKER_CONCURRENCY(既定1、最大2)で指定する
  • DB は NAS 上に tver 専用 Postgres 17 コンテナを置く。internal/store の埋め込み SQL を schema_migrations で管理し、pg_advisory_lock で複数プロセスからの同時適用を直列化する。web は起動時に pgxpool で接続してマイグレーションを適用する
  • download_jobs は episode_id を一意にして二重登録を防ぎ、ジョブの状態・段階別進捗・結果を持つ。subscriptions はシリーズ単位の登録と最終確認日時を持つ。worker からの操作と画面用の一覧取得を internal/store にまとめる
  • worker は yt-dlp[default] と ffmpeg を外部コマンドとして使い、映像・音声・結合・移動の段階別進捗を1秒間隔に間引いて保存する。独立した heartbeat を5秒ごとに更新し、正確な全体パーセントは保存しない。after_move:filepath で実パスを取り、ffprobe で H.264 と AAC の MP4 を確認して成功とする。失敗は取り消し、DRM、地域制限、ネットワーク、配信不可、抽出失敗、出力不正、その他の順に扱い、自動再試行しない
  • worker は起動時にマイグレーションと停止中ジョブの RequeueStale を行い、登録番組の確認も直ちに1回行う。SIGTERM では yt-dlp と確認処理を止め、担当中ジョブを queued に戻してから終了する。Dockerfile.worker は yt-dlp 2026.08.19 を ARG で固定し、ffmpeg・ffprobe とともに非 root で実行する
  • 保存先の NAS 共有フォルダは dockage VM に NFS でマウントし、worker(書き込み)と web(読み取り・削除)に同じ VIDEO_DIR でバインドマウントする

外部とのつながり

相手 用途 備考
TVer platform-api.tver.jp 新着一覧(/service/api/v1/callNewerDetail/{drama|variety})・番組のエピソード一覧 非公開 API。セッションは /v2/api/platform_users/browser/create で発行する(yt-dlp と同じ方式)
TVer service-api.tver.jp 番組の season 一覧(/api/v1/callSeriesSeasons/{series_id}) 非公開 API。yt-dlp の TVer extractor と同じ取得方式
yt-dlp ダウンロード TVer extractor を使う
NAS ds923p(NFS) 動画の保存先 DSM で NFS を有効化し、共有フォルダを 192.168.10.155 のみに許可する
NAS ds923p(Postgres 17) DB tver 専用コンテナを使う。DSM 内蔵の Postgres が 5432 を使うため別ポートで公開し、DSM ファイアウォールで 192.168.10.155 のみ許可する。公開ポートは実機で決める

確認済みの事実

  • 新着 API は drama / variety それぞれ 100 件を返す。予告動画も含まれる。各件に id・seriesID・seriesTitle・title・broadcasterName・duration・endAt・thumbnailPath がある [実機] 2026-10-03 確認
  • 番組 sr0lhdrtdb の callSeriesSeasons は result.contents に type=season の1件(ss3i6njoq3)を返し、callSeasonEpisodes は type=episode の5件を返した。エピソードには新着と同じ id・seriesID・seriesTitle・title・broadcasterName・duration・endAt・thumbnailPath が含まれる。他の番組での season 数や一覧件数は未確認 [実機] 2026-10-03 確認
  • dockage VM から TVer API に到達でき(HTTP 200)、送信元は日本と判定される [実機] 2026-10-03 確認
  • yt-dlp 2026.08.19 で TVer のエピソードを抽出できる(--simulate)。映像(H.264 / avc1)と音声が別ストリームで、結合に ffmpeg が要る。1080p・約 50 分で約 1.2GiB [実機] 2026-10-03 確認
  • 5 分の epq432nee4 を yt-dlp 2026.08.19 で取得した。--newline と --progress-template の 1 行 1 JSON は Go の外部プロセスから逐次読める。ただし uvx yt-dlp のみで pycryptodomex が無いと AES-128 HLS を ffmpeg に委譲し、途中のダウンロード進捗が出なかった。yt-dlp[default] に含まれる pycryptodomex を加えた試行では途中の進捗が出た [実機] 2026-10-03 確認
  • 別ストリームの映像と音声は進捗が個別に始まり、format_id で区別できる。結合処理は postprocess: の出力で検知でき、--print after_move:filepath で実ファイルのパスを取得した。完成した MP4 は 24,613,787 バイトで、ffprobe では H.264 640×360 と AAC LC だった。初回取得は約 10 秒だった [実機] 2026-10-03 確認
  • SIGTERM で止めると .part と .ytdl が残り、再実行で 437,066 バイトから 438,090 バイトへ続きから取得できた [実機] 2026-10-03 確認
  • 到達不能のプロキシ経由の通信失敗と存在しないエピソードは、ともに yt-dlp の終了コード 1 だった。stderr の文言で分類が必要。DRM・地域制限の失敗文言と終了コードは実測していない(yt-dlp の extractor ソースのみ確認) [実機] 2026-10-03 確認
  • yt-dlp の TVer extractor は日本のみ(_GEO_COUNTRIES = ['JP'])。DRM 付きの作品は取得できない [公式] https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/tver.py (取得日: 2026-10-03)
  • dockage VM に NAS のマウントは無い。ルートの空きは約 20GB [実機] 2026-10-03 確認

課題

  • 新着 API が 100 件で打ち切りか、続きを取得できるか [要確認]
  • 新着のうち DRM 付きの作品の割合 [要確認]
  • 非公開 API と yt-dlp の TVer 対応は、TVer 側の変更で予告なく壊れうる。壊れたことを検知する手段が要る
  • NAS の準備(NFS 共有フォルダ、tver 専用 Postgres 17 コンテナ、DSM ファイアウォール)は DSM での手作業になる。DB の公開ポート(候補 15434)は本番前に実機で確認する [要確認]
  • DRM・地域制限の yt-dlp 失敗時の文言と終了コードは未実測 [要確認]

確かめ方

  • Codex では go test -count=1 ./... を環境変数の前置なしで実行する。go test を許可する rules に一致させ、Postgres の共有メモリ作成に必要な権限で実行するため。初回のモジュール取得にはネットワーク接続が必要
  • web を起動するには .env.example を参考に、Git 管理外の .env に実際の DATABASE_URL と VIDEO_HOST_DIR を設定する。VIDEO_DIR の既定は /videos。DB 接続がある環境では起動時にマイグレーションが適用される
  • DB テストは embedded-postgres v1.34.0 が Postgres 17 を一度起動し、テストごとに独立した DB を作る。バイナリキャッシュ、実行ファイル、データは /private/tmp 配下に置く。Apple Silicon での実行には Rosetta 2 の導入が必要
  • TVer 応答のテストは internal/tver/testdata/newer_drama.json(2026-10-03 の実応答を 3 件に縮小)と httptest を使い、テストから本物の TVer にはアクセスしない
  • 番組一覧の応答は internal/tver/testdata/series_seasons.json と season_episodes.json(2026-10-03 の実応答を各1件に縮小)に記録した。テストは httptest を使い、実 API にアクセスしない
  • 新着画面のテストは偽物の TVer クライアント・store と差し替え可能な時計を使い、表示項目、絞り込みの境界、10 分キャッシュ、取得失敗、JST 表示、登録・状態表示・待ち行列の操作を確認する
  • ライブラリのテストは一時ディレクトリの小さな動画を使い、Range の 206・範囲外の 416、相対パスとシンボリックリンクの検査、404、ファイル削除後の記録削除を確認する。共有のファイル名規則は internal/media でテストする
  • worker のテストは internal/worker/testdata の偽 yt-dlp・ffprobe を使い、本物の TVer へ接続せずに正常完了、失敗分類、取消、終了時の待機復帰、進捗の間引き、heartbeat、出力検査、移動先、作業場所の片付けを確認する
  • 登録番組の定期確認は偽の TVer クライアントと差し替え可能な時計・タイマーを使い、24時間の境界、順次確認、1時間走査、失敗記録、予告の既知化、終了時の中断を確認する。store の抽出・既知化・投入・日時更新・エラー消去は embedded Postgres 17 で確認する
  • env GOPATH=/private/tmp/tver-gopath GOCACHE=/private/tmp/tver-go-cache GOMODCACHE=/private/tmp/tver-go-modcache GOPROXY=off go vet ./... で静的解析する
  • DATABASE_URL、VIDEO_DIR、WEB_ADDR=127.0.0.1:18080 を設定して go run ./cmd/web で web を起動できる。worker は同じ DATABASE_URL と VIDEO_DIR を設定して go run ./cmd/worker で起動する。worker の実行は TVer への実アクセスを伴うため手動確認は少数回に留める