コンテンツにスキップ
Documents for MorningStatusApp
Esc
↑↓移動↵開く⌘Jプレビュー
このページの内容

ラジオオンエア データ設計書

最終更新: 2026-08-30

1. 概要

ラジオ番組のオンエア情報(放送回・出演メンバー・オンエア楽曲)をスクリプトで自動収集し、Neon DB(Postgres)で管理・表示する機能の設計書。

対象番組:

番組 アカウント / 収集元 スクリプト
モーニング女学院(ラジオ日本) X @morning1422 scripts/workflow/collect-onair-morning.ts
ヤングタウン土曜日(MBSラジオ) X @yando_staff scripts/workflow/collect-onair-yando.ts
メイボンソワ(STV) STVサイト HTML scripts/workflow/collect-bonsoir-songs.ts

2. DBスキーマ

ラジオの放送データは「放送日(回)」単位で管理されるため、ライブ機能と同様に Neon (Postgres) で管理する。

2-1. radio_shows(ラジオ番組)

将来的に他の番組にも対応できるよう、番組単位のテーブルを用意する。

CREATE TABLE "radio_shows" (
  "showId"   VARCHAR(50)   PRIMARY KEY, -- 例: 'morning-jogakuin2026'
  "name"     VARCHAR(255)  NOT NULL,    -- 例: 'モーニング娘。'26 モーニング女学院〜放課後ミーティング〜'
  "station"  VARCHAR(100)               -- 例: 'ラジオ日本'
);

2-2. radio_episodes(放送回)

各放送回の情報を管理。

CREATE TABLE "radio_episodes" (
  "episodeId" VARCHAR(50)   PRIMARY KEY, -- 例: 'mj-20240504'
  "showId"    VARCHAR(50)   NOT NULL REFERENCES "radio_shows"("showId"),
  "date"      DATE          NOT NULL,
  "title"     VARCHAR(255)               -- 回題(あれば)。例: '737時間目'
);

2-3. radio_members(出演メンバー)

放送回ごとの出演メンバー。日直(メインパーソナリティ)かどうかのフラグを持つ。 PK は (memberId, episodeId) 順(メンバー → 出演回の検索を主とする)。

CREATE TABLE "radio_members" (
  "memberId"  VARCHAR(50)   NOT NULL,    -- Blob members.json の id と対応
  "episodeId" VARCHAR(50)   NOT NULL REFERENCES "radio_episodes"("episodeId"),
  "isDayDuty" BOOLEAN       NOT NULL DEFAULT FALSE, -- 日直フラグ
  PRIMARY KEY ("memberId", "episodeId")
);
CREATE INDEX "radio_members_episodeId_idx" ON "radio_members"("episodeId");

2-4. radio_onair_songs(オンエア楽曲)

各回で放送された楽曲。

CREATE TABLE "radio_onair_songs" (
  "onairId"    SERIAL        PRIMARY KEY,
  "episodeId"  VARCHAR(50)   NOT NULL REFERENCES "radio_episodes"("episodeId"),
  "trackOrder" INT           NOT NULL,   -- 放送順
  "songTitle"  VARCHAR(255)  NOT NULL,   -- 表示用タイトル
  "artistName" VARCHAR(255)              -- アーティスト名(外部アーティスト楽曲向け)
);

artistName はメイボンソワ(§4-3)で利用する拡張フィールド。NULL 許容のため既存データへの影響なし。

trackId への紐付けは本テーブルの列では持たず、§2-5「radio_onair_song_tracks」の中間テーブルで管理する(1曲が複数トラックに正しく該当しうるため。詳細は§4-0参照)。

2-5. radio_onair_song_tracks(オンエア楽曲 × トラック紐付け)

1件の RadioOnairSong が複数の Track(同一タイトルのCD盤・配信版シングル等)に該当しうるための中間テーブル。

CREATE TABLE "radio_onair_song_tracks" (
  "onairId" INT           NOT NULL REFERENCES "radio_onair_songs"("onairId") ON DELETE CASCADE,
  "trackId" VARCHAR(255)  NOT NULL REFERENCES "tracks"("trackId"),
  PRIMARY KEY ("onairId", "trackId")
);
CREATE INDEX "radio_onair_song_tracks_trackId_idx" ON "radio_onair_song_tracks"("trackId");

member_lives(ライブ・フェス機能 テーブル設計書 §2-4)と同じ、複合PKによる中間テーブルのパターン。曲詳細ページ(§3-3)の逆引き(この楽曲が過去にどの放送回で流れたか)は trackId のインデックスを使ったJOINで実現する。

セットリスト側にも同じパターンの中間テーブル(setlist_tracks)があり、詳細はライブ・フェス機能 テーブル設計書 §2-9を参照。


3. UI 設計

3-1. ラジオ番組一覧・放送回一覧ページ

  • /radio: 番組一覧
  • /radio/[show_id]: 放送回(エピソード)一覧。カレンダーまたは月別アーカイブ形式。
  • 各番組はオンエア情報確認用の外部リンク(Xアカウントまたは番組サイト)を一覧・詳細の両画面に表示する。RadioShow テーブルを動的にループする実装のため、lib/radio-helpers.ts の RADIO_SHOWS(showId prefix → 番組名・放送局・確認用リンク)で静的にマッピングする(#1333, #1344)。
  • 表示は components/RadioShowLinkBadge.tsx によるバッジ形式(ロゴ画像は使わず、サービスのブランドカラーを背景色にしたテキストバッジ。components/ExternalLinkBadge.tsx と同じ考え方)。Xアカウントは@アカウント名をバッジ文言、黒背景(Xの現行ブランドカラー)。番組サイトは「公式サイト」をバッジ文言、グレー背景(#1344レビューより)。
番組 リンク種別 バッジ文言 URL
モーニング女学院 Xアカウント @morning1422 https://x.com/morning1422
ヤングタウン土曜日 Xアカウント @yando_staff https://x.com/yando_staff
井上春華のはるさんち 番組サイト 公式サイト https://hicbc.com/radio/harusanchi/
メイボンソワ 番組サイト 公式サイト https://www.stv.jp/radio/bonsoir/senkyoku/index.html

3-2. 放送回詳細ページ

  • /radio/episodes/[episode_id]
  • 表示内容: 放送日、出演メンバー(日直にはバッジ等を表示)、オンエアリスト(楽曲一覧)。
  • 紐づく Track(§2-5 radio_onair_song_tracks)が1件以上ある場合、曲詳細ページ (/songs/[id]) へのリンクを該当件数分表示する(CD盤・配信版シングルのように複数のリンク先が並ぶことがある)。
  • 紐づく Track が0件(外部アーティスト楽曲)の場合、artistName が設定されていれば曲名の隣にアーティスト名を併記する(非リンク)。紐づく Track が1件以上ある場合はリンク表示を優先し、artistName は表示しない(#1501)。
    • 備考: 紐づく Track の有無と artistName は原則として排他的に設定される想定だが、§4-0「trackId未解決曲の再紐付け」のガード漏れにより両方が設定される可能性が残っている(詳細は§4-0参照)。その場合も本仕様(Track リンク優先)に従う。

3-3. 曲詳細ページへの追加

  • /songs/[id] の「オンエア情報」セクション。
  • この楽曲が過去にどの放送回で流れたかをリスト表示する。

3-4. メンバー詳細ページへの追加

  • /members/[id] の「ラジオ出演」セクション。
  • 過去の出演回をリスト表示。日直回は強調する。

3-5. タイムラインのアクセントカラー

タイムライン(年ビュー・週ビュー・日ビュー)のイベントカードは、type === "media" のとき番組名(タイトル文字列の部分一致)に応じてアクセントカラー(左ボーダー・テキストアート背景色)を表示する。実装は components/EventCard.tsx の RADIO_SHOW_COLORS。

番組 色 カラーコード
モーニング女学院 オレンジ #fb923c
ヤングタウン土曜日 スカイブルー #38bdf8
メイボンソワ バイオレット #a78bfa
井上春華のはるさんち エメラルドグリーン #34d399

未登録の番組はアクセントカラーなし(無地表示)となる。色は番組を視覚的に区別するための任意色であり、パーソナリティのメンバーカラーとは連動しない。新規番組追加時は、既存4色と被らない色を RADIO_SHOW_COLORS に追加すること。


4. 番組別 収集設計

4-0. 番組共通の楽曲登録・紐付け方針

番組ごとの収集元(X投稿・HTMLスクレイピング・手動データ等)によらず、RadioOnairSong への楽曲登録・trackId紐付けは以下の方針を共通とする。

楽曲未紐付け時の警告ログ

MM関連曲(アーティスト名なし)としてtrackId解決を試みたが Track テーブルに一致する曲が見つからなかった場合は、楽曲未登録の可能性 として logger.warn で警告を出す(register-manual-radio-episodes.ts, collect-onair-morning.ts で実施)。サイレントに trackId: null のまま処理を続けると、紐付け漏れに気づけないため。

trackOrder の重複防止

同一放送回内で trackOrder が重複したまま RadioOnairSong.createMany に渡すと、@@unique([episodeId, trackOrder]) 制約違反でスクリプトごと停止する。収集元パース時点(collect-onair-morning.ts の groupTweetsByEpisode 等)または入力バリデーション時点(register-manual-radio-episodes.ts の手動JSONスキーマ)で重複を検出・排除すること。

trackId未解決曲の再紐付け(再実行時)

放送時点でリリース未登録だった曲(新曲の場合など)は trackId: null で登録されるが、後日 Track テーブルに追加された際に再実行で自動的に紐付けられるよう、trackId: null かつ artistName: null の RadioOnairSong を対象とした再紐付けパスを、収集・登録処理の最後に設ける(collect-onair-morning.ts, register-manual-radio-episodes.ts, collect-bonsoir-songs.ts で実施)。

備考: collect-bonsoir-songs.ts は他の2スクリプトと異なり episodeId によるスコープ絞り込みを行わず、DB全体の RadioOnairSong を対象に再紐付けを実行する(意図的な設計)。bonsoirの収集(collectEpisodes)は既存登録済エピソードに到達した時点で遡及を打ち切る方式のため、一度登録されたエピソードは以後どの実行でも再処理対象にならない。episodeId スコープを適用すると、過去エピソードの未解決曲が後日 Track 登録された際に拾い直すという本方針の目的そのものが失われてしまうため、DB全体スキャンを維持する。

旧仕様の記載(バグの経緯): 以前は collect-bonsoir-songs.ts の再紐付けパスが trackId: null のみを条件としており artistName: null のガードが入っていなかった(#1523で是正)。bonsoir は artistName を持つ楽曲(外部アーティスト楽曲)を登録する唯一の収集元であり、登録直後にこの再紐付けパスがDB全体を対象に実行されるため、曲名がMM楽曲と緩い正規化で一致した場合に trackId と artistName が両方設定されるレコードが生じ得た(PR #61 レビューで検出)。

なお collect-bonsoir-songs.ts は #1333 により定期実行(schedule トリガー)を停止済で、workflow_dispatch による手動実行のみが可能な状態(詳細は§4-3参照)。新規放送分は §4-4 の手動登録に統一されているため、このガード漏れが実際に踏まれる機会は限定的(スクリプトを手動実行した場合のみ)だった。

曲名正規化とハイフン文字種の吸収

曲名からtrackIdを解決する正規化処理(normalizeForMatch・buildTrackMatchMapFromPrisma・findBestMatch)は lib/track-match.ts に置き、ラジオ(本設計書)・セットリスト(§「セットリストのtrackId紐付け」、バッチ設計書)・TikTok(TikTok投稿機能 データ設計書)が共通で利用する。正規化では、ASCIIハイフン(U+002D)だけでなく、MusicBrainz由来のトラックタイトルに使われるタイポグラフィハイフン類(HYPHEN U+2010〜HORIZONTAL BAR U+2015、MINUS SIGN U+2212等)・波ダッシュ(U+301C)・全角チルダ(U+FF5E)を同一の記号として除去し、文字種の違いによる表記ゆれを吸収する。同一正規化キーに複数トラックが該当する場合は「single優先、非single同士は最古リリース日優先」で1件を採用する。

関数名に Title を含めていないのは、TikTok側(titleMatchesCaption、TikTok投稿機能 データ設計書)では投稿キャプション(曲名ではない自由記述の本文)にも同じ正規化処理をかけるため。対象を「タイトル」に限定しない汎用的な名前にしている。

同一正規化キーに複数トラックが該当する場合、候補にシングル形式(format === 'single')が1件以上含まれるときは、シングル形式の候補のみを全トラック候補として採用し(例: 同一タイトルのCD盤シングルと配信版シングルが別トラックとして存在するケース)、§2-5「radio_onair_song_tracks」(セットリスト側はライブ・フェス機能 テーブル設計書 §2-9「setlist_tracks」)の中間テーブルへ全件登録する。1トラックのみ該当する通常のケースも同じ中間テーブルに1行登録する形で扱い、件数によって処理を分岐させない。

候補が全てシングル以外(アルバム・ライブアルバム等)の場合は、同一タイトルが複数のアルバム・ライブアルバムに収録されているだけの偶然の重複である可能性が高いため、全件登録の対象にはせず、初出(releaseDateが最古)の1件のみを採用する。releaseDateが空文字のレコードは「最古」判定から除外する(有効な日付を持つ候補が1件もない場合のみ、配列順の先頭を採用する)。

備考(バグの経緯、MorningStatusApp #1575で是正): #1558での実装当初は、シングル/非シングルを区別せず正規化タイトルが一致する全トラックを無条件で候補として返していた。この結果、CD盤・配信版シングルのような正当なケースだけでなく、アルバム収録曲が複数のライブアルバムに繰り返し収録されているケースまで無制限に候補が積み上がり、非シングル曲が誤って複数のアルバム・ライブアルバムに紐づく実害が生じた(本番DBのradio_onair_song_tracksで2件確認)。既存の単曲専用データパッチpatch-lonely-but-not-alone-track-links.ts(#1529)は上記の制約追加に伴い不要になったため廃止し、radio_onair_song_tracks全体を対象に修正後のロジックで再解決する汎用パッチpatch-radio-onair-track-candidates.tsに置き換えた。

ラジオ・セットリストいずれの登録処理も、曲名の正規化には normalizeForMatch を使うが、トラック候補の解決には buildTrackMatchMapFromPrisma(single優先で1件のみ採用)ではなく、正規化キーごとに該当する全トラックを返す buildTrackCandidatesMapFromPrisma(lib/track-match.ts、実装時に新設)を使う。buildTrackMatchMapFromPrisma・findBestMatch は従来どおりYouTubeプレイリスト・TikTok連携(1トラックへの単一紐付け前提)専用として残す。中間テーブルへの書き込み行の組み立ても、ドメイン(ラジオ/セットリスト)ごとに個別実装せず buildTrackJunctionRows(同ファイル)で共通化した(MorningStatusApp #1558)。

入力スキーマの共通化(seq + songTitle): フィールド名を揃えるだけでなく、Zodスキーマ自体を共有する。scripts/lib/song-entry-schema.ts(新設)に以下の基底スキーマを定義し、ラジオ側の入力スキーマはこれを .extend() してドメイン固有フィールド(artist)を追加する。セットリスト側(SetlistEntrySchema)は追加フィールドを持たないため、基底スキーマをそのまま使う。

// scripts/lib/song-entry-schema.ts
export const SongEntrySchema = z.object({
  seq: z.number().int().positive(),
  songTitle: z.string().min(1),
});
// scripts/console/register-manual-radio-episodes.ts
const ManualRadioSongSchema = SongEntrySchema.extend({
  artist: z.string(),
});
// scripts/console/register-tour-festival.ts
const SetlistEntrySchema = SongEntrySchema;

DBカラム名は変更しない(RadioOnairSong.trackOrder・Setlist は現状どおり)。入力JSONの seq は、ラジオ側の登録処理内で RadioOnairSong.trackOrder にマッピングして書き込む。この構成であれば、seq・songTitle という概念の型・バリデーションルールを変更する際に両方のスキーマへ同時に反映され、名前が偶然一致しているだけの状態(片方を変更してももう片方に波及しない状態)を避けられる。

備考(旧仕様、MorningStatusApp #1529で是正): 以前は同一正規化キーに複数トラックが該当する場合、「single優先、非single同士は最古リリース日優先」で1件のみを採用していた。CD盤・配信版シングルのように同一タイトルの複数シングルが存在するケースでは、本来は該当する全トラックに紐づけるべきところ、DBクエリの返却順(不定)でどちらか一方のみが単一の trackId 列に採用され、もう一方への紐付けが失われていた(#1552のバックフィル作業時に発覚)。単一 trackId 列という当時のスキーマ自体が複数トラックへの紐付けを表現できなかったことが根本原因であり、§2-5の中間テーブル導入によりこの制約を解消した。

旧仕様の記載(バグの経緯): 以前は除去対象の文字集合にU+2010等のタイポグラフィハイフンが含まれておらず、NFKC正規化でもU+002Dには変換されなかったため、MusicBrainz由来のシングル側タイトル(U+2010表記)と手動入力のライブ盤側タイトル(U+002D表記)が別々の正規化キーとして扱われていた。オンエア曲名は通常ASCIIハイフンで入力されるため、正規化後のキーがライブ盤側とのみ一致し、シングル優先ロジックが機能しないまま複数あるライブ盤のうち最古のものに誤って紐づく不具合があった(例: 「Wake-up Call~目覚めるとき~」がシングルでなく2024年リリースのライブ盤に紐づく。MorningStatusApp #1552で是正)。この修正に合わせて、共有関数を scripts/lib/track-match.ts(Node script専用の配置)から lib/track-match.ts(TikTok側の実行時コードからも参照可能な配置)へ移動した。

曲名抽出誤りの是正(一回限りのデータパッチ、#1292)

PR #1202 より前のモーニング女学院曲目ツイート抽出処理では、リリース紹介文などの説明文が曲名の先頭に混入するケースがあり、artistName: null かつ trackId: null のまま「説明文:正しい曲名」という形式のレコードが生成されることがあった(抽出処理自体は PR #1202 で改善済で新規データでは発生しない)。PR #1202 より前に登録されたレコードは、scripts/patch/patch-radio-onair-song-titles.ts で末尾の「:」以降を正しい曲名として再抽出し、track-matchで再紐付けする(一回限りの是正パッチ)。

4-1. モーニング女学院(@morning1422)

定期実行は停止済(#1333): X APIのタイムライン取得上限(直近3,200件付近、#1331)に到達し、これ以上の遡及取得ができなくなったため、collect-onair-morning.yml の schedule トリガーを削除した。workflow_dispatch による手動実行はいつでも可能。以下は当時の自動収集設計であり、手動実行時の参考として残す。新規放送分は register-manual-radio-episodes.ts(§4-4)による手動登録に統一する。

データソース

項目 内容
収集元 X アカウント @morning1422
収集手法 twitterapi.io GET /twitter/user/last_tweets(§5参照)
使用 Secret TWITTERAPI_IO_KEY、DATABASE_URL、BLOB_READ_WRITE_TOKEN
放送スケジュール 毎週土曜深夜0時(JST)
収集タイミング 毎週日曜午前 JST 09:00(UTC 00:00)に GitHub Actions で実行

ツイートフォーマットと収集フィールド

曲目ツイート(放送中)

🔔ラジオ日本「モーニング娘。'26のモーニング女学院~放課後ミーティング~」今週の2曲目 🔔涙にはしたくない/モーニング娘。☆ #morning1422 #小田さくら #岡村ほまれ #morningmusume26 #モーニング娘26

曲目ツイートは 🔔 の直後に全角コロン : が付く形式も存在する:

🔔ラジオ日本「…スペシャル」今週の4曲目 🔔:ピョコピョコ ウルトラ/モーニング娘。☆ #morning1422
収集フィールド 抽出方法
トラック番号 今週の(\d+)曲目
曲名 曲目\s*🔔[::]?\s*(.+?)[/\/](コロン有無両対応)
出演メンバー ハッシュタグ #メンバー名 から members.json と照合

メンバー告知ツイート(放送前)

🔔今夜のメンバー告知🔔
#ラジオ日本「モーニング娘。'26のモーニング女学院」土曜深夜0時‼️
12月21日(土)は #牧野真莉愛 さん #北川莉央 さん #山﨑愛生 さんの3人出席
(日直は牧野さん)
#morning1422 #morningmusume26 #モーニング娘26
収集フィールド 抽出方法
放送日 (\d+)月(\d+)日(土) → Date 変換(年はツイート投稿日から推定)
出演メンバー ハッシュタグ #メンバー名 から members.json と照合
日直 日直は(.+?)さん → isDayDuty = true
  • 放送日の年推定ロジック: 月差が6ヶ月超の場合のみ年を調整(年越し告知への対応)

エピソードタイトルツイート(放送後)

🔔ラジオ日本「モーニング娘。'26のモーニング女学院~放課後ミーティング~」🔔【737時間目】#morning1422
収集フィールド 抽出方法
エピソードタイトル 【(\d+時間目)】 形式のテキストを抽出
放送日 ツイート投稿日時から直近の土曜日を算出(nearestSaturday)

スキーママッピング

収集データ マッピング先 備考
放送日 RadioEpisode.date
エピソードタイトル RadioEpisode.title 「737時間目」等。取得できない場合は null
出演者 RadioMember.memberId ハッシュタグ → members.json 照合
日直 RadioMember.isDayDuty = true メンバー告知ツイートから取得
曲名 RadioOnairSong.songTitle
トラック番号 RadioOnairSong.trackOrder 「今週のN曲目」のN
トラックID radio_onair_song_tracks(§2-5) 収集後に §4-0 の方針で該当する全trackIdを解決し登録(#1014)

設計方針の決定事項

showId 採番ルール

年ごとに番組IDを変える(例: morning-jogakuin2026)。

episodeId の採番ルール

mj-YYYYMMDD
例: mj-20260510(2026年5月10日放送分)

ページネーション方針

実行ごとに2フェーズで取得する。総ページ予算は MORNING_ONAIR_MAX_PAGES(デフォルト: 20)で、両フェーズで共有する。

  • 当週放送回取得(必須): DB 登録済の放送回(episodeId)に突き当たるまで取得する早期終了モード(hasRegisteredEpisode)。何週分か取り逃した場合も DB 既存回に達するまで自動的に取得するため、取り逃し週数に依存しない。常に実行し、最大 MORNING_ONAIR_MAX_PAGES ページを上限とする。
  • 遡及取得(オプション): Vercel Blob(morning-onair-cursor.json)にカーソルが保存されており、かつ当週放送回取得後に残りページ数がある場合のみ実行。前回中断位置から残りページ数(MORNING_ONAIR_MAX_PAGES - phase1Pages)を上限として取得を再開し、続きのカーソルを Blob に保存する(次回継続)。has_next_page=false になった場合は Blob に { done: true } を保存し次回から通常モードに戻るが、これは投稿履歴の終端ではなく X APIのタイムライン取得上限(直近3,200件付近)への到達であり、それ以前の投稿は本APIでは取得できない(#1331)。当週放送回取得でページ予算を使い切った場合は遡及をスキップし、次回実行時に再開する。

ページ間インターバルは環境変数 TWITTERAPI_IO_PAGE_INTERVAL_MS で設定可能(デフォルト: 5000ms)。

重複処理方針(冪等性)

  • episodeId が DB に存在する場合: 不足データを補完してスキップ
    • 未登録の楽曲を radio_onair_songs に追加
    • title が未設定の場合は radio_episodes を更新
  • 楽曲の重複は (episodeId, trackOrder) の UNIQUE 制約で防止

スクリプト概要

項目 内容
スクリプト scripts/workflow/collect-onair-morning.ts
実行タイミング 毎週日曜午前 JST 09:00(UTC 00:00)に GitHub Actions で実行
使用 Secret TWITTERAPI_IO_KEY、DATABASE_URL、BLOB_READ_WRITE_TOKEN
環境変数 MORNING_ONAIR_MAX_PAGES(遡及取得の1回あたり上限ページ数、デフォルト: 20)

処理フロー

1. DB 登録済 episodeId 一覧を事前ロード(当週放送回取得の停止条件に使用)
2. twitterapi.io で @morning1422 の当週放送回ツイートを取得(必須)
   → hasRegisteredEpisode(DB 登録済放送回に突き当たった時点)で停止
   → 最大 MORNING_ONAIR_MAX_PAGES ページを上限とする
   → HTTPエラー・レート制限は警告ログを出してその時点までの取得分を返す(フェールセーフ)
3. Vercel Blob から遡及カーソルを読み込む
   カーソルあり、かつ残りページ数あり(遡及モード):
   → twitterapi.io でカーソル位置から残りページ数(MORNING_ONAIR_MAX_PAGES - phase1Pages)を取得(オプション)
   → 同様にフェールセーフ。例外発生時はカーソル更新をスキップ(次回同位置から再開)
   → 次のカーソルがあれば Blob に保存(次回継続)
   → 次のカーソルがなければ Blob に `{ done: true }` を保存(取得上限到達・通常モードに戻る。投稿履歴の終端ではない点に注意)
   カーソルあり、かつ残りページ数なし:
   → 遡及をスキップ(次回実行時に再開)
   カーソルなし(通常モード):
   → 当週放送回取得のみで完了
4. 取得ツイートからエピソードを解析して DB に登録
   → メンバー告知ツイート(放送日・出演メンバー・日直)
   → 曲目ツイート(トラック番号・曲名)
   → エピソードタイトルツイート(「737時間目」等)
   episodeId が DB に存在しない場合: 新規 INSERT
   → radio_episodes に放送回(タイトル含む)を挿入
   → radio_members に出演メンバーを挿入(日直フラグあり)
   → radio_onair_songs にオンエア楽曲を挿入
   episodeId が DB に存在する場合: 不足分を補完
   → 未登録の楽曲を radio_onair_songs に追加
   → title が未設定の場合は radio_episodes を更新
5. 実行サマリーをログ出力(LOG_LEVEL=INFO で進捗、LOG_LEVEL=DEBUG で詳細)

4-2. ヤングタウン土曜日(@yando_staff)

定期実行は停止済(#1280, #1333): 手動登録の方が容易なため collect-onair-yando.yml の schedule トリガーを削除した。workflow_dispatch による手動実行はいつでも可能。以下は当時の自動収集設計であり、手動実行時の参考として残す。新規放送分は register-manual-radio-episodes.ts(§4-4)による手動登録に統一する。

データソース

項目 内容
収集元 X アカウント @yando_staff
収集手法 twitterapi.io GET /twitter/user/last_tweets(§5参照)
使用 Secret TWITTERAPI_IO_KEY、DATABASE_URL
放送スケジュール 毎週土曜(MBSラジオ)
収集タイミング 毎週日曜午前 JST 09:00(UTC 00:00)に GitHub Actions で実行

ツイートフォーマットと収集フィールド

#yando または #ヤン土 ハッシュタグを含むツイートを収集対象とする。

収集フィールド 抽出方法
出演メンバー ハッシュタグ #メンバー名 から members.json と照合
放送日 ツイート投稿日時から直近の土曜日を算出(nearestSaturday)

モーニング女学院と異なり、楽曲情報・エピソードタイトル・日直の収集は対象外。

スキーママッピング

収集データ マッピング先 備考
放送日 RadioEpisode.date
出演者 RadioMember.memberId isDayDuty = false

設計方針の決定事項

showId

young-town-saturday

episodeId の採番ルール

yt-YYYYMMDD
例: yt-20260510(2026年5月10日放送分)

ページネーション方針

最新放送回のメンバー情報(出演者ハッシュタグ)が取得できた時点でページ取得を停止する(isLatestYandoEpisodeComplete)。 2018年分まで遡及取得済のため、最新回の情報が揃い次第停止することで不要なページ取得を抑制する。

重複処理方針(冪等性)

  • episodeId が DB に存在する場合はスキップする

スクリプト概要

項目 内容
スクリプト scripts/workflow/collect-onair-yando.ts
実行タイミング 毎週日曜午前 JST 09:00(UTC 00:00)に GitHub Actions で実行
使用 Secret TWITTERAPI_IO_KEY、DATABASE_URL

処理フロー

1. twitterapi.io で @yando_staff の直近ツイートを取得(最新放送回のメンバーが揃った時点で停止: `isLatestYandoEpisodeComplete`)
2. #yando / #ヤン土 ハッシュタグを含むツイートを抽出
   → ハッシュタグからメンバー情報を取得
3. episodeId が DB に存在しない場合: 新規 INSERT
   → radio_episodes に放送回を挿入
   → radio_members に出演メンバーを挿入(isDayDuty = false)
   episodeId が DB に存在する場合: スキップ
4. 実行サマリーをログ出力

4-3. メイボンソワ(STVサイト)

定期実行は停止済(#1333): モーニング女学院・ヤングタウン土曜日と同様に手動登録方式へ統一するため collect-bonsoir-songs.yml の schedule トリガーを削除した。workflow_dispatch による手動実行はいつでも可能。以下は当時の自動収集設計であり、手動実行時の参考として残す。新規放送分は register-manual-radio-episodes.ts(§4-4)による手動登録に統一する。

データソース

項目 内容
収集元 https://www.stv.jp/radio/bonsoir/senkyoku/index.html
過去回 https://www.stv.jp/radio/bonsoir/senkyoku/{ID}.html(IDはランダムな英数字)
収集手法 静的HTMLスクレイピング(fetch + 正規表現)
JavaScript レンダリング 不要
放送スケジュール 毎週土曜放送
収集タイミング 毎週日曜午前 JST 09:00(UTC 00:00)に GitHub Actions で実行
APIキー 不要(コスト無料)

収集対象: パーソナリティが山﨑愛生のコーナーのみ。

HTMLページ構造と収集フィールド

ページ内コンテンツ例:

放送日: 2026年5月9日(土)
コーナー名: 稲場愛香のまなかん♡ボンソワ
パーソナリティ: 稲場愛香

M1「熱狂バイレ」(稲場愛香)
M2「Hanabira」(3House)
M3「雫に恋して」(indigo la End)
M4「Die With a Smile」(レディーガガ&ブルーノマーズ)

楽曲データは <p> タグにプレーンテキストで記載(構造化マークアップなし)。通常4曲(M1〜M4)。

フィールド 正規表現 例
放送日 /(\d{4})年(\d{1,2})月(\d{1,2})日/ 2026年5月9日
パーソナリティ パーソナリティフィールドまたはコーナー名から抽出 山﨑愛生
トラック番号 /M(\d+)/ M1 → 1
曲名 /M\d+「(.+?)」/ 熱狂バイレ
アーティスト名 /M\d+「.+?」((.+?))/ 3House

スキーママッピング

収集データ マッピング先 備考
放送日 RadioEpisode.date
パーソナリティ(山﨑愛生) RadioMember.memberId isDayDuty = false(日直概念なし)
曲名 RadioOnairSong.songTitle
トラック番号 RadioOnairSong.trackOrder M1 → 1
アーティスト名 RadioOnairSong.artistName §2-4 の拡張フィールド

設計方針の決定事項

showId

mei-bonsoir

episodeId の採番ルール

mb-YYYYMMDD
例: mb-20260510(2026年5月10日放送分)

収集対象フィルタリング

  • パーソナリティが「山﨑愛生」のページのみ収集する
  • 他のパーソナリティ(例: 稲場愛香、北川莉央 等)のコーナーはスキップする

RadioMember の扱い

パーソナリティは山﨑愛生のみ固定。STVサイトにゲスト出演者情報はないため、パーソナリティのみを格納する(isDayDuty = false)。

重複処理方針(冪等性)

  • episodeId が DB に存在する場合はスキップする
  • 楽曲の重複は (episodeId, trackOrder) の UNIQUE 制約(既存スキーマ)で防止する

過去データ遡及取得の方針

  • index.html の最新回から「過去の放送回へのリンク」を再帰的にたどって遡及取得する
  • DB に既存の episodeId を発見した時点で遡及を打ち切る

スクリプト概要

項目 内容
スクリプト scripts/workflow/collect-bonsoir-songs.ts
実行タイミング 毎週日曜午前 JST 09:00(UTC 00:00)に GitHub Actions で実行
使用 Secret DATABASE_URL

処理フロー

1. https://www.stv.jp/radio/bonsoir/senkyoku/index.html を fetch
2. パーソナリティ = 山﨑愛生か確認(異なる場合はスキップして過去回リンクをたどる)
3. 放送日・楽曲データを正規表現でパース
4. episodeId が DB に存在しない場合のみ INSERT
   → radio_episodes に放送回を挿入
   → radio_members に山﨑愛生を挿入(isDayDuty = false)
   → radio_onair_songs にオンエア楽曲を挿入(artistName を含む)
5. 過去回リンクを再帰的にたどる(DB 既存エピソードで打ち切り)
6. 実行サマリーをログ出力

環境変数

変数名 必須 説明
DATABASE_URL ○ Neon DB 接続文字列(既存)

4-4. 手動登録(register-manual-radio-episodes.ts)

  • 元々は井上春華のはるさんち専用(自動収集は未実装のため手動データを読み込むスクリプトで登録、#1274)だったが、#1333でモーニング女学院・ヤングタウン土曜日・メイボンソワの自動収集を停止したことに伴い、4番組共通の手動登録スクリプトへ汎用化した
  • getShowMeta(showId) は lib/radio-helpers.ts の getRadioShowMeta の re-export で、showId の prefix(morning-jogakuin / young-town-saturday / mei-bonsoir / harusan-chi)で番組名・放送局を判定する。対応するprefixがない showId はエラーを投げる。§3-1のオンエア情報確認用リンクと同じ RADIO_SHOWS レジストリを参照するため、新規番組を追加する場合は lib/radio-helpers.ts の1箇所を更新すればよい(#1344)

データソース

項目 内容
収集元 data/inputs/manual-radio-episodes.json(コマンドライン引数未指定時のデフォルトパス)。手動作成データで、恒久データではなくスクリプト実行に合わせて用意する一時データ(リポジトリにコミットしない)
収集手法 手動JSONファイルの読み込み(自動収集なし)
実行方式 スクリプト実行のみ(GitHub Actions ワークフローには組み込まない)

入力ファイル形式(例: data/inputs/manual-radio-episodes.json)

{
  "showId": "harusan-chi2026",
  "episodes": [
    {
      "episodeId": "hs-20260629",
      "title": "【743時間目】",
      "members": [
        { "memberId": "inoue-haruka", "isDayDuty": true }
      ],
      "songs": [
        { "seq": 1, "songTitle": "すき焼き", "artist": "" },
        { "seq": 2, "songTitle": "ロマンスの神様", "artist": "広瀬香美" }
      ]
    }
  ]
}

members[].isDayDuty は省略可能(省略時は false)。1エピソードにつき isDayDuty: true を指定できるのは0〜1名まで(2名以上指定するとZodバリデーションでエラーになりファイル全体の読み込みが失敗する)。

episode.title は省略可能。モーニング女学院のように放送回ごとにタイトル(回数表記等)が存在する番組で指定する。省略時は RadioEpisode.title は null のまま。

複数番組のまとめ登録(#1394)

入力ファイルは番組ごとに1つ(showId は1ファイルにつき1件)。複数番組をまとめて登録したい場合は、番組ごとに別ファイルを用意し、コマンドライン引数でファイルパスを指定して番組数分実行する。

bun run scripts/console/register-manual-radio-episodes.ts data/inputs/harusan-radio-episodes.json
bun run scripts/console/register-manual-radio-episodes.ts data/inputs/young-town-radio-episodes.json

引数省略時は従来通り data/inputs/manual-radio-episodes.json を読み込む。

スキーママッピング

収集データ マッピング先 備考
showId RadioShow.showId
episodeId RadioEpisode.episodeId
title RadioEpisode.title 省略可。既存放送回に後から補完する場合は、既存の title が null の場合のみ反映する(上書きしない)
members[].memberId RadioMember.memberId 実在チェックは MEMBERS_BLOB_URL から取得したメンバー一覧と突き合わせる。一致しない場合は警告ログを出すのみで登録は継続する(メンバー一覧取得に失敗した場合もチェックをスキップして登録は継続する)
members[].isDayDuty RadioMember.isDayDuty 省略時は false。1エピソードにつき true は0〜1名まで(#1394)。既存放送回に既に日直が登録されている場合、補完メンバーの isDayDuty: true は無視され false として登録される(警告ログを出す)
songs[].songTitle RadioOnairSong.songTitle 入力フィールド名をセットリスト側の入力JSON(setlist[].songTitle)と統一(MorningStatusApp #1529・#1558で songs[].title から改称)
songs[].seq RadioOnairSong.trackOrder 入力フィールド名をセットリスト側の入力JSON(setlist[].seq)と統一(songs[].trackOrder から改称)。DBカラム名(trackOrder)は「放送順」という番組固有の意味を保つため変更せず、取り込み時に読み替える
songs[].artist RadioOnairSong.artistName / radio_onair_song_tracks(§2-5) artist が空文字の場合はモーニング娘。関連曲としてタイトルから該当する全trackIdを解決し radio_onair_song_tracks に登録する(artistName はnull)。artist 指定がある場合はそのまま artistName に格納する(trackIdへの紐付けは行わない)

設計方針の決定事項

showId 採番ルール

番組 showId 年サフィックス
モーニング女学院 morning-jogakuin{year} あり(§4-1のshowId採番ルールを踏襲)
ヤングタウン土曜日 young-town-saturday なし(固定)
メイボンソワ mei-bonsoir なし(固定)
井上春華のはるさんち harusan-chi{year} あり

getShowMeta(showId) はこの表の prefix で番組名・放送局を判定するため、新規番組を追加する場合は lib/radio-helpers.ts の RADIO_SHOWS にエントリを追加すること(§3-1のオンエア情報確認用リンクも同じレジストリから導出される)。

episodeId の採番ルール

hs-YYYYMMDD
例: hs-20260629(2026年6月29日放送分)

TrackId紐付け方式

normalizeForMatch による正規化タイトルの完全一致のみ(collect-bonsoir-songs.ts と同方式)。あいまい一致(findBestMatch)は使わない。§4-0 の方針通り、未紐付け時は警告ログを出す。

登録・補完方針(冪等性)

  • 放送回(RadioEpisode)が未登録の場合は新規作成する
  • 既存の放送回に対しては項目単位で不足分のみ補完し、既に揃っている項目は上書きしない
    • タイトル(RadioEpisode.title)が未設定(null)であれば補完する
    • 出演メンバー(RadioMember)が不足していれば追加する(既に日直が登録済の放送回に対し、isDayDuty: true のメンバーを追加しようとした場合は false に矯正して警告ログを出す。§4-4 参照)
    • オンエア楽曲(RadioOnairSong)が trackOrder 単位で不足していれば追加する
  • 手動JSONの songs 内で seq(DB上は trackOrder にマッピング)が重複している場合は、Zodスキーマのバリデーション時点でエラーとし処理をスキップする(§4-0 参照)
  • 手動JSONの members[].memberId が MEMBERS_BLOB_URL のメンバー一覧に存在しない場合は警告ログを出す(タイポ検知。登録自体はブロックしない)
  • trackId未解決曲の再紐付けも §4-0 の方針に従い、今回処理した放送回に絞って毎回実行する

スクリプト概要

項目 内容
スクリプト scripts/console/register-manual-radio-episodes.ts
実行タイミング 手動実行のみ(ワークフロー組み込みなし)
使用 Secret DATABASE_URL、MEMBERS_BLOB_URL(memberId 存在チェック用。未設定時はチェックのみスキップし処理は継続)

処理フロー

1. 入力ファイル(コマンドライン引数で指定、未指定時は data/inputs/manual-radio-episodes.json)を読み込む
   (Zodスキーマでバリデーション。episode内のseq重複・isDayDuty重複もここで検出)
   → ファイル未配置・バリデーション失敗時は処理をスキップして終了(一時データのため正常系)
2. MEMBERS_BLOB_URL からメンバー一覧を取得し、members[].memberId の実在チェックを行う(不一致は警告ログのみ。取得失敗時はチェックをスキップ)
3. radio_shows に showId を upsert
4. 各 episode ごとに既存 radio_episodes(onairSongs・members含む)を取得
   → 未登録なら放送回・出演メンバー・オンエア楽曲を新規作成(title を指定していれば設定)
   → 登録済なら不足しているタイトル・メンバー・楽曲のみ項目単位で補完
5. 楽曲の trackId 解決: artist が空文字の曲のみ、正規化タイトル完全一致で trackId を解決。未紐付けの場合は警告ログを出す
6. 実行サマリーをログ出力
7. 今回処理した放送回に絞って、trackId未解決の楽曲を再紐付け(§4-0 参照)

5. twitterapi.io 仕様(X投稿収集共通)

モーニング女学院・ヤングタウン土曜日の収集に使用する twitterapi.io の API 仕様。

5-1. 認証方法

項目 内容
認証方式 x-api-key ヘッダー単一認証
OAuth 不要

5-2. エンドポイント仕様: GET /twitter/user/last_tweets

パラメータ

パラメータ 必須 説明
userName 必須(userId との択一) Twitterハンドル(@ 付き or 無しどちらも可)
userId 必須(userName との択一) Twitter ユーザーID
cursor 任意 ページネーション用カーソル。初回は省略
includeReplies 任意 リプライを含めるか(デフォルト: false)

動作確認済の呼び出し例:

curl --request GET \
  --url 'https://api.twitterapi.io/twitter/user/last_tweets?includeReplies=false&userName=%40morning1422' \
  --header 'x-api-key: <TWITTERAPI_IO_KEY>'

レスポンス形式

{
  "data": {
    "tweets": [...]
  },
  "has_next_page": true,
  "next_cursor": "some_cursor_string"
}
フィールド 型 説明
data.tweets Tweet[] 取得ツイート一覧(1ページ最大20件)
has_next_page boolean 次ページが存在するか
next_cursor string 次ページ取得用カーソル

Tweet オブジェクトのフィールド

フィールド 型 説明
id string ツイートID
text string ツイート本文
createdAt string 投稿日時(ISO 8601)
url string ツイートURL
author UserInfo 投稿者情報
entities object ハッシュタグ・URL・メンション情報

5-3. ページネーション

  • 1ページあたり最大 20件
  • has_next_page: true の場合、次ページが存在する
  • next_cursor の値を次のリクエストの cursor パラメータに渡す
  • ページ間インターバルは TWITTERAPI_IO_PAGE_INTERVAL_MS 環境変数で設定(デフォルト: 5000ms)

実装は scripts/lib/twitterapi-client.ts の fetchUserTweets を共通利用する。

5-4. 料金試算

項目 単価
ツイート取得 $0.15 / 1,000件
無料試用クレジット サインアップ時に付与($0.10 相当)
シナリオ ページ数 取得件数 費用
週次実行 2アカウント(morning + yando) 2〜4ページ 40〜80件 $0.006〜$0.012
月換算(2アカウント × 4週) 16ページ 320件 ~$0.048 / 月

月間コストは $0.05 以下 に収まる見込み。


6. 改訂履歴

版 更新日 変更内容
2.21 2026-08-30 §4-0: トラック候補解決(buildTrackCandidatesMapFromPrisma)に、非シングル曲が複数のアルバム・ライブアルバムに誤って紐づかないための絞り込みを追加。候補にシングル形式が1件でも含まれる場合のみ全トラック候補を採用し、全て非シングルの場合はreleaseDate(空文字を除く)最古の1件のみを採用するよう是正。既存の単曲専用データパッチ(patch-lonely-but-not-alone-track-links.ts)を、radio_onair_song_tracks全体を再解決する汎用パッチ(patch-radio-onair-track-candidates.ts)に置き換え(MorningStatusApp #1575)
2.20 2026-08-29 §4-0の記載を実装(#1558)に合わせて是正。トラック候補解決に使う関数を buildTrackMatchMapFromPrisma(single優先で1件のみ採用、YouTube/TikTok専用として存置)から、正規化キーごとに全トラックを返す新設の buildTrackCandidatesMapFromPrisma に変更したことを明記(MorningStatusApp #1558)
2.19 2026-08-28 1曲が複数トラック(CD盤・配信版シングル等)に正しく該当しうるケースへの対応として、RadioOnairSong.trackId 列を廃止し§2-5「radio_onair_song_tracks」中間テーブルに置き換え。§4-0の紐付け方針を「1件採用」から「該当する全トラックを候補として採用」に変更し、実装(#1558)ではドメイン共通の関数として切り出すことを明記。§4-4手動入力JSONの songs[].title/songs[].trackOrder をセットリスト側と揃えて songs[].songTitle/songs[].seq に改称し、両者が共有Zodスキーマ(scripts/lib/song-entry-schema.tsのSongEntrySchema)を使う設計を追記(DBカラム名RadioOnairSong.trackOrder自体は変更なし)(MorningStatusApp #1529・morning-status-blume#80)
2.18 2026-08-23 §4-0に「曲名正規化とハイフン文字種の吸収」を新設。正規化関数を scripts/lib/track-match.ts から lib/track-match.ts へ移動し、ラジオ・セットリスト・TikTokで共通利用する方針を明記。ハイフン文字種の違いにより曲がシングルでなくライブ盤に誤って紐づく不具合の経緯を追記(MorningStatusApp #1552)
2.17 2026-08-20 タイトルを「ラジオオンエア収集 設計書」から他ドキュメントの命名規則({対象} データ設計書)に合わせた「ラジオオンエア データ設計書」に是正(3番組とも定期収集は停止済で手動登録が主体のため、「収集」を冠したタイトルは実態と乖離していた)。あわせて§4-0の備考を、collect-bonsoir-songs.ts のガード漏れ修正(#1523)と、episodeId スコープを意図的に付けない設計判断の説明に更新。同スクリプトは #1333 で定期実行を停止済で手動実行のみである旨も明記
2.16 2026-08-18 §3-2: 放送回詳細ページで track_id のない楽曲(外部アーティスト楽曲)に artistName を曲名の隣に併記する仕様を追記(#1501)。あわせて§4-0に collect-bonsoir-songs.ts の再紐付けガード漏れ(PR #61 レビューで検出)を備考として追記
2.15 2026-07-20 §4-4: 手動放送回登録スクリプトに episode.title(エピソードタイトル、モーニング女学院向け)を追加。あわせて members[].memberId の実在チェック(MEMBERS_BLOB_URL 突合・警告ログ)と、既存放送回に既に日直がいる場合の補完メンバーの日直指定を無視するガードを追加(PR #1395 レビュー対応)
2.14 2026-07-20 §4-4: 手動放送回登録スクリプトに日直指定(members[].isDayDuty、1エピソードにつき0〜1名)と、コマンドライン引数による入力ファイルパス指定(複数番組のまとめ登録)を追加。あわせて入力ファイルの参照パスを実際の配置 data/inputs/manual-radio-episodes.json に是正(#1394)
2.13 2026-07-20 §4-1: 定期実行停止の理由を「自動遡及取得の価値がなくなったため」から「これ以上の遡及取得ができなくなったため」に是正(X APIのタイムライン取得上限は原理的に超えられない制約であり、価値判断の結果停止したものではない、#1376 対応中に発見)
2.12 2026-07-19 §1, §4-1, §4-2, §4-3: 収集スクリプトの置き場所を scripts/collect-onair-*.ts から実際の配置 scripts/workflow/collect-onair-*.ts 等に是正(#1375)
2.11 2026-07-15 §3-1: オンエア情報確認用リンクの表示をテキストリンクから components/RadioShowLinkBadge.tsx によるバッジ形式に変更。RadioShowLink 型に badgeLabel・badgeColor を追加し、Xアカウントは@アカウント名+黒背景、番組サイトは「公式サイト」+グレー背景で表示(#1344 レビュー対応)
2.10 2026-07-15 §3-1, §4-4: lib/radio-helpers.ts の RADIO_SHOW_LINKS と scripts/console/register-manual-radio-episodes.ts の SHOW_META_BY_PREFIX に分かれていたshowId prefix定義を lib/radio-helpers.ts の RADIO_SHOWS に一本化。getShowMeta は getRadioShowMeta の re-export に変更(#1344 レビュー対応)
2.9 2026-07-15 §3-1: 各番組のオンエア情報確認用外部リンク(Xアカウント/番組サイト)追加を追記。§4-1〜4-3: モーニング女学院・ヤングタウン土曜日・メイボンソワの自動収集ワークフロー停止を追記。§4-4: register-manual-radio-episodes.ts の4番組共通対応(getShowMeta のprefixマッピング化)を追記し節タイトルを「手動登録」に変更(#1333)
2.8 2026-07-14 §4-0: PR #1202 より前に登録された曲名抽出誤りレコードを是正するスクリプト(patch-radio-onair-song-titles.ts)を追加(#1292)
2.7 2026-07-13 §3-5(新設): タイムラインのラジオ番組アクセントカラー一覧を追記。井上春華のはるさんちに未登録だったアクセントカラー(#34d399)を追加した修正に合わせて記載(#1325)
2.6 2026-07-12 §4-1 モーニング女学院: 遡及取得の done(has_next_page=false)は投稿履歴の終端ではなくX APIの取得上限(直近3,200件付近)への到達である旨を明記(#1331)
2.5 2026-07-04 §4-0(新設): 番組共通の楽曲登録・紐付け方針(警告ログ・trackOrder重複防止・trackId再紐付け)を追記。§4-4: PRレビュー指摘を踏まえ、警告ログ・trackOrder重複バリデーション・再紐付けパスの追加を反映(PR #1276 レビューより)
2.4 2026-07-04 §4-4 井上春華のはるさんち: 手動データ登録スクリプトのデータソース・スキーママッピング・登録方針・処理フローを追記(#1274)
2.3 2026-06-25 §4-4 モーニング娘。’26 井上春華のはるさんちの設計方針を追記(#1217)
2.2 2026-06-07 §4-1 モーニング女学院: ページネーション方針を2フェーズ制(当週放送回必須+遡及オプション)に変更。停止条件を hasRegisteredEpisode(DB登録済放送回に突き当たるまで取得)に変更。ページ予算共有・フェールセーフ動作を追記(#1083)
2.1 2026-06-07 §4-1 モーニング女学院: ページネーション方針を「停止条件なし・ページ終端まで取得」に変更。§4-2 ヤングタウン土曜日: isLatestYandoEpisodeComplete による停止条件を設計方針・処理フローに追記(#1080, #1081)
2.0 2026-05-27 bonsoir-data-design.md・morning1422-data-design.md・twitterapi-io-spec.md を本ドキュメントに集約(#1012)。§2「データソース」の旧 RapidAPI 記述を twitterapi.io に統一
1.1 2026-05-27 モーニング女学院 §4-1 にエピソードタイトルツイート・スマートページネーション・インターバル環境変数を追記(#1009, #1013)
1.0 2026-05-22 DBスキーマ・UI案・データ運用方針の初版(#920)

このページは役に立ちましたか?