バッチ設計書
最終更新: 2026-09-29
概要
MorningStatusApp は、最新情報を収集するために定期的に実行するバッチと、状況に応じて手動実行するバッチを GitHub Actions で運用している。これに加えて、GitHub Actions を介さず scripts/console/・scripts/patch/ 配下で手動実行するスクリプト群がある。
ワークフロー一覧
| バッチ名 | スクリプト | ワークフロー |
|---|---|---|
| メンバー近況同期 | scripts/workflow/sync-status.ts |
.github/workflows/sync-members.yml |
| ディスコグラフィー同期 | scripts/workflow/sync-discography.ts |
.github/workflows/sync-discography.yml |
| プレイリストリンク同期 | scripts/workflow/sync-playlist-links.ts 他 |
.github/workflows/sync-playlist-links.yml |
| Genius リンク同期 | scripts/workflow/sync-genius-links.ts |
.github/workflows/sync-genius-links.yml |
| Instagram 投稿収集 | scripts/workflow/collect-instagram-posts.ts |
.github/workflows/collect-instagram-posts.yml |
| TikTok 投稿収集(公式+OG) | scripts/workflow/collect-tiktok-posts.ts, scripts/workflow/collect-tiktok-og.ts |
.github/workflows/collect-tiktok.yml(1ワークフロー内で公式→OGの順に実行、#1546) |
| モーニング女学院放送データ収集 | scripts/workflow/collect-onair-morning.ts |
.github/workflows/collect-onair-morning.yml |
| ヤングタウン土曜日収集 | scripts/workflow/collect-onair-yando.ts |
.github/workflows/collect-onair-yando.yml |
| メイボンソワ選曲データ収集 | scripts/workflow/collect-bonsoir-songs.ts |
.github/workflows/collect-bonsoir-songs.yml |
| デイリーダイジェスト配信 | scripts/workflow/send-daily-digest.ts |
.github/workflows/send-daily-digest.yml |
| イベント同期 | scripts/workflow/sync-events.ts |
.github/workflows/sync-events.yml |
| YouTube 投稿収集(公式+OG) | scripts/workflow/collect-youtube-official.ts, scripts/workflow/collect-youtube-og.ts |
.github/workflows/collect-youtube.yml(1ワークフロー内で公式→OGの順に実行、#1546) |
| メンバー関係性マップ 動的エッジ同期 | scripts/workflow/sync-relationships.ts |
.github/workflows/sync-events.yml(イベント同期の後続ステップ) |
| Blob URLシークレット診断 | scripts/workflow/check-blob-secrets.ts |
.github/workflows/check-blob-secrets.yml |
それ以外のスクリプト一覧
scripts/console/・scripts/patch/ 配下のスクリプト。フル仕様(O1〜O4)で記載する4件を除き、それ以外のスクリプト セクションの一覧表に概要のみ記載する。
| スクリプト | 概要 | 分類 |
|---|---|---|
scripts/patch/patch-delete-non-song-tracks.ts |
非楽曲トラック削除パッチ(詳細: O2、#1057) | 一回限りパッチ(実行済) |
scripts/console/seed-member-relationships.ts |
メンバー関係性マップ 静的エッジシード(詳細: O3) | 定常運用 |
scripts/console/register-tour-festival.ts |
フェス・ツアー登録バッチ(詳細: O4) | 定常運用 |
ワークフロー
W1. メンバー近況同期バッチ
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | 毎日 JST 23:03(UTC 14:03) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
実行時刻は、収集系ワークフロー全体(W1・W4・W5・W7・W14)を対象にAmeba RSS実データを実測した結果に基づき、旧 JST 20:00 から後ろ倒しした。旧時刻(20:00)時点ではその日の投稿の8.8%しか投稿済でなく、大半が翌日の収集に持ち越されていた。新時刻(23:00)時点では96.9%が投稿済となり、翌日持ち越しは3.1%まで縮小する(詳細は#1546)。
OGメンバーの同期は
OG_SYNC_WEEKDAYS環境変数で実行曜日を制限できる。 デフォルト(空文字・未設定)は月曜のみ(曜日番号:1)。 曜日判定(shouldSyncOG)はscripts/lib/og-sync.tsの共通モジュールで、 YouTube 投稿収集バッチ(W7)のOG収集部分と共用している(#1104)。公式SNS未登録(または全 active:false)のメンバーに対する Exa 一般Web検索は、
WEB_SEARCH_SYNC_WEEKDAYS環境変数で実行曜日を制限できる。 デフォルト(空文字・未設定)は月曜のみ(曜日番号:1)。 曜日判定(shouldSearchWebForNoSns)はscripts/lib/web-search-schedule.tsに実装している(#1307)。
処理フロー
Ameba RSS 解析(#1328)
officialSns に active な Ameba Blog エントリを持つメンバーは、Exa 検索を使わず Ameba の RSS フィードから直接近況を取得する(エンドポイント: https://rssblog.ameba.jp/{amebaId}/rss20.xml)。
RSS抽出仕様(CDATA改行対応・content=タイトルのみ格納・グループブログの誤帰属防止・新着蓄積・重複防止・pubDate変換等)の詳細は「Amebaブログ機能 データ設計書」§「RSS取得仕様」を参照(#1367)。
検索エンジン
- Exa Search API(
exa-jsSDK) SEARCH_API_KEY未設定の場合はドライラン(検索なし・Blob 書き込みなし)
Exa Search API の料金体系(#1307)
- 従量課金制: Search 約 $7 / 1,000 リクエスト(出典: https://exa.ai/pricing)
- 新規登録時に $10 分の無料クレジットを付与(一度のみ、出典: https://exa.ai/docs/reference/billing)
- 支払い方法登録済の場合、毎月 $7 分の無料クレジットを付与(当月末で失効・繰越不可、出典: https://exa.ai/docs/reference/billing)
2026-07-10 にクレジット枯渇で検索が失敗する事象が発生したため、公式SNS登録状況に応じて検索範囲を排他化し、 公式SNS未登録メンバーへの一般Web検索を
WEB_SEARCH_SYNC_WEEKDAYSで週1回に制限している(#1307)。
日本語判定
ひらがな(U+3040-U+309F)の出現率が HIRAGANA_THRESHOLD(0.08 = 8%)以上の場合に日本語と判定。
日本語でないコンテンツは statusHistory に蓄積しない。
SNS チェック(snsCheck)の更新ロジック
| 条件 | status | hasRecentUpdate |
|---|---|---|
| SNS 検索成功 + 公式 SNS ソースあり | ok | true |
| SNS 検索成功 + 公式 SNS ソースなし / 更新なし | ok | false |
| officialSns 未設定 + Web 検索でコンテンツあり | skipped | true |
| officialSns 未設定 + Web 検索でコンテンツなし | skipped | false |
| officialSns 全 active:false + Web 検索でコンテンツあり | skipped | true |
| officialSns 全 active:false + Web 検索でコンテンツなし | skipped | false |
| snsCheck データなし(既存データ) | unknown | false |
注意: バッチは
active: falseへの自動変更を行わない(#326)。SNS 検索で日本語コンテンツが取れなかった場合は「更新なし」として扱い、activeは変更しない。
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
SEARCH_API_KEY |
○ | Exa Search API キー(未設定でドライラン) |
BLOB_READ_WRITE_TOKEN |
○ | Vercel Blob 読み書きトークン |
MEMBERS_BLOB_URL |
○ | Vercel Blob のメンバーデータ URL |
OG_SYNC_WEEKDAYS |
- | OG 同期する曜日番号(カンマ区切り、デフォルト: 1)。Repository variable(非機密な設定値のため、#1347) |
WEB_SEARCH_SYNC_WEEKDAYS |
- | 公式SNS未登録メンバーの一般Web検索(Exa)を実行する曜日番号(カンマ区切り、デフォルト: 1)(#1307)。Repository variable(非機密な設定値のため、#1347) |
GITHUB_EVENT_NAME |
○ | GitHub Actions がすべてのステップに自動的に注入するデフォルト環境変数で、トリガー種別(schedule/workflow_dispatch等)を表す。Ameba同期ステータスの定期実行判定に使用(#1387)。ワークフロー側での明示的な受け渡しは不要 |
注意: 秘匿すべき値(APIキー・トークン等)は Repository secrets に統一すること(Environment secrets は
environment:指定がないと注入されない)。曜日番号等の非機密な設定値は Repository variables に登録する(#1347)。
Vercel Blob への書き込み
await put(MEMBERS_BLOB_FILENAME, JSON.stringify({ members }), {
access: 'public',
addRandomSuffix: false, // 固定 URL での上書き保存
allowOverwrite: true, // 既存ファイルを上書き
cacheControlMaxAge: 60, // CDNエッジキャッシュを最小値に短縮(未指定だとデフォルト1ヶ月キャッシュされる、#1442)
token: process.env.BLOB_READ_WRITE_TOKEN,
});
members.json・デイリーダイジェスト(daily-digest/{日付}/blog.json。データ構造はW14参照)に続けて、Topページ新着投稿セクションのAmeba新着ウィンドウ判定に使う ameba/sync-status.json も書き込む。詳細(AmebaSyncStatus型・定期実行/手動実行での更新ロジック)は「Amebaブログ機能 データ設計書」§「新着表示の判定基準」を参照(#1387)。
W2. ディスコグラフィー同期バッチ
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | なし(手動実行のみ) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能(refetch_artist_credits 入力に 1 を指定すると artist 未設定トラックを強制再取得) |
処理フロー
1. Vercel Blob(`UNITS_BLOB_URL`)からユニット一覧を読み込み、MBID を持つユニットの重複排除済 MBID 一覧を取得
→ MBID を持つユニットが0件の場合は処理を中断
2. Neon DB から現在のリリースデータを取得(`getReleases()`、フェイルセーフ)
3. 各 MBID に対して MusicBrainz API からリリースグループを取得し、結果を結合
→ 全 MBID で取得0件の場合は既存データを維持して終了
→ 取得したリリースタイトルは記号表記(タイポグラフィハイフン U+2010 等・三点リーダー `…`)を ASCII 表記に正規化してから保持する(`normalizeTitlePunctuation`、#1553, #1594)
4. リリースデータを正規化・重複排除
5. トラックリストを付与(既存キャッシュ再利用 / MusicBrainz から新規取得)
→ artist-credit から artist・artistMbid を取得
→ 取得したトラックタイトルも 3 と同じ `normalizeTitlePunctuation` で正規化してから保持する(#1553, #1594)
→ REFETCH_MISSING_ARTIST_CREDITS=1 の場合は artist 未設定トラックを持つリリースを強制再取得
→ 非楽曲トラックの自動除外(isNonSongTrack): recording.video=true の映像トラック、インスト・カラオケ・MC・Opening・VTR 等
→ シングル(format: 'single')は全エディション(Type A・Type B・限定盤等)のトラックを取得して recording UUID でマージし、
エディション固有のカップリング曲(例: Nature is good!)が漏れなく取り込まれるようにする
→ アルバム・ライブアルバム等は最初のエディションのみ取得(#1058 で調査・判断予定)
6. Track.artistMbid / Track.artist をもとにユニット照合(assignUnitIds)
→ 照合優先度: MBID 一致 → 名前・年ベース(フォールバック)
7. 全トラックが同一 unitId のリリースを Unit.releaseIds に自動集約(assignReleaseIdsToUnits)
8. Track 単位の YouTube リンクを付与(`YOUTUBE_API_KEY` 設定時のみ)
→ プレイリスト掲載曲 ID を取得し、優先処理対象として `syncTrackLinks` に渡す
→ `syncTrackLinks`: YouTube MV URL(`youtube`)・YouTube Music URL(`youtube-music`)を track に付与
→ `syncTrackShortLinks`: シングル曲の YouTube Shorts URL(`youtube-short`)を付与(#774)
→ 1 回の実行あたりの上限: `YOUTUBE_MAX_CALLS_PER_RUN`(100 件)
→ リリース単位の YouTube MV URL 取得は廃止(Neon に保存先がなくクォータを空振りするため、#1123)
9. data/inputs/manual-releases.json から手動登録リリースを読み込み、MusicBrainz 取得分とマージ(mergeManualReleases)
→ ID 重複チェックを行い、既存リリースと ID が衝突する手動登録はスキップ
10. リリースデータ(MusicBrainz 取得分 + 手動登録分)を Neon DB に書き込み(`saveDiscographyToNeon`)
→ `release` テーブル: `createMany(skipDuplicates: true)`
→ `track` テーブル: `upsert` per track
→ `track_link` テーブル: `deleteMany` + `createMany` per track
11. units.json(releaseIds 更新済)を Vercel Blob(`UNITS_BLOB_URL`)に書き戻し
12. 同期ステータス(`discography-status.json`)を Vercel Blob に書き戻し
→ リリース件数・YouTube 取得対象数・取得済数・Shorts 取得対象数・Shorts 取得済数を日次スナップショットとして保存(最大 30 件)
MusicBrainz API
- エンドポイント:
https://musicbrainz.org/ws/2/ - 対象: Vercel Blob 上のユニット一覧の
mbidフィールドを持つ全ユニット(重複排除済 MBID 一覧) - レート制限: 秒間1リクエスト未満に制御(429 エラー時は指数バックオフで再試行)
- トラックリスト取得時に
inc=recordings+artist-creditsを指定し、Track.artistおよびTrack.artistMbidを取得 recording.videoフラグがtrueのトラックは映像として除外(#1055)- シングルはリリースグループ内の全リリース ID を取得し、全エディションをマージする(#1055)
ユニット照合(assignUnitIds)
各トラックの artist / artistMbid をもとに units.json と照合し Track.unitId を設定する。
| 優先順位 | 照合条件 |
|---|---|
| 1 | Track.artistMbid === Unit.mbid(MBID 一致・確実) |
| 2 | 名前完全一致 / ベース名 + 活動期間での絞り込み(フォールバック) |
既存の unitId(手動設定)は上書きしない。
releaseIds 自動集約(assignReleaseIdsToUnits)
全トラックが同一 unitId を持つリリース(コンピレーション等の混在リリースを除く)を、
対応するユニットの releaseIds に自動追加する。既存の releaseIds は保持し重複は除去する。
リリース正規化・重複排除
releaseDateの空白・ゼロ詰め正規化(normalizeDate())- タイトル + 正規化リリース日をキーとした重複排除(
deduplicateReleases()) - タイトルは取得時点(
lib/musicbrainz.ts)でnormalizeTitlePunctuation済のため、重複排除キーも正規化後のタイトルを用いる(#1553)。手動登録リリースの MusicBrainz ID 洗い替え(reconcile-manual-releases.ts)でも同じ正規化関数でタイトルを比較する → リリース データ設計書 §4-4
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列(未設定でドライラン) |
BLOB_READ_WRITE_TOKEN |
○ | Vercel Blob 読み書きトークン(units.json・同期ステータス書き込みに必要) |
DISCOGRAPHY_STATUS_BLOB_URL |
○ | Vercel Blob の同期ステータス保存先 URL(/sync-status ページで参照) |
YOUTUBE_API_KEY |
- | YouTube Data API v3 キー(未設定の場合は YouTube リンク同期をスキップ) |
PLAYLISTS_BLOB_URL |
- | Vercel Blob のプレイリストデータ URL(設定するとプレイリスト掲載曲を優先処理) |
UNITS_BLOB_URL |
○ | Vercel Blob のユニットデータ URL(ユニット照合・releaseIds 自動集約に使用) |
REFETCH_MISSING_ARTIST_CREDITS |
- | 1 に設定すると artist 未設定トラックを持つリリースを強制再取得 |
REFETCH_ALL_SINGLES |
- | 1 に設定すると全シングルの全エディションを強制再取得し映像・非楽曲トラックを削除する。workflow_dispatch の入力には未配線のため、ローカル実行(.env.local 等)でのみ指定可能 |
W3. プレイリストデータ同期スクリプト(旧: プレイリスト集計・インデックス生成スクリプト)
概要
data/inputs/playlists.json が更新された際、Playlist/PlaylistEntry(Neon)へ同期し、releaseId/trackId の自動紐付けを行う。
備考(旧方式からの変更、#1677・morning-status-blume#166): 以前は
playlists.json本体・派生インデックス(member-playlist-index.json・track-playlist-index.json)をいずれもVercel Blobに保存し、メンバー詳細・曲詳細画面での高速表示のため事前集計していた。この方式では、自動紐付けが参照するRELEASES_BLOB_URLがリリースのNeon移行(v4.1.0、#853)時点で更新の止まったスナップショットのままになる問題があった(#1618で発覚)。Neon移行後は、メンバー詳細・曲詳細画面がPlaylistEntryへ直接クエリするため(プレイリスト機能 データ設計書 §7-2)、派生インデックス生成自体が不要になった。詳細は同設計書 §7 を参照。
実行タイミング
手動(プレイリスト更新時)。bun run upload-playlists を実行。
選曲リリースの自動解決(Resolution)ロジック
プレイリストに登録された楽曲(trackId)が複数のリリースに含まれる場合、以下の優先順位で最適な releaseId を自動選択する。
- 形式優先順位:
single>ep>album>other- プレイリスト側でアルバムが指定されていても、シングルが存在すればシングル盤を優先する。
- 初出優先順位: 同一形式(例:複数のシングル)に収録されている場合、
releaseDateが最も古いものを優先する。
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
DATABASE_URL |
○ | Neon 接続文字列(Playlist/PlaylistEntryの読み書き・リリース解決に使用) |
W4. Instagram 投稿収集バッチ
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | 毎日 JST 23:33(UTC 14:33) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
実行時刻の後ろ倒しの経緯はW1を参照(#1546)。
処理フロー
1. auth_state.json を読み込み、Playwright ブラウザコンテキストを復元する
2. Instagram トップページにアクセスし、セッション有効性を確認する
→ ログインページへのリダイレクトを検知した場合は sessionStatus: 'expired' を Blob に書き込み終了(非ゼロ終了コード)
3. Vercel Blob からメンバーデータを取得する
- OGメンバーは `OG_SYNC_WEEKDAYS` で指定した曜日のみ収集対象とする(Activeメンバーは常に対象。メンバー近況同期バッチ・YouTube OGチャンネル収集バッチと同一ルール、#1297)
- officialSns に active な instagram.com URL を持つメンバーを対象とする
4. 各対象メンバーのプロフィールページにアクセスし、投稿 URL 一覧を収集する
5. 既存の posts.json と比較し、新規投稿のみを取得対象とする(差分検出)
6. 各新規投稿ページにアクセスし、メタデータを取得する
- og:description からキャプションの1行目を取得する
7. サムネイル URL として `/p/{postId}/media/?size=m` 形式のリダイレクト URL を記録する(Vercel Blob への画像アップロードは行わない)
- 収集時のサムネイル URL 有効性検証は行わない(Instagram が非ブラウザの HTTP リクエストを 429 で弾くため・#1114)。表示時に取得失敗した投稿はカード非表示で対応する(#1094)
8. instagram/{memberId}/posts.json を更新する(既存投稿を保持しつつ新規を先頭に追加)
9. instagram/daily-new-posts.json を今回の実行で新たに取得した投稿一覧で上書き保存する
10. instagram/sync-status.json を更新する
11. daily-digest/{日付}/instagram.json に実メンバー別の新着件数を書き込む(データ構造はW14参照)
12. `OFFICIAL_INSTAGRAM_ACCOUNTS`(`constants/instagram-official-accounts.ts`)の各アカウント(モーニング娘。公式・ミニモちゃん)について、daily-digest/{日付}/instagram-official.json にアカウント別の新着件数を書き込む(データ構造はW14参照)
Secret 運用手順
INSTAGRAM_AUTH_STATE は Playwright の storageState(Cookie等を含む JSON)。
Session 失効時(ワークフロー失敗時)に以下の手順で再登録する。
# 0. playwright のブラウザをインストール(初回のみ)
bunx playwright install chromium
# 1. ヘッドフルブラウザで Instagram にログインし、認証状態を保存する
bun run instagram:auth
# 2. 生成された auth_state.json を GitHub Actions Secret に登録する
gh secret set INSTAGRAM_AUTH_STATE < auth_state.json
# 3. auth_state.json を削除する(機密情報のため必ず削除)
rm auth_state.json
注意: Session の有効期間は数週間〜数ヶ月。ワークフローが認証エラーで失敗したらリポジトリオーナーにメール通知が届く(GitHub Actions の失敗通知)。
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
BLOB_READ_WRITE_TOKEN |
○ | Vercel Blob 読み書きトークン |
MEMBERS_BLOB_URL |
○ | Vercel Blob のメンバーデータ URL(既存) |
INSTAGRAM_AUTH_STATE |
○ | Playwright 認証状態(JSON文字列)。Repository secret |
OG_SYNC_WEEKDAYS |
- | OG メンバーを収集する曜日番号(カンマ区切り、デフォルト: 1)。Repository variable(非機密な設定値のため、#1347) |
注意: 秘匿すべき値(APIキー・トークン等)は Repository secrets に統一すること(Environment secrets は
environment:指定がないと注入されない)。曜日番号等の非機密な設定値は Repository variables に登録する(#1347)。
Blob 書き込みパス
| パス | 内容 | タイミング |
|---|---|---|
instagram/{memberId}/posts.json |
メンバー単位の全投稿メタデータ | 毎回(差分追加) |
instagram/sync-status.json |
同期実行状態 | 毎回上書き |
instagram/daily-new-posts.json |
当日新規取得投稿一覧 | 毎回上書き |
daily-digest/{日付}/instagram.json |
実メンバー別新着件数サマリー(デイリーダイジェスト配信用、データ構造はW14参照) | 毎回(同日は件数を加算マージ) |
daily-digest/{日付}/instagram-official.json |
公式アカウント別新着件数サマリー(デイリーダイジェスト配信用、データ構造はW14参照) | 毎回(同日は件数を加算マージ) |
注意:
instagram/{memberId}/{postId}.jpgへの画像保存は廃止された。
W5. TikTok 投稿収集バッチ(公式+OG)
本節は、実装(RapidAPI + Neon)と乖離した旧アーキテクチャ(Playwright + Vercel Blob、ストレージがNeon移行前の#888当時の記述)のまま放置されていたため、#1489 対応時に現行実装に合わせて全面的に書き直した。
統合の経緯(#1546): 公式収集とOG収集(旧W15)は
collect-tiktok-posts.yml/collect-tiktok-og.ymlという別ワークフロー・別cronで運用していたが、収集系ワークフロー全体の実行時刻見直し(詳細はW1参照)に合わせてcollect-tiktok.yml1本に統合した。旧W15の内容は本節に統合済のため、W15は欠番とする。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | 毎日 JST 翌0:03(UTC 15:03)。collect-tiktok.yml内で公式→OGの順に1ジョブ内で実行する |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
OGの収集は
OG_SYNC_WEEKDAYS環境変数(YouTube OG収集と共用)で指定した曜日のみ実行する(shouldSyncOG(scripts/lib/og-sync.ts)で判定)。デフォルト(空文字・未設定)は月曜のみ(曜日番号:1)。実行時刻の後ろ倒しの経緯はW1を参照(#1546)。
処理フロー
collect-tiktok.yml内で公式収集(1〜9)→OG収集(10〜17)の順に実行する。OG収集はOG_SYNC_WEEKDAYS対象曜日以外は手順10で終了する。OG収集のステップはif: always()を付与しており、公式収集側が失敗(クォータ超過等)しても独立して実行される(統合前の別ワークフロー運用時と同等のフォールトアイソレーションを維持、#1546, PR #1547レビューで追加)。
1. Neon DB の tiktok_posts テーブルから既存 postId 一覧を取得する(postId はアカウント・チャンネル種別を問わずグローバルに一意なため、絞り込まず全件取得する)
2. `OFFICIAL_TIKTOK_ACCOUNTS`(`constants/tiktok-official-accounts.ts`)の各アカウント(モーニング娘。公式・ミニモちゃん)について、RapidAPI(`tiktok-scraper7.p.rapidapi.com`)の `/user/posts` を最新ページから呼び出し、既存 postId に到達するかページ上限(`MAX_PAGES`=10)に達するまで取得する
3. 取得した投稿を TikTokPost(channelType='official'、accountId 付き)に変換する
4. Vercel Blob からメンバーデータを取得し、キャプションのハッシュタグからメンバーへの紐付けを行う(`attributeTikTokPostToMembers`、詳細は次項)
5. 新規投稿のみを tiktok_posts に保存する(差分追加・skipDuplicates)
6. tiktok_sync_status(Neon、id=1固定の1行)を更新する。`lastSyncedAt`・`syncStatus`・`capturedMembersCount`・`totalTargetMembersCount`(`/sync-status` 画面向けの公式収集ステータス表示用)を毎回更新する。加えて、TOP新着投稿セクションの新着ウィンドウ判定用の `officialLastSyncedAt`/`officialLastAutoSyncedAt`/`officialPreviousAutoSyncedAt` も更新する(OG収集とは行を共有するが、official用カラムのみを更新しog用カラムには触れない)。ウィンドウ判定ロジックの詳細は「[TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design)」§4「新着表示の判定基準」を参照(#1489)
7. 投稿キャプションと楽曲タイトルを照合し、シングル曲へのTrackLink(`type='tiktok'`)を自動生成・修正する(`fixTikTokTrackLinks`・`syncTikTokTrackLinks`)。タイトル照合は `lib/track-match.ts` の正規化ロジックを使う(詳細は「[TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design)」§6「TikTok公式アカウント詳細画面」を参照)。`fixTikTokTrackLinks` の「既存リンクをシングルトラックへ修正」ステップは毎回実行時に既存の誤リンクを検出して自己修正するため、過去分の個別バックフィルは不要
8. daily-digest/{日付}/tiktok.json に実メンバー別の新着件数を書き込む(データ構造はW14参照)
9. daily-digest/{日付}/tiktok-official.json にアカウント別の新着件数を書き込む(データ構造はW14参照)
10. OG_SYNC_WEEKDAYS の対象曜日でなければ何もせず正常終了する
11. Vercel Blob からメンバーデータを取得し、tiktokChannel を持つ OG メンバー(status が Active 以外)を対象とする
12. 既存 tiktok_posts から channelType='og' の既存 postId 一覧を取得する(新着件数の集計に使用)
13. 各対象メンバーについて fetchUserPosts(RapidAPI、unique_id=tiktokChannel.username)を1ページ(先頭5件)のみ呼び出し、最新5件を取得する(公式アカウント収集と異なり、既存 postId に到達するまでページを遡る差分取得は行わない)
14. 取得した投稿を TikTokPost(channelType='og'・memberId 付き、mentionedMemberIds は常に空配列)に変換する
15. メンバーごとに、取得した最新5件に含まれない投稿を tiktok_posts から削除したうえで、取得した5件を保存する(結果としてメンバーごとに常に最新5件のみが残る)
16. daily-digest/{日付}/tiktok-og.json にメンバー別の新着件数を書き込む(データ構造はW14参照)
17. tiktok_sync_status(Neon、id=1固定の1行。手順6と行は共有するが、og用カラムのみを更新しofficial用カラムには触れない)の `ogLastSyncedAt`/`ogLastAutoSyncedAt`/`ogPreviousAutoSyncedAt` を更新する。TOP新着投稿セクションの新着ウィンドウ判定用フィールドであり、`/sync-status` 画面向けの公式収集ステータス表示用フィールド(`syncStatus`等)には触れない。定期実行/手動実行での更新ロジックの詳細は「[TikTok投稿機能 データ設計書](/design/sns/tiktok-data-design)」§4「新着表示の判定基準」を参照(#1489)
注意(旧: tiktok_sync_status を更新しない設計だった理由、#1478時点): 当初(#1478時点)は、YouTube が
youtube_sync_statusを official/og で独立カラム化しTOP新着投稿セクションのウィンドウ判定に使っている(W7、#1450)のに対し、TikTokのtiktok_sync_statusはTOP新着投稿セクションのウィンドウ判定には使われておらず/sync-status画面の公式収集ステータス表示専用だったため、手順10〜17(旧W15)はこのテーブルを更新しない設計だった。その後、手順1〜9の「同日capturedAt + 件数キャップ」方式では複数アカウント・複数OGメンバーの同時収集時に新着を取りこぼす不具合が判明したため、YouTubeと同じofficial/og独立ウィンドウ方式に変更し、OG収集もtiktok_sync_statusのog用カラムを更新するようになった(#1489)。
キャプションベースのメンバー属性付与(公式収集のみ、手順4)
メンバーへの紐付けは、収集時(手順4)にキャプションのハッシュタグから判定し、mentionedMemberIds としてDBに保存する(lib/tiktok-attribution.ts)。表示側は保存済の値をそのまま用いる(アプリ実行時の都度計算ではない)。判定対象はキャプション内のハッシュタグ(# で始まるトークン)のみで、本文中の言及はハッシュタグに含まれていなければマッチしない(#1412)。OG収集(手順14)はハッシュタグ判定を行わず、mentionedMemberIds は常に空配列。
- ハッシュタグ内のフルネーム(正規化後)が部分一致すればマッチ
- フルネームが一致しない場合、苗字または名前(2文字以上のもの)が部分一致すればマッチ
- 異体字(﨑/崎・髙/高・栁/柳等)を正規化してからマッチング
エラーハンドリング(OG収集、手順10〜17)
| エラー種別 | 動作 |
|---|---|
| ネットワーク例外(DNS 解決失敗・接続タイムアウト等) | 当該メンバーのみスキップしてログ出力後、後続メンバーの処理を続行する |
| API HTTP エラー(404 / 500 等) | fetchUserPosts が例外を送出するため、当該メンバーの削除・保存をスキップして続行する(既存データ保持) |
| 全メンバー失敗(取得成功 0 人かつ失敗 1 人以上) | 非ゼロ終了して GitHub Actions の失敗通知に乗せる(YouTube OG収集と同じ方針、PR #1116 レビュー申し送りより採用) |
環境変数
| 変数名 | 必須(公式) | 必須(OG) | 説明 |
|---|---|---|---|
RAPIDAPI_KEY |
○ | ○ | RapidAPI キー(未設定時はエラーで終了) |
DATABASE_URL |
○ | ○ | Neon PostgreSQL 接続文字列(未設定時はエラーで終了) |
MEMBERS_BLOB_URL |
- | ○ | 公式: 未設定時はハッシュタグ属性付与をスキップ/OG: 未設定時はエラーで終了 |
BLOB_READ_WRITE_TOKEN |
○ | ○ | Vercel Blob 読み書きトークン(デイリーダイジェスト用サマリーの書き込みに使用、W14) |
OG_SYNC_WEEKDAYS |
- | - | OG 収集する曜日番号(カンマ区切り、デフォルト: 1)。YouTube OG収集と共用。Repository variable(非機密な設定値のため、#1347) |
注意: 秘匿すべき値(APIキー・トークン等)は Repository secrets に統一すること(Environment secrets は
environment:指定がないと注入されない)。曜日番号等の非機密な設定値は Repository variables に登録する(#1347)。
W6. イベント同期バッチ
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | 毎日 JST 03:03(UTC 18:03) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
| 手動実行(検証専用) | bun scripts/workflow/sync-events.ts --validate-only(ローカル実行専用。詳細は「検証専用モード」節を参照) |
処理フロー
1. Vercel Blob からメンバーデータを取得する(MEMBERS_BLOB_URL)
→ 未設定の場合はメンバーイベントをスキップ(ドライラン)
2. Neon DB からリリースデータを取得する(releases テーブル)
3. Neon DB からライブデータを取得する(lives テーブル、ツアー・参加メンバー含む)
4. Neon DB からラジオ放送回データを取得する(radio_episodes テーブル、番組・出演メンバー含む)
5. Neon DB からフェスデータを取得する(festivals テーブル)
6. `data/inputs/manual-events.json` から手動イベントを読み込む。`events` 配列の要素は1件ずつバリデーションし、不正な要素はスキップして残りを同期する(1件の不正で全件が同期対象から外れることを防ぐグレースフルな失敗処理。詳細は [イベントテーブル定義書 §5-5](/design/timeline/events-table#5-5) 参照。MorningStatusApp #1590)
7. 手動イベントの `venueName`/`songs` を解決する(morning-status-blume#67)
- `venueName` 指定分: 既存 `venues.name` 一致で再解決、未一致分は Google Maps Geocoding API(`GEOCODING_API_KEY`)でジオコーディング。`GEOCODING_API_KEY` 未設定時は会場解決をスキップ(`venueId` は更新せず処理継続)。詳細は [イベントテーブル定義書 §5-5](/design/timeline/events-table#5-5) 参照
- `songs` 指定分: `lib/track-match.ts` の `buildTrackCandidatesMapFromPrisma` で曲名からトラック候補を解決する(`artist` が指定された曲(他アーティスト楽曲)は解決をスキップする。詳細は [イベントテーブル定義書 §5-5](/design/timeline/events-table#5-5) 参照。MorningStatusApp #1585)。この時点では `event_songs`/`event_song_tracks` への書き込みは行わない(`event_songs.eventId` は `events(id)` への FK であり、新規の手動イベントはまだ `events` 行が存在しないため)
8. 各ソースを events 形式に変換する
- release : releaseDate を Date に変換(YYYY-MM-DD / YYYY-MM / YYYY 形式に対応)
- join : profile.joinDate を加入イベントに変換
- graduate : profile.gradDate を卒業イベントに変換(gradDate が null のメンバーはスキップ)
- live : ライブ公演を live イベントに変換(title が null の場合はツアータイトルにフォールバック)
- media : ラジオ放送回を media イベントに変換
- festival : フェスを festival イベントに変換(`time` は出演時刻を優先し、不明なら開催時刻。詳細は [イベントテーブル定義書 §5-6](/design/timeline/events-table#5-6) 参照)
- topic : 手動イベントを topic イベントに変換(手順7で解決した venueId を付与)
9. 全イベントを events テーブルへ upsert する(source + sourceId で一意判定)
→ 既存レコードあり → UPDATE(date / type / title / members 等を更新、既存の `id` を保持)
→ 既存レコードなし → INSERT(新規 `id` を採番)
10. `songs` 指定分の手動イベントについて、手順9で確定した `event_id` を使って `event_songs`/`event_song_tracks` を差分書き込みする(差分ルールは [イベントテーブル定義書 §5-5](/design/timeline/events-table#5-5) 参照)
11. 同期完了ログを出力する(created / updated / errors / validationErrors 件数)
upsert キー規則
| source | type | sourceId |
|---|---|---|
| release | release | releaseId |
| member | join | memberId |
| member | graduate | ${memberId}:graduate |
| live | live | liveId |
| radio | media | episodeId |
| festival | festival | festivalId |
| manual | topic 等 | manual-events.json の sourceId(任意の識別子) |
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
MEMBERS_BLOB_URL |
○ | Vercel Blob のメンバーデータ URL(未設定でメンバーイベントをスキップ) |
GEOCODING_API_KEY |
- | Google Maps Geocoding API キー。未設定時は手動イベントの会場解決をスキップ(morning-status-blume#67) |
注意: 全シークレットは Repository secrets に統一すること(Environment secrets は
environment:指定がないと注入されない)。
検証専用モード
--validate-only フラグ指定時は、通常の同期フロー(手順1〜11)を全てバイパスし、data/inputs/manual-events.json の読み込み・バリデーション(手順6と同じ loadManualEvents())のみを実行する。
- Vercel Blob(メンバーデータ)・Neon DB(releases / lives / radio_episodes / festivals)・Google Maps Geocoding API への一切のアクセスを行わない
manual-events.jsonの変更内容だけを素早く検証したい場合に使う(通常の--dry-runは全データソースを読み取るため、DBアクセスを伴う分だけ実行時間が長い)- 結果(読み込み件数・バリデーションエラー件数)をログ出力するのみで、
eventsテーブルへの書き込みは行わない(--dry-runと同様に書き込みなしだが、読み取り自体も manual-events.json 単体に限定される点が異なる) - 本番の定期実行・GitHub Actions ワークフロー(
sync-events.yml)はこのモードを使わず、常に全データソースを同期する
ドライラン動作
| スクリプト | ドライラン条件 | 動作 |
|---|---|---|
| sync-status.ts | SEARCH_API_KEY 未設定 |
検索せず、Blob 書き込みなし |
| sync-status.ts(Instagram) | INSTAGRAM_ACCESS_TOKEN または INSTAGRAM_PAGE_ID 未設定 |
Instagram 取得をスキップ(近況同期は通常通り実行) |
| sync-discography.ts | DATABASE_URL 未設定 |
MusicBrainz 取得のみ、Neon 書き込みなし(units.json・ステータス Blob 書き込みも行わない) |
| sync-genius-links.ts | DATABASE_URL 未設定 |
Genius API 取得のみ、Neon 書き込みなし |
| sync-playlist-links.ts | DATABASE_URL 未設定 |
YouTube API 取得のみ、Neon 書き込みなし |
| sync-events.ts | MEMBERS_BLOB_URL 未設定 |
メンバーイベントをスキップ(Releases / Lives / Radio は通常通り同期) |
| sync-events.ts | GEOCODING_API_KEY 未設定 |
手動イベントの会場解決をスキップ(venueId は更新しない。他の同期は通常通り実行) |
フェイルセーフ
- Blob 取得失敗: 空配列を使用して処理を継続
- 外部 API エラー:
console.errorでログ出力後、次のメンバーの処理を継続 - MusicBrainz 0件取得: 既存データを維持して終了
- 手動イベントのバリデーション失敗:
events配列の要素単位でバリデーションし、不正な要素のみスキップ。有効な要素の同期は継続する(MorningStatusApp #1590)
GitHub Actions ワークフロー共通設定
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # Node.js 24 に早期移行(#310)
runs-on: ubuntu-latest
W7. YouTube 投稿収集バッチ(公式+OG)
統合の経緯(#1546): 公式収集(本バッチ)とOG収集(旧W8)は
collect-youtube-official.yml/collect-youtube-og.ymlという別ワークフロー・別cronで運用していたが、収集系ワークフロー全体の実行時刻見直し(詳細はW1参照)に合わせてcollect-youtube.yml1本に統合した。旧W8の内容は本節に統合済のため、W8は欠番とする。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | 毎日 JST 翌0:33(UTC 15:33)。collect-youtube.yml内で公式→OGの順に1ジョブ内で実行する |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
OGの収集は
OG_SYNC_WEEKDAYS環境変数で指定した曜日のみ実行する(スクリプト内のshouldSyncOG(scripts/lib/og-sync.ts)で判定。メンバー近況同期バッチと同一ルール)。デフォルト(空文字・未設定)は月曜のみ(曜日番号:1)。実行時刻の後ろ倒しの経緯はW1を参照(#1546)。
処理フロー
collect-youtube.yml内で公式収集(1〜5)→OG収集(6〜12)の順に実行する。OG収集はOG_SYNC_WEEKDAYS対象曜日以外は手順6で終了する。OG収集のステップはif: always()を付与しており、公式収集側が失敗(クォータ超過等)しても独立して実行される(統合前の別ワークフロー運用時と同等のフォールトアイソレーションを維持、#1546, PR #1547レビューで追加)。
1. Neon DB の youtube_posts テーブルから channelType='official' の既存 videoId 一覧を取得する
2. ハードコードされた公式チャンネルハンドル(@morningmusume)ごとに以下を実行する
- channels API(forHandle)でアップロードプレイリスト ID を取得する(fetchPlaylistId)
→ 取得できない場合は当該チャンネルをスキップ
- playlistItems API で最新動画を取得する(fetchVideosFromPlaylist、最大 50 件)
- 取得した動画を YoutubePost(channelType='official')に変換する
3. 既存 videoId を除外した新規投稿のみを youtube_posts に保存する(差分追加・skipDuplicates)
4. youtube_sync_status(Neon、id=1固定の1行)を更新する。official用カラムのみを更新しog用カラムには触れない。定期実行/手動実行での更新ロジックの詳細は「[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design)」§6.2「新着表示の判定基準」を参照(#1450)
5. daily-digest/{日付}/youtube-official.json にアカウント別(`@morningmusume`)の新規取得件数を書き込む(データ構造はW14参照)
6. OG_SYNC_WEEKDAYS の対象曜日でなければ何もせず正常終了する
7. Vercel Blob からメンバーデータを取得し、youtubeChannel を持つ OG メンバー(status が Active 以外)を対象とする
8. 各対象メンバーについて playlistItems API で最新動画を取得する(fetchVideosFromPlaylist、最大 5 件)
- playlistId は members.json の youtubeChannel に保存済のため channels API は呼ばない
9. 取得した動画を YoutubePost(channelType='og'・memberId 付き)に変換する
10. メンバーごとに、今回取得分に含まれない古い投稿を youtube_posts から削除し、新規投稿を保存する(最新 5 件のみ保持)
11. youtube_sync_status(Neon、id=1固定の1行。手順4と行は共有するが、og用カラムのみを更新しofficial用カラムには触れない)を更新する。定期実行/手動実行での更新ロジックの詳細は「[YouTube投稿一覧機能 データ設計書](/design/sns/youtube-data-design)」§6.2「新着表示の判定基準」を参照(#1450)
12. daily-digest/{日付}/youtube-og.json にメンバー別の新着件数を書き込む(データ構造はW14参照)
エラーハンドリング
| エラー種別 | 対象 | 動作 |
|---|---|---|
クォータ超過(YouTubeQuotaExceededError) |
公式・OG共通 | 処理全体を中断する(クォータ管理方針: youtube-data-design.md 参照) |
| ネットワーク例外(DNS 解決失敗・接続タイムアウト等) | 公式(手順1〜5) | 当該チャンネルのみスキップしてログ出力後、後続チャンネルの処理を続行する |
| API HTTP エラー(404 / 500 等) | 公式(手順1〜5) | fetchPlaylistId / fetchVideosFromPlaylist が null / 空配列を返すため、当該チャンネルをスキップして続行する |
| ネットワーク例外(DNS 解決失敗・接続タイムアウト等) | OG(手順6〜12) | 当該メンバーのみスキップしてログ出力後、後続メンバーの処理を続行する |
| API HTTP エラー(404 / 500 等) | OG(手順6〜12) | fetchVideosFromPlaylist が空配列を返すため、当該メンバーの削除・保存をスキップして続行する(既存データ保持) |
| 全チャンネル/全メンバー失敗(取得成功 0 人かつ失敗 1 人以上) | OG(手順6〜12) | 非ゼロ終了して GitHub Actions の失敗通知に乗せる(PR #1116 レビュー申し送りより採用) |
環境変数
| 変数名 | 必須(公式) | 必須(OG) | 説明 |
|---|---|---|---|
YOUTUBE_API_KEY |
○ | ○ | YouTube Data API v3 キー(未設定時はエラーで終了) |
DATABASE_URL |
○ | ○ | Neon PostgreSQL 接続文字列(未設定時はエラーで終了) |
MEMBERS_BLOB_URL |
- | ○ | OG: 未設定時はエラーで終了(公式は不使用) |
BLOB_READ_WRITE_TOKEN |
○ | ○ | Vercel Blob 読み書きトークン(デイリーダイジェスト用サマリーの書き込みに使用、W14) |
OG_SYNC_WEEKDAYS |
- | - | OG 収集する曜日番号(カンマ区切り、デフォルト: 1)。Repository variable(非機密な設定値のため、#1347) |
注意: 秘匿すべき値(APIキー・トークン等)は Repository secrets に統一すること(Environment secrets は
environment:指定がないと注入されない)。曜日番号等の非機密な設定値は Repository variables に登録する(#1347)。
W9. メンバー関係性マップ 動的エッジ同期バッチ
概要
member_relationships テーブルのうち、ライブ・ラジオ・イベント共演から動的に導出されるエッジ(live-coappearance / radio-coappearance / event-coappearance)を再集計する。イベント同期バッチ(W6)の後続ステップとして同一ワークフロー(sync-events.yml)内で実行される。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | 毎日 JST 03:03(UTC 18:03)。イベント同期バッチの直後に実行 |
| 手動実行 | GitHub Actions の workflow_dispatch で可能(Sync Events ワークフロー) |
処理フロー
1. ライブ共演(live-coappearance)を集計する
→ member_lives → lives → tours(LEFT JOIN)を辿り、同一 liveId を共有するメンバーペアごとの共演回数を集計
→ label は Tour.title →(null時)Live.title →(両方null時)固定文言「ライブ共演」の順でフォールバック
2. ラジオ共演(radio-coappearance)を集計する
→ radio_members → radio_episodes → radio_shows を JOIN し、ペア×番組名ごとの共演回数を集計
3. イベント共演(event-coappearance)を集計する
→ events.members(String[])を unnest してペアを展開し、ペア×イベントタイトルごとの共演回数を集計
→ type='live'(ライブ種別イベント)はライブ共演で別途集計するため除外する
4. ペアごとに、weight(全ラベル共通の合計共演回数)とは別にラベルごとの出現回数を集計し、最多出現ラベルを label に採用する(同数の場合は文字列順で決定的に選ぶ)
5. member_relationships から動的エッジ種別(live-coappearance / radio-coappearance / event-coappearance)を一括削除する
6. 集計結果を member_relationships に一括登録する(createMany, skipDuplicates: true)
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
関連ドキュメント
ラベル算出ロジックの詳細・型定義・エッジ表示ルールは docs/design/member/member-map-data-design.md を参照。
W10. Genius リンク同期バッチ
概要
Neon 管理のリリースデータのうち、YouTube リンクを持ち Genius リンク未取得のトラックを対象に、Genius API(検索エンドポイント)から歌詞ページ URL を取得し、TrackLink(type: genius)として Neon に保存する。取得状況は同期状況画面(/sync-status)向けに genius-links-status.json(Vercel Blob)にも記録する。詳細設計は 歌詞サイトリンク機能 データ設計書 を参照。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | なし(#1179 で定期実行を停止) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
GENIUS_ACCESS_TOKEN |
○ | Genius API アクセストークン |
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
BLOB_READ_WRITE_TOKEN |
○ | Vercel Blob 読み書きトークン(同期状況の保存に使用) |
RELEASES_BLOB_URL |
- | releases.json の Blob URL(GENIUS_LINKS_STATUS_BLOB_URL 未設定時のフォールバック導出元として直接参照) |
GENIUS_LINKS_STATUS_BLOB_URL |
- | 同期状況保存先の Blob URL(未設定時は RELEASES_BLOB_URL から導出) |
W11. モーニング女学院放送データ収集バッチ
概要
X(@morning1422)の投稿からラジオ番組「モーニング女学院」の放送回・出演メンバー・オンエア楽曲を抽出し、Neon DB に保存する。詳細設計は ラジオオンエア データ設計書 §4-1 を参照。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | なし(#1333 で定期実行を停止。X API のタイムライン取得上限(直近3,200件付近、#1331)に到達し、これ以上の遡及取得ができなくなったため) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
新規放送分は
register-manual-radio-episodes.ts(それ以外のスクリプト §定常運用)による手動登録に統一している。
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
TWITTERAPI_IO_KEY |
○ | twitterapi.io API キー |
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
BLOB_READ_WRITE_TOKEN |
○ | Vercel Blob 読み書きトークン |
MEMBERS_BLOB_URL |
○ | Vercel Blob のメンバーデータ URL |
MORNING_ONAIR_MAX_PAGES |
- | 遡及取得の1回あたり上限ページ数(デフォルト: 20) |
W12. ヤングタウン土曜日収集バッチ
概要
X(@yando_staff)の投稿からラジオ番組「ヤングタウン土曜日」の放送回・出演メンバー・オンエア楽曲を抽出し、Neon DB に保存する。詳細設計は ラジオオンエア データ設計書 §4-2 を参照。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | なし(#1280, #1333 で定期実行を停止。手動登録の方が容易なため) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
新規放送分は
register-manual-radio-episodes.ts(それ以外のスクリプト §定常運用)による手動登録に統一している。
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
TWITTERAPI_IO_KEY |
○ | twitterapi.io API キー |
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
MEMBERS_BLOB_URL |
○ | Vercel Blob のメンバーデータ URL |
W13. メイボンソワ選曲データ収集バッチ
概要
STV 公式サイトの HTML を解析し、ラジオ番組「メイボンソワ」の放送回・選曲データを抽出して Neon DB に保存する。詳細設計は ラジオオンエア データ設計書 §4-3 を参照。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | なし(#1333 で定期実行を停止。モーニング女学院・ヤングタウン土曜日と同様に手動登録方式へ統一するため) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
新規放送分は
register-manual-radio-episodes.ts(それ以外のスクリプト §定常運用)による手動登録に統一している。
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
MEMBERS_BLOB_URL |
○ | Vercel Blob のメンバーデータ URL |
W14. デイリーダイジェスト配信バッチ
概要
Instagram 投稿収集(W4)・TikTok 投稿収集(W5、公式+OG)・YouTube 投稿収集(W7、公式+OG)・メンバー近況同期(W1、Ameba ブログ分)の各バッチが daily-digest/{日付}/{ソース}.json(Vercel Blob)に書き出した新着件数サマリーを集約し、1通のメール本文にまとめて送信する。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | 毎日 JST 翌01:03(UTC 16:03) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
処理フロー
1. MEMBERS_BLOB_URL からメンバーデータを取得する(未設定時はエラーで終了)
2. daily-digest/ 配下の Blob を列挙し、ソース種別
(instagram/instagram-official/tiktok/tiktok-official/tiktok-og/youtube-og/youtube-official/blog)
ごとに最新日付のファイル URL を取得する(resolveLatestDigestUrls)
→ 日付をまたいで各収集バッチが実行された場合でも最新のファイルを参照できる
3. 各ソースのダイジェストファイルを取得する
→ 取得失敗(fetch 失敗・非 200 応答)したソースはスキップし、警告ログを出力して処理を継続する
4. メンバー・投稿種別ごとに件数を集計する(buildDigestEmailBody)
→ `instagram-official`/`tiktok-official`/`youtube-official`(いずれも `OfficialSummaryEntry`)は、
SNSごとに「モーニング娘。公式 2件、ミニモちゃん 1件」のようにアカウント名付きの内訳を
「公式アカウント」セクションに表示する(Instagram公式アカウントにはモーニング娘。公式で
ないミニモちゃんも含まれるため、セクション見出しは特定アカウント名ではなく「公式アカウント」とする)
→ それ以外(`blog`/`instagram`/`tiktok`/`tiktok-og`/`youtube-og`、いずれも実メンバーのみを持つ `MemberSummaryEntry`)は
メンバー別セクションに振り分け、現役→OG・期(generation)昇順・加入日昇順でソートする
(compareMemberSections)
→ 同一メンバーが複数ソースに新着を持つ場合は「Instagram 2件、ブログ 1件」のように読点区切りで1行にまとめる
→ 新着が1件もないソース・セクションは「(新着なし)」と表示する(新着なしでもメールは送信される)
5. 集計結果をメール本文としてローカルファイル(`daily-digest-email.txt`)に書き出す
6. 集約に使用した daily-digest/ 配下の Blob ファイルを削除する(del)
→ 削除は集計元ファイルの重複読み込み防止のためで、次回実行時は新規書き込み分のみを対象とする
7. GitHub Actions ワークフローが `daily-digest-email.txt` の内容を読み込み、
dawidd6/action-send-mail でメールを送信する
ダイジェストファイルのデータ構造
daily-digest/{日付}/{ソース}.json は以下の各バッチが書き込む。source の値によって使用するデータ型(types/daily-digest.ts)が異なる。実メンバーの新着件数と公式・グループアカウントの新着件数は同一SNS内でもファイルを分け、youtube-og/youtube-officialの分割にならってinstagram/instagram-official・tiktok/tiktok-officialのように管理する(#1423)。
ソース (source) |
書き込み元バッチ | ファイルパス | 使用する型 |
|---|---|---|---|
blog |
W1 メンバー近況同期バッチ | daily-digest/{日付}/blog.json |
MemberSummaryEntry |
instagram |
W4 Instagram投稿収集バッチ | daily-digest/{日付}/instagram.json |
MemberSummaryEntry |
instagram-official |
W4 Instagram投稿収集バッチ | daily-digest/{日付}/instagram-official.json |
OfficialSummaryEntry |
tiktok |
W5 TikTok投稿収集バッチ | daily-digest/{日付}/tiktok.json |
MemberSummaryEntry |
tiktok-official |
W5 TikTok投稿収集バッチ | daily-digest/{日付}/tiktok-official.json |
OfficialSummaryEntry |
tiktok-og |
W5 TikTok投稿収集バッチ(OG) | daily-digest/{日付}/tiktok-og.json |
MemberSummaryEntry |
youtube-og |
W7 YouTube投稿収集バッチ(OG) | daily-digest/{日付}/youtube-og.json |
MemberSummaryEntry |
youtube-official |
W7 YouTube投稿収集バッチ(公式) | daily-digest/{日付}/youtube-official.json |
OfficialSummaryEntry |
MemberSummaryEntry(実メンバー別新着件数)
実メンバー単位で新着を持つソース(blog/instagram/tiktok/tiktok-og/youtube-og)が使用する。公式・グループアカウントはこの型には含まれない(後述の OfficialSummaryEntry で別ファイルに分離する)。
| フィールド | 型 | 内容 |
|---|---|---|
source |
'blog' | 'instagram' | 'tiktok' | 'tiktok-og' | 'youtube-og' |
ソース種別 |
date |
string(YYYY-MM-DD、JST) |
書き込み日 |
entries[].memberId |
string |
実メンバーID |
entries[].memberName |
string |
メンバー名 |
entries[].count |
number |
書き込み元バッチがその実行で新規に取得・蓄積した件数 |
entries[].retroDates |
string[](任意、YYYY-MM-DD) |
countのうち遡及収集(実投稿日がそのソースの前回同期時刻以前)と判定された投稿の実投稿日一覧。該当なしの場合はフィールド自体を省略する。instagramは実投稿日を取得していないため常に空(#1502) |
遡及収集の判定基準: 投稿の実投稿日が、そのソースの前回同期時刻(新着ウィンドウの起点)以前の場合、そのバッチ実行で新規に検出されたにもかかわらず本来はそれ以前の同期サイクルで取得されているべきだった投稿とみなし、遡及収集と判定する。実投稿日・前回同期時刻の具体的なフィールド名はソースごとに異なるため、詳細は各データ設計書の新着表示の判定基準(Amebaブログ機能 データ設計書§8、TikTok投稿機能 データ設計書§4、YouTube投稿一覧機能 データ設計書§6.2)を参照。デイリーダイジェストメール・TOPページ新着投稿セクションの双方で、該当投稿には実投稿日を併記する(#1502)。
OfficialSummaryEntry(公式・グループアカウント別新着件数)
公式・グループアカウントを持つ全ソース(instagram-official/tiktok-official/youtube-official)が共通で使用する型。アカウント単位でカウントを保持するため、1ソースにつき複数アカウントを個別に集計できる。
| フィールド | 型 | 内容 |
|---|---|---|
source |
'instagram-official' | 'tiktok-official' | 'youtube-official' |
ソース種別 |
date |
string(YYYY-MM-DD、JST) |
書き込み日 |
accounts[].accountId |
string |
公式アカウントID(下表「対象アカウント一覧」参照) |
accounts[].accountName |
string |
メール本文に表示するアカウント表示名 |
accounts[].count |
number |
書き込み元バッチがその実行で新規に取得した件数 |
対象アカウント一覧
| ソース | 対象アカウント | 管理方法 |
|---|---|---|
youtube-official |
モーニング娘。公式(@morningmusume) |
collect-youtube-official.ts 内の OFFICIAL_CHANNEL_HANDLES |
instagram-official |
モーニング娘。公式・ミニモちゃんの2アカウント | OFFICIAL_INSTAGRAM_ACCOUNTS(constants/instagram-official-accounts.ts) |
tiktok-official |
モーニング娘。公式・ミニモちゃんの2アカウント(#1458) | OFFICIAL_TIKTOK_ACCOUNTS(constants/tiktok-official-accounts.ts) |
同日複数回実行時のマージ
同日に複数回実行された場合、書き込み時に putDigestEntry(lib/daily-digest.ts)が既存ファイルと同一の集計単位(MemberSummaryEntryはmemberId、OfficialSummaryEntryはaccountId)のcountを加算してマージする。MemberSummaryEntryのretroDatesは配列を連結してマージする(#1502)。
エラーハンドリング
| エラー種別 | 動作 |
|---|---|
| 各ソースのダイジェストファイル取得失敗(fetch 失敗・非 200 応答) | 当該ソースをスキップし警告ログを出力、後続処理を継続する(メールは残りのソースのみで生成される) |
daily-digest/ 配下の Blob ファイル削除失敗 |
スクリプト全体が非ゼロ終了する。daily-digest-email.txt はステップ5で生成済だが、send-daily-digest.yml の後続ステップ(メール本文読み込み・送信)は前段ステップの成功が前提(if: always() 等の条件なし)のため実行されず、メール送信自体がスキップされる |
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
BLOB_READ_WRITE_TOKEN |
○ | Vercel Blob 読み書きトークン(daily-digest/ の一覧取得・削除に使用) |
MEMBERS_BLOB_URL |
○ | Vercel Blob のメンバーデータ URL(未設定時はエラーで終了) |
メール送信設定
メール送信ステップ(dawidd6/action-send-mail@v3)はスクリプト本体の環境変数とは別に、ワークフロー内で以下を使用する。
| 変数名 | 種別 | 説明 |
|---|---|---|
MAIL_SERVER |
Repository variables | SMTP サーバーアドレス |
MAIL_PORT |
Repository variables | SMTP ポート番号 |
MAIL_USERNAME |
Repository secrets | SMTP 認証ユーザー名 |
MAIL_PASSWORD |
Repository secrets | SMTP 認証パスワード |
MAIL_TO |
Repository secrets | 送信先メールアドレス |
注意:
daily-digest-email.txtが生成されなかった場合(MEMBERS_BLOB_URL未設定等でスクリプトが早期にエラー終了した場合や、上記「エラーハンドリング」の Blob 削除失敗の場合)はメール送信ステップ自体がスキップされる。新着が0件の場合はファイルは生成されるため、その場合は「(新着なし)」を含むメールが送信される。
W16. Blob URLシークレット診断バッチ
概要
GitHub Actions の Repository secrets に登録された *_BLOB_URL 系シークレット4件について、「参照可能か(未設定・空でないか)」「fetch して 2xx 応答が返るか」「期待する形式の JSON か」を検証する、読み取り専用の診断バッチ。Repository secrets は値を読み出せない仕様のため、本番ワークフローを動かさずにシークレットの値と Blob 側の実際の URL とのズレを早期に発見することを目的とする(MorningStatusApp#1618)。
RELEASES_BLOB_URL(releases.json)・PLAYLISTS_BLOB_URL(playlists.json)は検証対象外とする。リリース・プレイリストデータはいずれもNeonへ移行済(それぞれMorningStatusApp#854・#1680)で、Blob側は移行時点で更新が止まったスナップショットのため、疎通・形式を検証する意味がない(詳細 → プレイリスト機能 データ設計書 §7)。
lib/blob.ts の読み取り関数のうち getUnitsFromBlob()・getDiscographyStatusFromBlob()・getGeniusLinksStatusFromBlob() は、取得に失敗しても空配列・null を返すフェイルセーフ設計である。そのためシークレットの値がズレていても、利用側のバッチ(W2・W10)はジョブ成功のまま実質何もしない状態になりうる。本バッチはこれらの読み取り関数を経由せずに直接 fetch し、異常があればジョブを失敗させる。
put() 等の Blob への書き込み、DB アクセス、外部 API 呼び出しは一切行わない。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | 毎日 JST 09:00(UTC 00:00) |
| 手動実行 | GitHub Actions の workflow_dispatch で可能 |
深夜帯ではなく日中に実行するのは、異常を検知した時点で対応できるようにするためである。特定のバッチの直前に合わせる必要はない。フェイルセーフ設計のシークレットを使う W2・W10 は定期実行を停止しており、MEMBERS_BLOB_URL は読み取り関数が取得失敗時に例外を投げるため、利用側のバッチ(W1 等)でも異常を検知できる。
検証内容
全シークレット共通で、次の3点を検証する。
- 環境変数が未設定・空文字でないこと。
DISCOGRAPHY_STATUS_BLOB_URL・GENIUS_LINKS_STATUS_BLOB_URLの読み取り関数は未設定時にRELEASES_BLOB_URLから URL を導出するが、本バッチはシークレット自体の登録状況を検証する目的のため、この導出は行わない - fetch が例外なく完了し、2xx 応答が返ること
- 応答本文が JSON としてパースでき、下表の形式に適合すること
| シークレット | 期待する形式 |
|---|---|
MEMBERS_BLOB_URL |
{ members: [...] } 形式で、各要素がメンバーのスキーマ(MemberSchema)に適合すること |
UNITS_BLOB_URL |
配列で、各要素が id・name(文字列)と memberIds(文字列の配列)を持つこと。ユニットは Zod スキーマが未定義のため、必須フィールドの簡易チェックに留める |
DISCOGRAPHY_STATUS_BLOB_URL |
ディスコグラフィー同期状況のスキーマ(DiscographyStatusSchema)に適合すること |
GENIUS_LINKS_STATUS_BLOB_URL |
Genius リンク同期状況のスキーマ(GeniusLinksStatusSchema)に適合すること |
MEMBERS_BLOB_URL の形式判定は、アプリ側の読み取り関数(getMembersFromBlob())と同じ検証処理を共用する。診断バッチ独自の判定基準を持つと、アプリ側では読めるデータを異常と判定する(またはその逆の)乖離が生じるためである。
処理フロー
- 4件のシークレットを順に検証する。1件が失敗しても中断せず、全件を検証する
- 検証結果を1件ずつログに出力する(成功時は件数等の概要、失敗時は失敗理由)。URL そのものはログに出力しない
- 1件でも失敗があれば非ゼロ(
exit 1)で終了する
通知
異常の通知はワークフローの失敗(exit 1)による GitHub Actions 上の可視化のみとする。Issue の自動作成・メール通知は行わない。
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
MEMBERS_BLOB_URL |
○ | 検証対象(members.json) |
UNITS_BLOB_URL |
○ | 検証対象(units.json) |
DISCOGRAPHY_STATUS_BLOB_URL |
○ | 検証対象(discography-status.json) |
GENIUS_LINKS_STATUS_BLOB_URL |
○ | 検証対象(genius-links-status.json) |
GitHub Actions ワークフロー設定
- 依存関係のインストール(
bun install)後にスクリプトを実行するのみ。DB と画面スタイルに依存しないため、Prisma Client の生成と Panda CSS の styled-system 生成のステップは不要 - fetch が応答しないまま停止した場合に備え、ジョブに
timeout-minutes: 5を設定する
それ以外のスクリプト
scripts/console/・scripts/patch/ 配下の、GitHub Actions を介さず手動実行するスクリプト。用途に応じて以下に大別される。
- 定常運用スクリプト: データ更新や運用作業のたびに繰り返し手動実行するもの
- 一回限りパッチ: 特定の不具合修正・データ移行のために作成され、既に実行済で再実行を想定しないもの
- 開発用・共通ヘルパー: 単体実行を想定しない補助コードや、開発時のアドホックな動作確認用コード
うち3件(O2〜O4)は稼働中バッチと同水準のフル仕様で記載し、残りは概要のみの一覧表で管理する。
O2. 非楽曲トラック削除パッチ(一回限り・#1057)
概要
#1055 の sync スクリプト改修前に Neon DB に蓄積した非楽曲トラック(タイトルからインスト・カラオケ・MC と判定されるトラック)を削除し、
RadioOnairSong および playlists.json からの参照を事前にクリアする一回限りのパッチスクリプト。
実行手順
# .env.local に以下を設定してから実行
# DATABASE_URL=...
# BLOB_READ_WRITE_TOKEN=...
# PLAYLISTS_BLOB_URL=... (省略可: 未設定時はローカルの data/inputs/playlists.json を使用)
# 1. まず --dry-run で削除対象を確認する
bun scripts/patch/patch-delete-non-song-tracks.ts --dry-run
# 2. 内容を確認したら本実行する
bun scripts/patch/patch-delete-non-song-tracks.ts
処理フロー
1. Neon DB の tracks テーブルからすべてのトラックを取得する
2. タイトルパターン(isNonSongTrack)で非楽曲トラックを特定する
→ recording.video フラグは DB に保存されていないため、タイトルパターンのみで判定
→ 映像トラック(Dance Shot Ver. 等)は sync-discography 再実行時に自然と除外される
3. RadioOnairSong.trackId が対象 trackId を参照している行を NULL に更新する
4. playlists.json(Blob + ローカルファイル)の entries.trackId / releaseId をクリアする
→ PLAYLISTS_BLOB_URL 未設定時はローカルファイルのみ更新
5. 非楽曲トラックを tracks テーブルから削除する
→ track_links は onDelete: Cascade で自動削除
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
BLOB_READ_WRITE_TOKEN |
○ | Vercel Blob 書き込みトークン |
PLAYLISTS_BLOB_URL |
△ | playlists.json の Blob URL(未設定時はローカルのみ) |
注意事項
- 冪等性あり: 削除対象がない場合は何もしない(再実行可能)
- パッチ実行後は sync-discography を再実行し、欠落していた楽曲(
Nature is good!等)を補完すること
O3. メンバー関係性マップ 静的エッジシードスクリプト
概要
member_relationships テーブルのうち、メンバー・ユニットのプロフィールデータから機械的に導出できる静的エッジ(同期・同郷・同ユニット・同時在籍・リーダー継承・師弟関係の6種別)を生成する定常運用の手動実行スクリプト。メンバー・ユニットデータの更新時(加入・卒業・出身地変更・ユニット編成変更等)に再実行する。W9の動的エッジ同期とは異なり、GitHub Actions ワークフローには組み込まれていない。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | なし |
| 手動実行 | bun scripts/console/seed-member-relationships.ts |
処理フロー
1. Vercel Blob からメンバーデータ(MEMBERS_BLOB_URL)・ユニットデータ(UNITS_BLOB_URL)を取得する
2. 静的エッジを生成する(generateEdges)
1. 同期(same-gen): 同一 generation の全ペアに無方向エッジ(weight=1)
2. 同郷(same-hometown): 同一 profile.birthplace の全ペアに無方向エッジ(weight=1)
3. 同ユニット(same-unit): モーニング娘。本体・年度別ユニット(morning-musume / morning-musume-*)を除くサブユニットのうち、メンバー2名以上のユニット内の全ペアに無方向エッジ(label にユニット名)
4. 同時在籍(overlapping): 加入日〜卒業日(未卒業は現在日扱い)の期間が重なる全ペアに無方向エッジ(weight=1)
5. リーダー継承(leader-succession): leaderGenerationNumber が連番で隣接するペアに有向エッジ(前任→後任)
6. 師弟関係(mentor-student): profile.instructor に列挙された師匠 ID ごとに有向エッジ(fromMemberId=師匠、toMemberId=弟子、weight=1)
3. member_relationships から静的エッジ種別(6種)を一括削除する
4. 生成したエッジを member_relationships に一括登録する(createMany, skipDuplicates: true)
5. 種別ごとの生成件数をログ出力する
weight / label(現状)
| type | weight | label |
|---|---|---|
| same-gen / same-hometown / overlapping / leader-succession / mentor-student | 1 固定 | null |
| same-unit | 1 固定 | ユニット名 |
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
MEMBERS_BLOB_URL |
○ | Vercel Blob のメンバーデータ URL(未設定で0件) |
UNITS_BLOB_URL |
○ | Vercel Blob のユニットデータ URL(未設定で同ユニットエッジをスキップ) |
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
スコアリング組み込みによる変更点(実装: #1269)
関係性スコアリングロジックの実装(#1269)により、以下の変更を加える。計算式の詳細は docs/design/member/member-map-data-design.md §8 を参照。
- overlapping の weight 算出方法変更: 現状の
weight=1固定から、重なり月数を 0〜40pt に正規化した値に変更する(上限120ヶ月=10年で満点、以降40pt上限) - mentor-student の direct/indirect 判定:
- 既存の direct エッジ(
profile.instructor由来、depth=1)はweight=30・label='direct'に更新する profile.instructorの連鎖を depth 2 以上たどって到達できるメンバーペア(indirect)を新たに算出し、weight=15・label='indirect'で追加する- 循環参照防止: 連鎖探索は訪問済メンバー ID を
Setで管理し、既訪問メンバーには再度たどらない
- 既存の direct エッジ(
data/inputs/manual-mentor-relationships.jsonのマージ: 連鎖では拾えない師弟関係(本人の発言による影響表明等)を手動追記するファイル(初期状態は空)を新設し、data/inputs/manual-releases.jsonと同様のパターンで sync 時にマージする
O4. フェス・ツアー登録バッチ
概要
複数日で実施するフェス・ライブツアーの公演・会場・セットリストを一括登録する手動実行スクリプト。親Issue #1517「フェス・ツアー登録編集機能」のバッチ部分。会場の緯度経度・都道府県コードの自動取得(ジオコーディング)、フェス/ツアーIDの自動採番、セットリストの trackId 自動紐付けを行う点が、既存の register-tour-lives.ts(O4に統合、後述)との主な差分。
設計の詳細検討経緯は MorningStatusApp #1519 を参照。
実行タイミング
| トリガー | スケジュール |
|---|---|
| 定期実行 | なし |
| 手動実行 | bun scripts/console/register-tour-festival.ts <入力JSONファイルパス> [--dry-run] |
--dry-run 指定時の動作は「ドライラン」節を参照。
入力フォーマット: JSON
register-manual-radio-episodes.ts(data/inputs/manual-radio-episodes.json + Zodスキーマ)を前例とする。CSV(register-tour-lives.ts)ではなく JSON を採用する理由は、セットリストのような可変長ネスト配列を表現するのに適しているため。
セットリストの各曲({ seq, songTitle })の入力スキーマは、ラジオ側の手動登録(ラジオオンエア データ設計書 §4-0)と共通の scripts/lib/song-entry-schema.ts の SongEntrySchema をそのまま使う(フィールド名を揃えるだけでなく、Zodスキーマ自体を共有する)。
構造上の注意点: Setlist.tourId はツアー単位、Setlist.festivalId はフェス参加日単位(Festival は複数日開催フェスの1日を1レコードとして管理)に紐づく。そのため入力ファイルの単位が種別で異なる。
- ツアー: 1ファイル = 1ツアー。複数
lives(日程×会場)を持ち、セットリストはツアー全体で1つ - フェス: 1ファイル = 1フェス。複数
occurrences(参加日)を持ち、セットリストは参加日ごとに1つ
// ツアー登録用入力(例)
{
"type": "tour",
"title": "モーニング娘。'26 コンサートツアー秋",
"year": 2026, // tourId採番に使用。日程からの自動判定はしない(下記「ID自動採番」参照)
"season": "autumn", // 同上。"winter" | "spring" | "summer" | "autumn"
"releaseId": null, // 任意。Neon releases の id と対応
"lives": [
{ "date": "2026-10-03", "time": "18:00", "venueName": "日本武道館" }
],
"setlist": [
{ "seq": 1, "songTitle": "LOVEマシーン" }
]
}
// フェス登録用入力(例)
{
"type": "festival",
"name": "ROCK IN JAPAN FESTIVAL 2026",
"slug": null, // 任意。未指定時は name からラテン文字・数字を自動抽出
"occurrences": [
{
"date": "2026-08-09",
"time": "10:00", // 開催時刻(必須)
"performanceTime": "15:30", // 出演時刻(必須。不明なら "00:00")
"venueName": "国営ひたち海浜公園",
"setlist": [{ "seq": 1, "songTitle": "恋愛レボリューション21" }]
}
]
}
会場は venueName(会場名)のみで指定する。venues テーブルに同名の会場が既に登録済の場合は既存 venueId を再利用する。未登録の場合は venueName をそのまま Google Maps Geocoding API のクエリとして渡し、住所を別途入力させることなく緯度経度・都道府県を取得する(詳細は次項)。旧 register-tour-lives.ts は新規会場の都道府県・緯度経度を new-venues.csv に人手で用意する必要があったが、この人手調査の手間自体を解消することが本バッチ導入の主目的の一つであるため、住所の別途入力は求めない。
フェスの時刻: フェスの occurrences の各要素は、time(開催時刻。フェス全体の開演時刻)と performanceTime(出演時刻。モーニング娘。の出演時刻)を HH:MM 形式で指定する。どちらも省略不可で、出演時刻が不明な場合は "00:00" を指定する(ツアーの lives[].time と同じ「00:00 は時刻不明」の運用ルール。ライブ・フェス機能 テーブル設計書 §2-7参照)。開催時刻は不明になるケースがほぼ無いため、省略を許容しない。
セットリストの後追加: 公演(lives/occurrences)は新規作成時に必ず1件以上の予定日程を伴うため空配列は許容しない。一方セットリストは公演実施後に確定する情報のため、既に登録済のツアー/フェスに対して後から追加登録できる必要がある。
- ツアー: セットリストの登録は
tourId単位で行われ、個々のliveの重複判定とは独立している。そのため、既存のlivesをそのまま含めた入力(重複としてスキップされる)にsetlistだけ拡張したファイルを再実行すれば、新しい曲だけが追加登録される - フェス:
occurrencesの各要素が独立したFestivalレコード(festivalId)に対応するため、ツアーと同じ理屈にするには「日程・会場が既存 occurrence と重複する場合、新規Festivalは作成しないが、その既存festivalIdに対するsetlistの追加登録は試みる」という扱いが必要(詳細は次項の処理フロー手順6参照)
時刻の後追い登録(フェス): 開催時刻・出演時刻は、フェスの登録後に確定・訂正されることがある。日程・会場が既存の Festival と重複する occurrence については、その Festival の time・performance_time が入力と異なる場合に限り、この2フィールドのみを更新する(name・date・venue_id は従来どおり変更しない)。既存の時刻が NULL の場合も、入力の時刻で埋める。入力と同じ場合は更新しないため、同じファイルを再実行しても差分は生じない。更新した時刻は、次回のイベント同期バッチ(W6)の実行でタイムラインの events.time に反映される。
処理フロー
1. 入力JSONを読み込み、Zodスキーマで検証する(不正な場合はエラー終了、部分実行はしない)
2. 対象の会場名(venueName)を既存 venues と突き合わせ、未登録の会場を抽出する
3. 未登録の会場ごとに Google Maps Geocoding API へ venueName をクエリとして渡し、都道府県コード・緯度経度を取得する(次項参照)
→ 取得失敗(該当なし・タイムアウト等)した会場は警告ログを出力して保留し、その会場を含む live/occurrence の登録をスキップする(他の会場・日程の登録は続行)
4. 新規会場の venueId を採番し(都道府県コード + 連番3桁、[ライブ・フェス機能 テーブル設計書 §2-1](/design/live/live-data-design#2-1)参照)、venues に登録する
5. ツアー/フェスIDを採番する(採番ルールは次々項参照)
6. lives(ツアー)/ occurrences(フェス)を登録する。日程・会場(既存 or 手順4で新規登録した venueId)から live_id / festival_id を採番し、既存レコードと重複する場合はスキップする(register-tour-lives.ts の差分登録計画と同様の考え方)。ツアーはこの重複判定にかかわらず手順8でセットリストを追加登録する。フェスは日程・会場が既存 occurrence と重複する場合、新規 Festival は作成しないが、その既存 festivalId に対するセットリストの追加登録は試みる(「セットリストの後追加」参照)。あわせて、その既存 Festival の time・performance_time が入力と異なる場合は、この2フィールドのみを更新する(「時刻の後追い登録(フェス)」参照)
7. 手順6で新規登録する lives(ツアーの公演)について、公演日が在籍期間(joinDate〜gradDate。gradDate未設定は上限なし)に含まれる全メンバーを算出し、member_lives の登録計画を組み立てる(`scripts/lib/member-live-sync.ts`。詳細は[ライブ・フェス機能 テーブル設計書 §2-4](/design/live/live-data-design#2-4)参照)。member_lives の重複判定(PK: member_id, live_id)は手順6の live 自体の重複判定とは独立に行う。member_lives は `lives` テーブルのみを参照する設計(`live_id` への FK)のため、フェスの occurrences は対象外
8. セットリストの各曲について、songTitle を trackId 候補(該当する全件)に解決する(次々項参照)
→ 未解決(一致する楽曲が見つからない)の場合は警告ログを出力してその1曲のみ保留し、他の曲・後続処理は続行する
9. 手順4〜8の結果をまとめてトランザクションで Neon に登録する(venues → tours/festivals(新規フェスの作成と、既存フェスの時刻更新を含む) → lives → member_lives → setlists → setlist_tracks の順)
10. 登録件数(新規会場数・新規公演数・member_lives 新規紐付け数・既存フェスの時刻更新件数・保留件数)をログ出力する
会場ジオコーディング(Google Maps Geocoding API)
採用APIの選定経緯・比較検討は MorningStatusApp #1518 を参照。
- エンドポイント:
GET https://maps.googleapis.com/maps/api/geocode/json?address={venueName}®ion=jp&language=ja&key={GEOCODING_API_KEY}。addressパラメータには住所ではなくvenueName(会場名)をそのまま渡す。Google Maps Geocoding API は施設名等の自由記述クエリも解決できるため、入力側に住所を別途用意させない設計とした region=jpは検索結果の地理的な絞り込み(バイアス)用であり、レスポンスの表記言語には影響しない。language=jaを指定しないとaddress_componentsの都道府県名が英語(例:Tokyo)で返り、prefecture_codesとの照合に失敗するため必須- レスポンスの
results[0].geometry.location(lat/lng)を緯度経度として使用する - レスポンスの
results[0].address_componentsからadministrative_area_level_1(都道府県)を抽出し、prefecture_codes(ライブ・フェス機能 テーブル設計書 §2-5)のaliasesと照合して JIS 2桁コードに変換する statusがOK以外(ZERO_RESULTS・OVER_QUERY_LIMIT等)の場合は取得失敗として扱う- レート制限: 3,000リクエスト/分(公式ドキュメント値)。本バッチの想定呼び出し件数(1回の登録で数件〜十数件の新規会場)では制約にならないため、リトライ・スロットリング制御は設けない
ID自動採番
ツアーID: 年 + 季節コード(冬=A・春=B・夏=C・秋=D)。同一年・同一季節にツアーが重なることはないため、衝突時のサフィックス処理は不要(例: 2026D)。
- 年・季節はいずれも入力JSONの明示フィールド(
year: 数値、season:"winter" | "spring" | "summer" | "autumn")で指定する。ツアーの年・季節は公演日程から機械的に逆算しない。ツアー名の呼称(例: 秋ツアーが年をまたいで12月公演を含む等)は暦月の区分と一致するとは限らず、日程からの自動判定は境界を誤る可能性があるため、企画上の呼称として人手で確定している値をそのまま使う
フェスID: 年 - スラッグ - 連番(例: 2025-ROCKINJAPAN-1)。
- 年は各
occurrences[].dateから取得する。フェスはツアーと異なりoccurrencesの1要素が1Festivalレコード(festivalId 1件)に対応するため、要素ごとの単一日付から機械的に算出しても複数日にまたがる曖昧さは発生しない(ツアーのように明示フィールドを設ける必要はない) - スラッグは既定で
nameからラテン文字・数字のみを正規表現抽出(記号・スペース・日本語除去、大文字化)して自動導出する - 完全に日本語のみの
nameで抽出結果が空になっても、そのまま空文字のスラッグとして扱ってよい(一意性は年+連番側で担保されるため、スラッグは可読性向上のための補助的な役割に過ぎない) - 入力JSONの
slugフィールドで自動抽出結果を上書きできる slugに空文字列""を指定した場合も「未指定」と同じ扱いとし、自動導出にフォールバックする。空スラッグを許容するのは、直上の「完全に日本語のみのnameで抽出結果が空になった」場合のみであり、""の明示指定を「意図的な空スラッグ」とは解釈しない- 漢字→ローマ字変換ライブラリ(kuroshiro等)の新規導入は行わない
備考: 以前は input.slug ?? deriveSlug(input.name)(Nullish coalescing)を使っており、slug に空文字列が渡されても未指定と判定されず、festivalId が 2026--1 のような不正な形式になる不具合があった(#1527)。空文字列もフォールバック対象にするため、?? ではなく || を使うこと。
セットリストID: ライブ・フェス機能 テーブル設計書 §2-8の一般形式 {tourId or festivalId}-{3桁連番} に従い、${親ID(tourId or festivalId)}-${seqを3桁ゼロ埋め} で生成する(例: ツアーのセットリスト3曲目なら 2026D-003)。seq は入力JSONの setlist[].seq をそのまま使う。
ID採番の格納先(tour_id/festival_id のPK制約)自体はライブ・フェス機能 テーブル設計書 §2-2・同 §2-7を参照。
セットリストの trackId 紐付け
lib/track-match.ts の normalizeForMatch / buildTrackMatchMapFromPrisma を再利用する(register-manual-radio-episodes.ts のセットリスト曲名解決と同一ロジック)。曲名の正規化(空白・記号・ハイフン類・波ダッシュ等の除去+小文字化という緩い正規化)で一致した楽曲の trackId を該当する全件採用し、setlists テーブルの列ではなく ライブ・フェス機能 テーブル設計書 §2-9「setlist_tracks」の中間テーブルへ登録する(1曲が複数トラックに正しく該当しうるケースへの対応。正規化・紐付け方針の詳細はラジオオンエア データ設計書 §4-0「曲名正規化とハイフン文字種の吸収」を参照)。
エラーハンドリング
| エラー種別 | 動作 |
|---|---|
| 入力JSONのスキーマ検証失敗 | 処理全体を中断する(部分実行はしない) |
| 会場ジオコーディング失敗(該当なし・APIエラー) | 警告ログを出力し、当該会場を含む live/occurrence をスキップして続行する |
セットリスト曲名の trackId 未解決 |
警告ログを出力し、当該曲のみ保留して続行する |
| 日程・会場の重複(登録済) | 差分登録計画でスキップし、正常終了として扱う(register-tour-lives.ts と同様) |
ドライラン
--dry-run フラグ指定時は以下のように動作する。
- 会場ジオコーディング(Google Maps Geocoding API 呼び出し)は通常通り実行する。未登録会場の解決可否は課金対象の実APIで検証しないと確認できないため
- 手順4〜7の登録計画(新規会場・新規ツアー/フェス・既存フェスの時刻更新・新規公演・新規セットリスト・保留一覧)は通常通り組み立てる
- 手順8のトランザクションによる Neon への書き込みのみをスキップする
- 書き込む予定だった内容を、通常時のログ(手順9)と同じ形式で「[DRY RUN] 」プレフィックス付きで出力する
Google Maps Geocoding API は呼び出し課金対象、Neon への書き込みは本番データへの直接反映となるため、実行前に登録内容を確認する手段として設ける。
既存スクリプトとの関係
scripts/console/register-tour-lives.ts(CSV版、会場緯度経度・都道府県コードは人手入力)は、本バッチが機能を完全にカバーするため塩漬けとする。コードは削除せず残すが、以後のツアー・フェス登録では本バッチ(O4)を使用する。
運用手順書 docs/operations/tour-live-registration.md の更新は本設計書のスコープ外とする。本バッチ(MorningStatusApp #1520)およびデスクトップモード編集画面(MorningStatusApp #1522)の両方の実装完了後、実装内容を踏まえてまとめて更新する。
環境変数
| 変数名 | 必須 | 説明 |
|---|---|---|
DATABASE_URL |
○ | Neon PostgreSQL 接続文字列 |
GEOCODING_API_KEY |
○ | Google Maps Geocoding API キー。Repository secret |
定常運用スクリプト一覧
O1・O3・O4 を除く、繰り返し手動実行することを想定したスクリプト。
| スクリプト | 概要 |
|---|---|
scripts/console/check-playlist-integrity.ts |
プレイリストと Neon リリースの trackId 整合性を検証する |
scripts/console/find-missing-tour-album-links.ts |
ツアーとライブアルバムの紐づき漏れを抽出・表示する(docs/operations/tour-album-link-operations.md 参照) |
scripts/console/playlist-link.ts |
プレイリスト楽曲の YouTube リンクを検索・修正する CLI |
scripts/console/reconcile-manual-releases.ts |
手動登録リリースの releaseId を MusicBrainz ID(モーニング娘。本体固定)に洗い替える(手動確定モード対応)(リリース データ設計書 §4-4) |
scripts/console/register-manual-radio-episodes.ts |
手動用意した放送回 JSON を Neon のラジオ番組 DB に登録する(ラジオオンエア データ設計書 §4-4) |
scripts/console/register-tour-lives.ts |
CSV からツアー公演・会場情報を Neon に登録する(docs/operations/tour-live-registration.md 参照)。塩漬け: O4(フェス・ツアー登録バッチ)が機能を完全にカバーするため、以後のツアー・フェス登録には使用しない |
scripts/console/save-instagram-auth.ts |
Instagram に手動ログインし認証状態ファイルを保存する(W4 の Secret 運用手順) |
scripts/console/seed-member-map-layout.ts |
全メンバーの初期円形座標を Neon にシード登録する |
scripts/console/sync-tiktok-track-links.ts |
既存 TikTok リンクを修正し TrackLink と再照合登録する |
scripts/patch/fetch-release-images.ts |
Cover Art Archive からリリースイベントの jacket 画像を補完する |
scripts/patch/migrate-to-blob.ts |
ローカルの members.json を Blob へマージしてアップロードする(新規メンバー登録の定型フロー) |
scripts/patch/patch-birthplace.ts |
ローカルデータの出身地情報を Blob にマージ反映する |
scripts/patch/patch-instagram-reel-flag.ts |
指定アカウント(メンバー or 公式アカウント)のプロフィールグリッドを再確認し、既存投稿の isReel フラグをバックフィルする CLI(docs/operations/patch-instagram-reel-flag.md 参照、#1373) |
scripts/patch/patch-instructor.ts |
ローカルデータの指導者情報を Blob にマージしキャッシュを破棄する |
scripts/patch/patch-leader-generations.ts |
ローカルデータのリーダー世代番号を Blob にマージ反映する(リーダー交代時) |
scripts/patch/patch-member-colors.ts |
ローカルデータのメンバーカラーを Blob にマージ反映する(新メンバー加入時) |
scripts/patch/patch-member-graduate.ts |
指定メンバーを OG 卒業扱いに更新する CLI |
scripts/patch/patch-member-joining-routes.ts |
ローカルデータの加入ルートを Blob にマージ反映する |
scripts/patch/patch-member-nicknames.ts |
ローカルデータの愛称情報を Blob にマージ反映する |
scripts/patch/patch-official-sns.ts |
ローカルデータの公式 SNS リンクを Blob にマージ反映する |
一回限りパッチ一覧(実行済)
O2 を除く、特定の不具合修正・データ移行のために作成され、既に実行済で再実行を想定しないスクリプト。
| スクリプト | 概要 | 関連Issue |
|---|---|---|
scripts/console/delete-youtube-shorts.ts |
Neon の TrackLink から youtube-short 型を一括削除する | - |
scripts/console/restore-youtube-links.ts |
Blob の releases.json から youtube リンクを Neon に復元する | - |
scripts/patch/migrate-instagram-thumbnails.ts |
Instagram サムネイル URL を新形式に一括移行する | - |
scripts/patch/migrate-releases-to-neon.ts |
releases.json を Neon の Release/Track 等に一括移行する | #854 |
scripts/patch/migrate-tiktok-to-neon.ts |
tiktok/posts.json を Neon の tiktok_posts に移行する | #888 |
scripts/patch/backfill-ameba-captured-at.ts |
statusHistory の既存エントリへ capturedAt(収集日時)を手動投入する(新着判定を投稿日時基準から収集日時基準に変更した対応の表示確認用) | #1408 |
scripts/patch/patch-ameba-duplicate-status.ts |
statusHistory の重複 Ameba エントリを削除する | #1329 |
scripts/patch/patch-ameba-misattributed-status.ts |
他メンバーに誤帰属した Ameba 投稿を削除する | #1328 |
scripts/patch/patch-ameba-title-only-status.ts |
Ameba 由来 content を本文除去しタイトルのみに補正する | #1328 |
scripts/patch/patch-blog-history-pubdates.ts |
ブログ履歴の lastUpdated を RSS pubDate で補正・整列する | - |
scripts/patch/patch-morning-musume-subunit-ids.ts |
特定楽曲の unitId を正しいサブユニットへ修正する | #826 |
scripts/patch/patch-radio-onair-song-titles.ts |
誤抽出された放送曲名を是正し再紐付けする(PR #1202 以前登録分のみ対象) | #1292 |
scripts/patch/patch-tampopo-unit-id.ts |
unitId「tampopo」の誤字を「tanpopo」に修正する | - |
scripts/patch/seed-ameba-sync-status.ts |
Ameba同期状態(ameba/sync-status.json)の初期データを投入する(sync-members 初回実行前の表示確認用) |
#1387 |
scripts/patch/patch-empty-slug-festival-id.ts |
空スラッグのためfestivalIdが{年}--{連番}の不正形式になっていたFestivalレコードを、nameから導出した正しいスラッグへ補正する |
#1538 |
scripts/patch/patch-radio-onair-track-candidates.ts |
radio_onair_song_tracks全体を、非シングル曲の誤紐付けを解消した修正後のトラック解決ロジックで再解決する(単曲専用パッチpatch-lonely-but-not-alone-track-links.tsの汎用版への置き換え) |
#1575 |
scripts/patch/backfill-member-lives.ts |
全メンバー・全公演を対象に、在籍期間からmember_livesの欠落分を再同期する(フェス・ツアー登録バッチ側がmember_livesへ書き込んでいなかったことによる欠落の一括補完) |
#1619 |
開発用・共通ヘルパー
単体実行を想定しない補助コード、または開発時のアドホックな動作確認用コード。
| スクリプト | 概要 |
|---|---|
scripts/console/test-api.ts |
ローカル API への PUT リクエストを試す開発用テストスクリプト |
lib/revalidate-cache.ts |
パッチスクリプトおよびデスクトップモードのAPIルート(app/api/members・app/api/link-editor・app/api/tracks/[id]/youtube-link)から呼ばれる本番Webキャッシュ破棄処理の共通ヘルパー(単体実行不可、#1600でscripts/patch/から移設) |
改訂履歴
| 版 | 更新日 | 変更内容 |
|---|---|---|
| 4.26 | 2026-09-29 | 【対応する実装: 実装時に追記(MorningStatusApp#1680)】W3を「プレイリスト集計・インデックス生成スクリプト」から「プレイリストデータ同期スクリプト」に改称。playlists.json・派生インデックスのBlob→Neon移行(プレイリスト機能 データ設計書 §7)に伴い、環境変数表をDATABASE_URLのみに変更し、RELEASES_BLOB_URLのstale参照問題と派生インデックス生成の廃止経緯を備考として追記(MorningStatusApp#1677, morning-status-blume#166) |
| 4.25 | 2026-09-27 | 【対応する実装: v11.0.1(MorningStatusApp#1618)】W16(Blob URLシークレット診断バッチ)を新規追加。Repository secrets の *_BLOB_URL 系5件(Neon移行済のRELEASES_BLOB_URLを除く)の疎通・形式を毎日 JST 09:00 に検証し、異常時はジョブを失敗させる読み取り専用の診断バッチ。ワークフロー一覧に行を追加(MorningStatusApp#1618) |
| 4.24 | 2026-09-22 | O1(曲対比インデックス生成スクリプト)を欠番化。対比機能を手動作成方式(Neon DB)へ再構築したことに伴い、タイトル前方一致による自動生成バッチを廃止(曲対比機能 データ設計書 参照)。それ以外のスクリプト一覧からbuild-comparison-index.tsの行を削除し、フル仕様記載件数を「4件(O1〜O4)」から「3件(O2〜O4)」に修正(MorningStatusApp#1633, morning-status-blume#148) |
| 4.23 | 2026-09-22 | 【対応する実装: 実装時に追記(MorningStatusApp#1545)】W6(イベント同期バッチ)のフェスの変換に、time(出演時刻を優先し、不明なら開催時刻)を追加。O4(フェス・ツアー登録バッチ)の入力フォーマットに、フェスの occurrences[].time(開催時刻)・performanceTime(出演時刻)を追加し、日程・会場が既存のフェスと重複する場合に時刻のみを後から更新する仕様(時刻の後追い登録)を追記。処理フロー手順6・9・10とドライランの記述を追随して更新(MorningStatusApp#1545) |
| 4.22 | 2026-09-14 | O4(フェス・ツアー登録バッチ)の処理フローに、公演登録時の対象メンバー算出・member_lives登録計画の組み立てステップを追加し、トランザクション登録順・ログ出力にmember_livesを反映。一回限りパッチ一覧にbackfill-member-lives.ts(member_lives欠落分の再同期)を追加。詳細はライブ・フェス機能 テーブル設計書 §2-4参照(MorningStatusApp #1619) |
| 4.21 | 2026-09-09 | 事前に存在したevents テーブル定義書へのリンクテキスト(4箇所)を、対象ドキュメントのH1タイトル変更(「events テーブル定義書」→「イベントテーブル定義書」)に追随して更新(morning-status-blume#113レビューで検出) |
| 4.20 | 2026-09-08 | W2 の処理フロー・リリース正規化節に、MusicBrainz 取得タイトルの記号表記正規化(normalizeTitlePunctuation。ハイフン・三点リーダー)を追記(#1553, #1594)。定常運用スクリプト一覧にscripts/console/reconcile-manual-releases.ts(手動登録リリースのMusicBrainz ID洗い替え)を追加、詳細はリリース データ設計書 §4-4(#1594) |
| 4.19 | 2026-09-07 | revalidate-cache.tsをscripts/patch/からlib/へ移設。デスクトップモードのAPIルート(app/api/members・app/api/link-editor・app/api/tracks/[id]/youtube-link)からも本番Webキャッシュ破棄を呼ぶようになったことに伴う(MorningStatusApp #1600) |
| 4.18 | 2026-09-06 | W6にmanual-events.json単体をDB/API呼び出しなしで検証できる--validate-onlyモードを追加。実行タイミング表・検証専用モード節を新設(MorningStatusApp #1592)。あわせて処理フロー・upsertキー規則に既存の記載漏れだったフェス(festivals)取得・変換ステップを追記(PR #109レビューで検出) |
| 4.17 | 2026-09-05 | W6の処理フロー手順5に、manual-events.jsonのevents配列を1件ずつバリデーションし不正な要素のみスキップする仕様を追記(フェイルセーフにも追加)。手順6のsongs解決に、artist指定曲(他アーティスト楽曲)はtrackId解決をスキップする仕様を追記。手順10の同期完了ログにvalidationErrors件数を追加(MorningStatusApp #1590, #1585) |
| 4.16 | 2026-08-30 | イベント同期バッチ(W6)に、手動イベントの venueName/songs 解決ステップ(会場ジオコーディング・トラック候補解決)を追加。upsertキー規則に manual 行を追加(既存の記載漏れも合わせて是正)。環境変数表に GEOCODING_API_KEY(任意)、ドライラン動作表に未設定時のグレースフルスキップを追加(MorningStatusApp #1528, morning-status-blume#67) |
| 4.15 | 2026-08-30 | 一回限りパッチ一覧に、単曲専用パッチpatch-lonely-but-not-alone-track-links.ts(一覧未掲載だった)を汎用化したpatch-radio-onair-track-candidates.tsを追加(トラック候補解決ロジックの制約追加に伴う置き換え、詳細はラジオオンエア データ設計書§4-0参照。MorningStatusApp #1575) |
| 4.14 | 2026-08-29 | 収集系ワークフロー6件(W1・W4・W5・W6・W7・W9)の定期実行スケジュールを3分後ろ倒し(同時刻集中による遅延・実行スキップ対策)。あわせてW14の実行タイミング記載の誤記(「23:00」表記のまま据え置かれていたが実際のcronは「翌1:00」)を是正し、今回のオフセットを反映した「翌01:03」に更新(MorningStatusApp #1566) |
| 4.13 | 2026-08-28 | セットリストのtrackId紐付けを、該当する全トラックを候補としてsetlist_tracks中間テーブルへ登録する方式に変更(1曲が複数トラックに正しく該当しうるケースへの対応)。処理フロー手順8の登録順にsetlist_tracksを追加。SetlistEntrySchemaをラジオ側と共有のSongEntrySchema(scripts/lib/song-entry-schema.ts)ベースに変更する方針を追記。入力JSON例のreleaseIdコメントを「releases.json」から「Neon releases」に是正(MorningStatusApp #1529, morning-status-blume#80) |
| 4.12 | 2026-08-23 | セットリストのtrackId紐付けとTikTokのTrackLink生成の参照先をlib/track-match.tsに更新。曲名正規化の詳細説明はラジオオンエア データ設計書に集約し、本書からは参照リンクのみとした(MorningStatusApp #1552) |
| 4.11 | 2026-08-22 | W5・W7の処理フローに、OG収集ステップがif: always()で公式収集の成否と独立して実行されることを追記(統合前の別ワークフロー運用時と同等のフォールトアイソレーションを維持するための実装、#1546, PR #1547のセルフレビューで検出) |
| 4.10 | 2026-08-22 | TikTok(W5)・YouTube(W7)の公式/OG収集ワークフローを1本ずつに統合(collect-tiktok.yml・collect-youtube.yml)。旧W8・W15は本文(W7・W5)に統合し欠番化。Ameba実データの実測(旧時刻20:00時点で同日投稿の8.8%しか投稿済でなく大半が翌日収集に持ち越されていた。23:00時点なら96.9%)に基づき、収集系ワークフロー全体の実行時刻を3時間後ろ倒し(W1: 23:00、W4: 23:30、W5: 翌0:00、W7: 翌0:30、W14: 翌1:00)。W1・W4・W7の実行タイミング記載が実際のcron設定と乖離していた誤記も合わせて是正(#1546) |
| 4.9 | 2026-08-22 | 一回限りパッチ一覧にpatch-empty-slug-festival-id.tsを追加。#1527のバグ(??が空文字列のslugをフォールバックしない)により生成された不正形式festivalId(例: 2026--1)を補正するデータパッチ(#1538) |
| 4.8 | 2026-08-21 | O4「ID自動採番」フェスIDのスラッグ仕様に、slugへ空文字列""を指定した場合も未指定と同じ扱い(自動導出にフォールバック)とする旨を明記。実装側で??(Nullish coalescing)が空文字列をフォールバックせず採用してしまうバグを検出したことに伴う仕様明確化(#1527) |
| 4.7 | 2026-08-20 | ラジオオンエア データ設計書へのリンクテキストをタイトル変更(「ラジオオンエア収集 設計書」→「ラジオオンエア データ設計書」)に追随して更新(#1523) |
| 4.6 | 2026-08-19 | 会場ジオコーディングのエンドポイントにlanguage=jaを追加(#1520実装後の実機テストで、region=jpだけでは都道府県名が英語表記で返りprefecture_codesとの照合に失敗する不具合を検出)(#1520) |
| 4.5 | 2026-08-19 | O4の会場指定方式をvenueName+addressからvenueNameのみに変更し、Google Maps Geocoding APIへはvenueNameをそのままクエリとして渡す方式に修正(住所の別途入力を求めない)。セットリストの後追加(ツアー・フェスそれぞれの既存レコード再利用ルール)・ドライラン(--dry-run)を新設。いずれも#1520実装検討時に検出した設計漏れ(#1520) |
| 4.4 | 2026-08-19 | O4「ID自動採番」にセットリストID(setlistId)の生成規則を追記(#1519実装時の#1520レビューで、tourId・festivalIDのみ記載されておりセットリストIDが抜けていたことを検出)。あわせて冒頭の「最終更新」を実際の最新改版日に同期した(4.3反映時に更新漏れ) |
| 4.3 | 2026-08-19 | フェス・ツアー登録バッチ(O4、scripts/console/register-tour-festival.ts)を新設。JSON入力フォーマット・会場ジオコーディング(Google Maps Geocoding API、#1518)連携・ID自動採番(ツアー/フェス)・セットリストtrackId紐付け・フェイルセーフ方針を記載。既存のregister-tour-lives.tsはO4に機能が統合されるため塩漬けと明記(#1519) |
| 4.2 | 2026-08-15 | W14のMemberSummaryEntryにretroDatesフィールドを追加し、遡及収集(実投稿日がソースの前回同期時刻以前)の判定基準・マージ仕様を明記(対象: blog/tiktok/tiktok-og/youtube-og。instagramは実投稿日未取得のため対象外)(#1502) |
| 4.1 | 2026-08-09 | W5をPlaywright + Vercel Blob前提の旧アーキテクチャ記述(#888のNeon移行前のまま放置されていた)からRapidAPI + Neon前提の現行実装に全面的に書き直し、tiktok_sync_statusのofficial用ウィンドウ(officialLastSyncedAt等)を更新するステップを追記。W15にもtiktok_sync_statusのog用ウィンドウを更新するステップを追記し、「更新しない」としていた§注意を経緯の記録に更新。TOP新着投稿セクションの新着判定を「同日capturedAt + slice(0, 10)」の件数キャップ方式からYouTube(W7・W8、#1450)と同じofficial/og独立ウィンドウ方式に置き換えたことに伴う変更(詳細は「TikTok投稿機能 データ設計書」§4、#1489) |
| 4.0 | 2026-08-09 | OGメンバーのTikTok個人アカウント収集バッチ(W15)を新設(YouTube OG収集・W8と同方式)。ワークフロー一覧・W14のデイリーダイジェストデータ構造(tiktok-ogソース追加)・対象アカウント一覧のtiktok-officialの記述(#1458完了を反映)を更新(#1478) |
| 3.9 | 2026-07-31 | W14のメール本文集計(buildDigestEmailBody)の記述を実装(#1457)に合わせて修正。公式アカウントのセクション見出しを「モーニング娘。公式」から「公式アカウント」に訂正し(Instagram公式アカウントにはモーニング娘。公式でないミニモちゃんも含まれるため)、集計内容もSNS単位でのアカウント名付き内訳表示であることを明記 |
| 3.8 | 2026-07-31 | W14のデイリーダイジェストファイルのデータ構造を、公式・グループアカウント集計方法の統一方針に合わせて改訂。OfficialSummaryEntryをinstagram-official/tiktok-official/youtube-officialの3ソース共通型(アカウント単位カウント)に拡張し、Instagram・TikTokがMemberSummaryEntry.entriesに疑似メンバーとして間借りする現行実装を廃止する設計に変更。W4・W5の処理フロー・Blob書き込みパス表に新ファイル(instagram-official.json・tiktok-official.json)を追記(#1423) |
| 3.7 | 2026-07-30 | W7・W8のyoutube_sync_status更新記述を、official/og独立カラム構成に合わせて修正(PRレビューで単一ウィンドウ共有の回帰を検出したため。詳細は「YouTube投稿一覧機能 データ設計書」§6.2、#1450) |
| 3.6 | 2026-07-30 | W7・W8の処理フローに youtube_sync_status(Neon)を更新するステップを追記。新着投稿のTOP表示に使う新着ウィンドウ判定の同期ステータス書き込み処理が抜けていたため是正(詳細は「YouTube投稿一覧機能 データ設計書」§6.2、#1450) |
| 3.5 | 2026-07-25 | W14にデイリーダイジェストファイルのデータ構造節を新設し、MemberSummaryEntry/OfficialSummaryEntryの型・公式グループアカウントの扱い(ソースごとに管理方法が異なる点、Instagram2アカウント分がメール集計で合算される点)を明記。W1・W4・W5・W7・W8の処理フロー・Blob書き込みパス表にダイジェストファイル書き込みの記載が欠けていたため追記(#1416) |
| 3.4 | 2026-07-24 | 一回限りパッチ一覧にbackfill-ameba-captured-at.tsを追加。新着判定基準を投稿日時から収集日時(StatusEntry.capturedAt)に変更した対応の表示確認用(#1408) |
| 3.3 | 2026-07-20 | W1にAmeba新着表示用の同期ステータス書き込み(ameba/sync-status.json)・GITHUB_EVENT_NAME環境変数を追記。一回限りパッチ一覧にseed-ameba-sync-status.tsを追加(#1387) |
| 3.2 | 2026-07-20 | 環境変数の使用状況棚卸し(#1363)で判明した各バッチの環境変数テーブルの欠落を是正。W2にUNITS_BLOB_URL・REFETCH_ALL_SINGLES、W5にRAPIDAPI_KEY、W7・W8にBLOB_READ_WRITE_TOKEN、W10にRELEASES_BLOB_URLを追記。横断的な環境変数一覧は新規 環境変数一覧 を参照 |
| 3.1 | 2026-07-20 | デイリーダイジェスト配信バッチ(scripts/workflow/send-daily-digest.ts)の本文セクションを新設(W14)。概要テーブルには記載があるが本文セクションが存在しなかった状態を解消。エラーハンドリング節を追加し、Blob 削除失敗時にメール送信自体がスキップされる挙動を明記(#1377, PR #1388 レビュー対応) |
| 3.0 | 2026-07-20 | ドキュメント全体を「ワークフロー」「それ以外のスクリプト」の2カテゴリに再構成し、カテゴリごとの連番(W1〜W13・O1〜O3)に変更(カテゴリを跨いだ追加のたびに発生していた全体繰り下げを解消)。§4・§8(旧番号)のスクリプトパス誤記(scripts/console/・scripts/patch/ 配下の実際の配置が未反映)を是正。Genius リンク同期・モーニング女学院放送データ収集・ヤングタウン土曜日収集・メイボンソワ選曲データ収集の本文セクションを新設(W10〜W13、#1378)。scripts/console/・scripts/patch/ 配下の未記載スクリプト32件を「それ以外のスクリプト」に一覧化し、定常運用(18件)/一回限りパッチ(12件)/開発用・共通ヘルパー(2件)に分類(#1376) |
| 2.16 | 2026-07-19 | 概要テーブル: スクリプトパス9件を実際の配置(scripts/workflow/ 配下)に是正。モーニング女学院・メイボンソワ・Geniusリンク同期の状態を実態(定期実行停止・手動実行のみ)に修正し、同様に定期実行停止済だったディスコグラフィー同期・プレイリストリンク同期の状態も是正。欠落していたヤングタウン土曜日収集・デイリーダイジェスト配信の2行を追加(#1376) |
| 2.15 | 2026-07-19 | Ameba RSS 解析セクションの詳細記述を「Amebaブログ機能 データ設計書」に集約し、二重管理を解消(#1367) |
| 2.14 | 2026-07-18 | OG_SYNC_WEEKDAYS / WEB_SEARCH_SYNC_WEEKDAYS の保存先を Repository secrets から Repository variables に修正(非機密な短い数字列がCIログの無関係な箇所を誤マスクする不具合の対策、#1347) |
| 2.13 | 2026-07-13 | メンバー近況同期バッチ: 複数新着ブログ投稿の蓄積処理で、ループ後の最新投稿蓄積にも重複チェック(shouldSkipAccumulation)を追加し、同一投稿が2件連続で蓄積されるバグを修正(#1329) |
| 2.12 | 2026-07-12 | メンバー近況同期バッチ: Ameba RSS 解析セクションを新設。CDATA抽出の改行対応、content にタイトルのみを格納する仕様(著作権保護のため本文を含めない)、グループブログでは本文一致によるフォールバックを行わない投稿者判定の変更を明記(#1328) |
| 2.11 | 2026-07-12 | Instagram 投稿収集バッチ: OGメンバーの収集を OG_SYNC_WEEKDAYS で指定した曜日のみに制限するよう修正(毎日収集されていたバグの修正、#1297) |
| 2.10 | 2026-07-11 | メンバー近況同期バッチ: Exaクレジット消費削減のため公式SNS登録状況に応じた検索範囲の排他化・公式SNS未登録メンバーの一般Web検索週1回制限(WEB_SEARCH_SYNC_WEEKDAYS)を追加。Exa Search API の料金体系を追記(#1307) |
| 2.9 | 2026-07-04 | メンバー関係性マップ 静的エッジシードスクリプトをセクション12と概要テーブルに追加。スコアリング組み込みによる変更点(#1269)を追記 |
| 2.8 | 2026-07-04 | メンバー関係性マップ 動的エッジ同期バッチ(#1236, #1263)をセクション11と概要テーブルに追加 |
| 2.7 | 2026-06-13 | ディスコグラフィー同期バッチ: リリース単位 MV 取得廃止・定期実行停止・実行タイミングを手動のみに変更(#1123) |
| 2.6 | 2026-06-11 | YouTube OG チャンネル投稿収集バッチ(#1104)をセクション10と概要テーブルに追加。OG 曜日判定 shouldSyncOG を scripts/lib/og-sync.ts に共通化 |
| 2.5 | 2026-06-11 | YouTube 公式チャンネル投稿収集バッチ(#1103)をセクション9と概要テーブルに追加 |
| 2.4 | 2026-06-10 | Instagram 投稿収集: 収集時のサムネイル URL 有効性検証を廃止(非ブラウザリクエストへの 429 対策・#1114)。処理フローから未使用の og:image 取得の記述を削除(PR #1115 レビュー対応) |
| 2.3 | 2026-06-02 | 非楽曲トラック削除パッチスクリプト(#1057)のセクションを追加 |
| 2.2 | 2026-06-02 | ディスコグラフィー同期バッチ: 非楽曲トラック自動除外(recording.video フラグ・タイトルパターン)とシングル全エディションマージを追加(#1055) |
| 2.1 | 2026-05-24 | イベント同期バッチ(#927)を概要テーブルとセクション7に追加 |
| 2.0 | 2026-05-22 | モーニング女学院放送データ収集・メイボンソワ選曲データ収集バッチ(#921/#923)を概要テーブルに追加 |
| 1.9 | 2026-05-14 | TikTok 投稿収集バッチ(#805)を追加 |
| 1.8 | 2026-05-14 | sync-discography・sync-genius-links・sync-playlist-links の書き込み先を Vercel Blob から Neon DB に移行。ドライラン条件を DATABASE_URL 未設定に変更(#856, #851) |
| 1.7 | 2026-05-07 | Shorts 取得クエリに Shorts キーワードを追加し、同期ステータスに Shorts 取得件数を追加(#830) |
| 1.6 | 2026-05-06 | ディスコグラフィー同期バッチに syncTrackShortLinks(YouTube Shorts URL 付与)を追加し、処理フローと環境変数表を更新(#774) |
| 1.5 | 2026-04-29 | Instagram サムネイル取得方式への移行(Blob 保存の廃止)(#751) |
| 1.4 | 2026-04-26 | Instagram 投稿収集バッチ(#718)の設計を追加 |
| 1.3 | 2026-03-29 | エイリアスファイル・除外パターンの説明追加、処理概要を最新化(#508) |
| 1.2 | 2026-03-28 | 曲対比インデックス生成スクリプト(#488)の設計を追加 |
| 1.1 | 2026-03-27 | プレイリスト集計・インデックス生成スクリプトの設計、および選曲リリースの自動解決ロジックを追加 |
| 1.0 | 2026-03-21 | 初版作成 |