30 KiB
30 KiB
title, tags, status
| title | tags | status | ||
|---|---|---|---|---|
| tver の現状 |
|
本番稼働中 |
tver の現状
[!info] このファイルの位置づけ tver プロジェクトの現状(決定事項・要件・構成・課題)を積み上げていく。運用規約は AGENTS を参照。
今の状態
- Go の HTTP サーバー、TVer API クライアント、ドラマ・バラエティの新着表示、テスト、web・worker コンテナの定義がある
- 本番稼働中。 dockageのweb・worker、NASのPostgreSQL 17、NFS動画保存を構成し、実動画の取得・再生・Range配信まで確認済み
- リポジトリ:
https://gitea.kazu634.com/kazu634/tver.git
目的
TVer(https://tver.jp/)で公開中の動画情報を取得し、選んだ動画を yt-dlp でダウンロードし、アプリ内で再生する Web アプリを作る。
決定事項
| 項目 | 決定 |
|---|---|
| TVer 利用規約との関係 | 鑑賞を前提とするダウンロードのため、問題無しと判断(ユーザー判断) |
| 利用者・公開範囲 | 自分のみ・LAN 内だけ。EC2 nginx では公開しない |
| 保存先 | NAS(ds923p)の /volume1/video/tver。既存のanime・drama・misc・personal・varietyと分け、admin相当の書き込み範囲を限定する。/volume1/videoをdockageの/mnt/nas-tverへNFSマウントし、.envはVIDEO_HOST_DIR=/mnt/nas-tver/tverとする |
| 自動ダウンロード | 作る(番組を登録し、新しい回を自動でダウンロードする) |
| 再生 | アプリの中で再生する |
| ダウンロード | yt-dlp を使う |
| デプロイ | 読み取り専用のGiteaユーザーtver-deployでdockage VMの/opt/stacks/tverへcloneし、docker compose up -d --buildで起動する。Dockgeにも同じスタックが表示される。更新はgit pull --ff-only後に再ビルドする |
| 配置用のGit認証 | tver-deployをtverの読み取り共同作業者とし、repository読み取りトークンをdockageの権限600の専用credentialファイルに保存する。トークンの実値はGitや文書へ残さない |
| 開発言語 | Go(web・worker とも)。yt-dlp は外部コマンドとして呼び、進捗・結果は yt-dlp の出力から読み取る |
| DB | NAS上のtver専用Postgres 17コンテナ(Giteaと同じ方式)。ユーザー・DB名はtver、公開ポートは15434。dockageからの到達を確認済み |
| 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。dockageの/etc/fstabへ_netdev,nofail付きで登録し、DSMでは接続元192.168.10.155だけを許可する |
| NFS 権限 | 読み書き、Squash「すべてのユーザーをadminに割り当てる」、sys、非同期オン。DSMのadminアカウントが無効でもUID 65532から書き込み確認済み |
| DBの通信制限 | DSMファイアウォールでTCP 15434を192.168.10.155からだけ許可する |
| DBバックアップ | NASのDSMタスクスケジューラからrootで毎日実行する。tver-postgres内のpg_dumpをgzip圧縮して/volume1/backups/tver/tver_<YYYYmmdd_HHMMSS>.sql.gzへ保存し、14日より古い世代を削除する。同名の完成ファイルは上書きせず失敗させる。初回は一時DBへの復元と主要4表の行数一致まで確認する |
| 保持期間 | 削除は手動のみ。自動削除はしない |
| 待ち行列 | 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で90バイト、回タイトルは140バイト、サニタイズ後のエピソードIDは64バイトまでとし、ファイル名全体を255バイト以下に収める。64バイトを超えるIDはworkerでinvalid_outputにする。実際のTVer IDは約10文字で、yt-dlpのextractorも英数字として扱うため64バイトを上限とした。既存ファイルは改名せず、新しい保存から適用する |
| 動画ファイルの境界 | webはVIDEO_DIRをos.OpenRootで開き、DBの相対file_pathをRoot.Open・Root.Removeへ渡す。VIDEO_DIR外へのパストラバーサルと外向きシンボリックリンクをOSのディレクトリ境界で拒否する |
| 同名ファイル | 移動先に同名ファイルがあれば上書きする。ファイル名にエピソード ID が入り episode_id は一意なので、同名は同じ回の前回処理の残りに限られる。再実行で同じ結果になることを優先する |
| ライブラリ一覧 | DBの完了済みジョブから作り、NASのフォルダ走査やファイル存在確認は行わない。番組一覧、番組ごとの回一覧、再生の3段構成にする |
| 回の自然順 | 回タイトル中の半角・全角数字の並びを数値として比較し、同順位は完了日時の古い順、さらにジョブ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 | 番組(シリーズ)を登録し、新しい回を自動でダウンロードする | 登録・解除・一覧。定期的に新しい回を確認する。番組URL(tver.jp/series/…)から登録でき、登録前に確認画面を表示する |
| 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。新着一覧の取得、待ち行列への登録、番組URLを含む番組登録、動画の配信(再生) |
worker |
Go + yt-dlp + ffmpeg。待ち行列からダウンロードし、登録番組の新しい回を定期的に確認する |
- 現在の
webは/healthz、新着(/)、待ち行列(/queue)、登録番組(/subscriptions)、番組単位のライブラリ(/library)、番組ごとの回一覧(/library/series/{series_id})、再生ページ(/library/{id})、Range 対応の MP4 配信(/videos/{id})を提供する - ライブラリはDBの完了済みジョブだけを基に、番組一覧から番組ページ、再生ページへ進む。番組ページは完了が新しい順と、回タイトルの半角・全角数字を数値比較する話の順を切り替えられる。再生ページは話の順で前後の回へ移動できる
- 動画配信と削除はクライアントからジョブ ID だけを受け取り、DB の相対
file_pathをos.RootのOpen・Removeで扱う。絶対パス・..・VIDEO_DIR外を指すシンボリックリンクをディレクトリ境界で拒否する。削除はアプリからだけ行い、動画削除後に実ディレクトリの親が空なら削除する。削除後は新着から手動で再登録できるが、登録番組の既知記録は残るため自動再登録しない - 新着の「番組を登録」から、公開中の回をダウンロードするか選ぶ。web はサーバー側の新着キャッシュから番組 ID と名前を引き、TVer の season ごとの公開中エピソードを取得する。取得失敗時は登録しない
/subscriptionsではhttps://tver.jp/series/<ID>形式の番組URLを受け付け、検証した番組IDからTVer APIの接続先を組み立てる。登録前に番組名・番組ID・公開中の回数を確認し、登録時にもサーバー側で番組名と公開中の回を取り直す。クライアントから番組名は受け取らない- 登録と公開中の回の既知記録・任意の待ち行列投入は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)で指定する- NAS用の
deploy/nas/tver-db/compose.yamlはpostgres:17を15434:5432で公開し、/volume1/docker/tver-db/pgdataへ保存する。パスワードは同ディレクトリのGit管理外.envから注入する - NAS用の
backup-db.shはコンテナ内のpg_dumpを検査後にgzip圧縮し、同一ディレクトリ内のハードリンクで完成ファイルを上書きせず公開する。restore-test.shは最新ダンプを一時DBへ復元してdownload_jobs・subscriptions・known_episodes・schema_migrationsの行数を本番DBと比較する。一時DBはEXIT trapで必ず削除し、復元SQLの通常出力はログへ記録しない - 本番配置と更新の手順はdocs/deployにまとめる。dockageでは
/volume1/videoを/mnt/nas-tverへNFSマウントし、その配下のtverだけをコンテナ内の/videosへ渡す - Giteaは匿名閲覧とSSHを無効にしているため、dockageのclone・pullは読み取り専用
tver-deployのrepository読み取りトークンを使う。Gitのcredential helperはGiteaのHTTPS URLに限定する - 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})・番組名(/service/api/v2/callSeries/{series_id})・番組のエピソード一覧 |
非公開 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 を使う |
Gitea gitea.kazu634.com |
配置元 | 匿名cloneとSSHは使えない。tver-deployのHTTPSトークンでclone・pullする |
| NAS ds923p(NFS) | 動画の保存先 | /volume1/videoをdockageの/mnt/nas-tverへNFSv3でマウントし、アプリはtverサブフォルダを使う。DSMのNFS権限は192.168.10.155のみ |
| NAS ds923p(Postgres 17) | DB | tver専用コンテナを15434:5432で公開し、DSMファイアウォールで192.168.10.155のみ許可する。dockageから接続確認済み |
確認済みの事実
- 新着 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) - DSMのNFS権限を接続元
192.168.10.155、読み書き、全ユーザーをadminへ割り当て、sys、非同期オンにすると、dockageからNFSv3で/volume1/videoをマウントできた。設定直後の1回目はaccess denied、直後の2回目は成功したため、権限の反映待ちと考えられる [実機] 2026-10-04 確認 - DSMのadminアカウントが無効でも、UID
65532のコンテナからtverサブフォルダの作成・書き込み・読み込み・rename・削除に成功した。NAS上の所有者はUID1024・GID100。/mnt/nas-tver/tverは作成済み [実機] 2026-10-04 確認 - NFSマウントのファイル名上限は
namlen=255[実機] 2026-10-04 確認 - Giteaは
REQUIRE_SIGNIN_VIEW = true・DISABLE_SSH = trueで、匿名HTTPSのinfo/refsはHTTP401だった。読み取り専用ユーザーtver-deployとrepository読み取りトークンで/opt/stacks/tverへのcloneに成功した [実機] 2026-10-04 確認 - dockageで
docker compose up -d --buildを実行してweb・workerを起動でき、Dockgeにもtverスタックとして表示された。DSMファイアウォール設定後、dockageからNASのTCP15434へ到達できた [実機] 2026-10-04 確認 - 本番の
/healthzはHTTP200、新着バラエティは98件だった。4分の本編ep5hcgopp2は登録から20秒以内に完了し、92,340,468バイトの1080p H.264/AAC MP4をNASへ保存した。所有者はUID1024・GID100、.incomingは空で、/libraryとHTTP Range206も確認した [実機] 2026-10-04 確認 - NASは番組フォルダ内に所有者UID
0の@eaDirを作る。ライブラリ一覧はDBから作るため、アプリへの影響はない [実機] 2026-10-04 確認 callNewerDetail/dramaは毎回100件を返し、応答に続きや総件数を示す項目はなかった。result.contentsはtype=newerで、contentはid・version・titleのみ。page=2・offset=100・limit=200を付けても先頭・末尾IDが同じ100件となり、パラメータは無視された。100件を超えて取得する手段は見つからなかったが、非公開APIのため不可能とは断定しない。新着一覧は直近100件までとなる一方、登録番組の自動確認は番組ごとのcallSeriesSeasons→callSeasonEpisodesを使うため、新着から外れた回も取りこぼさない [実機] 2026-10-04 確認- dockage上の使い捨て
postgres:17コンテナへマイグレーション001〜003を適用してテスト用の行を投入し、backup-db.shとrestore-test.shを実行した。gzip圧縮・権限600のダンプ作成、同じSTAMPでの2回目がlnの失敗で非0終了して既存ダンプを保持すること、主要4表の行数一致、一時DBの削除、ログにpsqlの標準出力が残らないことを確認した [実機] 2026-10-04 確認 - NASの
/volume1/docker/tver-db/へスクリプトを配置し、DSMタスクスケジューラのrootタスクから実行して、/volume1/backups/tver/へtver_20261004_182838.sql.gz(2,286バイト)とtver_20261004_183256.sql.gz(2,289バイト)を作成した。ダンプはroot所有・権限600で、バックアップディレクトリはroot所有・drwxr-xr-xだった。restore-test.shをsudoで実行し、download_jobs1件・subscriptions1件・known_episodes1件・schema_migrations3件の一致、ログへの結果記録、一時DBの削除を確認した。一般ユーザーではroot所有のログへ書き込めず失敗する [実機] 2026-10-04 確認 - DSMのACLで全員書き込み可だった
/volume1/docker配下について、/volume1/docker/tver-dbをchmod 755、backup-db.shとrestore-test.shをchmod 700にしてACLが外れたことを確認した。変更後も本番アプリの/queueと/subscriptionsはHTTP200でDBを読み取れた。PostgreSQLコンテナ再起動後の動作は未確認 [実機] 2026-10-04 確認 - platform-apiの
v2/callSeries/{series_id}はresult.content.content.titleに番組名を返し、存在しないIDにはHTTP404を返した。sr8a864g0wでは番組名がエピソード側のseriesTitleと一致した。v1/callEpisode/{episode_id}はseriesID・seriesTitleを返した(今回は使わない) [実機] 2026-10-07 確認
課題
- 登録済みのDSM日次バックアップタスクが翌日以降に自動実行され、新しいダンプを作ることを確認する [要確認]
- DRM・地域制限の実例探しはTVerへの大量アクセスになるため行わず、運用中に観測する。workerは失敗時の
error_messageをDBへ保存し、既知の文言に一致しない失敗はunknownにする。実例が出た時点で分類を見直す
経緯
- ステップ12でライブラリを番組一覧、番組ページ、再生ページの3段構成にした。番組単位の集計と登録状態はDBから取得し、番組ページの話の順と再生ページの前後移動には同じ自然順を使う。動画削除後は空になった番組フォルダだけを
os.Root経由で削除する
確かめ方
- 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-postgresv1.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 にアクセスしない - 番組情報の応答は
internal/tver/testdata/series.json(2026-10-07の実応答を番組情報だけに縮小)に記録した。番組URLは純関数でscheme・host・path・IDを検証し、TVerクライアントのテストはhttptestだけへ接続する - 新着画面のテストは偽物の TVer クライアント・store と差し替え可能な時計を使い、表示項目、絞り込みの境界、10 分キャッシュ、取得失敗、JST 表示、登録・状態表示・待ち行列の操作を確認する
- ライブラリのテストは番組単位の集計、番組別の回取得、3画面、半角・全角数字の自然順、前後移動を確認する。一時ディレクトリの小さな動画を使い、Rangeの206・範囲外の416、
os.Rootによる相対パスとシンボリックリンクの拒否、404、ファイル削除後の記録削除、空の番組フォルダだけを削除することも確認する。共有のファイル名規則はinternal/mediaで90・140・255バイトの境界とUTF-8の完全性をテストする - worker のテストは
internal/worker/testdataの偽 yt-dlp・ffprobe を使い、本物の TVer へ接続せずに正常完了、失敗分類、取消、終了時の待機復帰、進捗の間引き、heartbeat、出力検査、移動先、作業場所の片付けを確認する - 登録番組の定期確認は偽の TVer クライアントと差し替え可能な時計・タイマーを使い、24時間の境界、順次確認、1時間走査、失敗記録、予告の既知化、終了時の中断を確認する。store の抽出・既知化・投入・日時更新・エラー消去は embedded Postgres 17 で確認する
- NAS用Composeは
deploy/nas/tver-db/compose.yamlをYAMLパーサーで読み、services.postgres、環境変数、ポート、保存先、healthcheckを確認する。Dockerが使える環境ではdocker compose --env-file deploy/nas/tver-db/.env.example -f deploy/nas/tver-db/compose.yaml configでも展開結果を確認する - DBバックアップスクリプトは
deploy/nas/tver-db/scripts_test.goからPATH上の偽dockerを使って実行し、正常な圧縮、同名・同時実行時の上書き防止、空ダンプとpg_dump失敗、14日の世代境界、復元失敗時の一時DB削除、主要表の行数不一致を確認する。NAS実機ではdocs/deploy#5. PostgreSQLをバックアップするに従って手動バックアップと復元確認を行う - 本番の確認手順はdocs/deployに従い、NFS上の
/mnt/nas-tver/tverをUID65532で読み書き・rename・削除できること、DBがhealthyであること、webの/healthz、実動画の保存・再生・シークを確認する 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 への実アクセスを伴うため手動確認は少数回に留める