リリース データ設計書
最終更新: 2026-09-09
1. 概要
Neon(Vercel Postgres)release テーブルに格納される Release(リリース)エンティティのデータ設計。
Release は他の全機能(トラックリスト・プレイリスト・ユニット・タイムライン・曲対比等)から参照される最も基盤的なエンティティの一つだが、専用のデータ設計書がこれまで存在しなかった(#1379)。
2. 背景・目的
| 目的 | 詳細 |
|---|---|
Release 本体のフィールド定義の一元化 |
types/release.ts の ReleaseSchema と prisma/schema.prisma の Release モデルの対応関係をまとめる |
| Blob → Neon 移行の経緯の記録 | v4.1.0(#853)で releases.json(Vercel Blob)から Neon に移行した際の差分を明示する |
Member との紐付けロジックの記録 |
MemberReleaseLink(実行時算出、永続ファイルなし)の算出アルゴリズムを記録する |
3. 型定義
Release インターフェース(types/release.ts)
export const ReleaseSchema = z.object({
id: z.string().min(1),
title: z.string(),
releaseDate: z.string(),
format: z.string().min(1),
source: z.string().min(1),
sourceUrl: z.string(),
links: z.array(ExternalLinkSchema).optional(),
tracks: z.array(TrackSchema).optional(),
});
export type Release = Omit<z.infer<typeof ReleaseSchema>, 'format'> & { format: ReleaseFormat };
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
id |
string |
✅ | リリースの識別子。MusicBrainz release-group MBID または手動登録リリースの一意な ID |
title |
string |
✅ | リリースタイトル |
releaseDate |
string |
✅ | 発売日。YYYY-MM-DD・YYYY-MM・YYYY のいずれかの精度を許容する(MemberReleaseLink 算出時の精度判定は §5 参照) |
format |
ReleaseFormat |
✅ | リリース形態。single / album / ep / live / soundtrack / compilation / other の列挙値。バリデーターは現行データとの互換性のため非空文字列のみを検証し、型レベルで ReleaseFormat に限定する |
source |
string |
✅ | データ取得元(musicbrainz または手動登録の識別子) |
sourceUrl |
string |
✅ | 取得元 URL。手動登録リリースで URL 不明な場合は空文字を許容する |
links |
ExternalLink[] |
— | リリース単位の外部リンク(型定義上は存在するが、§4-2 の通り現行の DB 読み込み経路では未設定) |
tracks |
Track[] |
— | トラックリスト。詳細 → トラックリスト データ設計書 |
4. Blob → Neon 移行(v4.1.0、#853)
4-1. Prisma モデル(prisma/schema.prisma)
model Release {
releaseId String @id @db.VarChar(255)
title String @db.VarChar(255)
releaseDate String @db.VarChar(20)
format String @db.VarChar(50)
source String @db.VarChar(255)
sourceUrl String @db.Text
tracks Track[]
@@map("releases")
}
Release.id(Zod)とRelease.releaseId(Prisma)は同一値。lib/releases.tsのmapReleaseが変換時にリネームするlinksフィールドに対応する DB カラムは存在しない(§4-2)- 読み取りは
lib/releases.tsのfetchReleasesFromDB/getReleases(unstable_cache)経由で行う。旧getReleasesFromBlob(lib/blob.ts)は移行前の実装として残るが、アプリの読み取り経路としては使われていない
4-2. links フィールドが未使用である理由
ReleaseSchema.links は型定義上は存在するが、mapRelease(lib/releases.ts)はこのフィールドを設定しない。リリース単位の YouTube MV URL 自動取得は「Neon に保存先がなくクォータを空振りするため」廃止済である(#1123、詳細 → バッチ設計書 W2「ディスコグラフィー同期バッチ」手順8)。リリース単位のリンクは廃止され、リンクは全てトラック単位の Track.links[] に一元化されている。
4-3. sync-discography バッチによる書き込み
MusicBrainz からの取得・手動登録リリースのマージ・Neon への保存フローの詳細は → バッチ設計書 W2「ディスコグラフィー同期バッチ」を参照すること。
4-4. 手動登録リリースの MusicBrainz ID による洗い替え(reconcile-manual-releases.ts、#1594)
最近のリリースは MusicBrainz の登録有無を確認せず手動登録している(source: "manual"、releaseId は manual-<slug> 形式)。その後 MusicBrainz に登録された場合、releaseId を MusicBrainz の release-group MBID に洗い替えるための手動実行専用スクリプト。
実行方法
scripts/console/reconcile-manual-releases.ts(GitHub Actions を介さない手動実行専用。--dry-run 対応)。
対象アーティストの決定(モーニング娘。本体に固定)
Release モデル自体にはユニット/アーティストへの紐付けフィールドが存在しない(unitId は Track 側にのみ存在し、MusicBrainz の artist-credit から解決されるため、手動登録直後のトラックには設定されていない)。そのため、どの MusicBrainz アーティスト MBID 配下を検索すべきかを Release から機械的に決定する手段がない。
この制約への対応として、当初は「ユニット選択UI」「全ユニット横断検索」を検討したが、いずれも過剰実装と判断し撤回した。手動登録リリースは実質的にモーニング娘。本体のリリースのみである(サブユニットは活動終了済で新規リリースの登録が見込めない)ため、対象は units.json 上の基幹ユニット(id: 'morning-musume'。各期ユニット morning-musume-2014〜morning-musume-2026 等はこのユニットを parentId として持ち、MBID は全期で共通)の MBID に固定する。
処理フロー(自動照合モード、デフォルト)
1. units.json から id: 'morning-musume' のユニットを取得し、その MBID を対象とする
(見つからない・MBID未設定の場合は警告して終了)
2. 対象 MBID で fetchReleaseGroupsByArtist(lib/musicbrainz.ts、既存関数を再利用)を呼び、
MusicBrainz 側のリリース一覧を取得する
3. manual- 始まりの Release(source: "manual")を全件走査し、各リリースについてタイトル
(記号表記正規化後)+ releaseDate の完全一致で 2 の取得結果と照合する
→ 完全一致1件: releaseId を MBID に洗い替える(--dry-run 時は実行せず対象のみ表示)
→ 不一致(0件): スキップ
→ 複数候補: 候補の MBID・タイトル一覧をコンソール出力してスキップ
4. 結果(洗い替え件数・複数候補件数・衝突件数・不一致件数)を表示して終了する
自動照合モードの目的は、洗い替えにより「手動登録のまま残っている件数」を明らかにすることにある。完全一致しない手動リリースは無理に自動判定せず未解決のまま残し、次項の手動確定モードに委ねる。
手動確定モード
自動照合で複数候補が出た場合、オペレーターが候補から選んだ1件を明示指定して洗い替えを確定する一発実行モード。
bun run scripts/console/reconcile-manual-releases.ts --apply <旧releaseId>=<確定先MBID> [--dry-run]
洗い替え先 releaseId が既に存在する場合(衝突)
洗い替え先の MBID が既に別レコードとして Release テーブルに存在する場合(sync-discography バッチが同一リリースを先に取得済のケース等)、Release.releaseId の一意制約違反となる。実データ調査(2026-09-08時点、手動登録9件)では該当ケースは0件だったため、名寄せ・強制削除等の対応は行わず、一意制約違反(Prisma P2002)を検出したら該当リリースをスキップしてログ出力するのみとする。実際に発生した場合は、その時点で個別に対応方針を検討する。
記号表記の正規化(ハイフン・三点リーダー、#1553 との共通化)
MusicBrainz 側の記号表記と、手動登録・公式表記側の記号表記が異なり、本来一致すべきタイトルが不一致・スキップになるケースがある。lib/musicbrainz.ts の normalizeTitlePunctuation()(新設、旧 normalizeHyphens() から改称)で正規化してから比較する。
- ハイフン類: MusicBrainz 側のタイポグラフィハイフン(U+2010 等)を ASCII ハイフン(U+002D)に正規化する
- 三点リーダー: MusicBrainz 側の三点リーダー(
…、U+2026。直後にスペースを伴うことがある)を ASCII ピリオド3つ(...)に正規化する(実例: MusicBrainz「Lonely… But not Alone」↔ 手動登録「Lonely…But not Alone」、2026-09-08 の--dry-run実行で発覚)
normalizeTitlePunctuation() は本スクリプトの照合用途だけでなく、fetchReleaseGroupsByArtist・fetchTracksForRelease(lib/musicbrainz.ts)が MusicBrainz API から取得した生のリリース・トラックタイトルにも同一関数を適用し、Neon への保存時点で正規化する(#1553、詳細は バッチ設計書 W2 参照)。取得元1箇所で正規化することで、本スクリプトが比較する MusicBrainz 側タイトルは常に正規化済の状態になる。
スコープ: Track.mbTrackId は対象外
Track.mbTrackId も手動登録リリースでは manual-<slug>-<disc>-<track> 形式の ID を持つが、本スクリプトの洗い替え対象は Release.releaseId のみで、Track.mbTrackId は対象外とする。Track.mbTrackId はラジオオンエア情報等の他機能から既に参照されており、参照側を追従更新する手段が現状存在しないため。
Release.releaseId の変更は tracks_releaseId_fkey 制約(ON UPDATE CASCADE)により Track.releaseId に自動的に追従するため、Track テーブル側の追加対応は不要。
出力: 孤立イベントレコード削除ユーティリティ(#1607)との連携
洗い替えにより無効になった旧 releaseId の一覧を、イベントテーブル定義書 §4a の孤立レコード削除ユーティリティ(cleanup-orphaned-event-source-ids.ts)がそのまま読み込める形式で出力する。
- 出力先:
data/work/event-source-ids-to-delete.txt - フォーマット: 1行目
release、2行目以降に無効化した旧releaseIdを1行1件
sync-events.ts は source + sourceId で存在確認して無ければ新規作成するため、洗い替え後の新 ID(MBID)を指す正しい Event レコードは通常の同期処理が自動的に作成する。対応が必要なのは、使われなくなった旧 releaseId 側の孤立 Event レコードの削除のみであり、それは上記ユーティリティによる別途のフォローアップ作業とする(本スクリプトのスコープには含めない)。
5. Member との紐付け(MemberReleaseLink)
Release と Member の関連は永続ファイルを持たず、lib/member-release-linker.ts により実行時に算出される。
MemberReleaseLink インターフェース(types/member-release-link.ts)
export interface MemberReleaseLink {
memberId: string;
releaseId: string;
linkType: LinkType;
confidence: Confidence;
reason: string;
}
| フィールド | 型 | 説明 |
|---|---|---|
memberId |
string |
Member.id |
releaseId |
string |
Release.id |
linkType |
'primary_member' | 'group_member' | 'support_member' | 'unknown' |
紐付け種別。自動判定ロジックでは現状 group_member(在籍期間内一致)と unknown(override 由来でリンク種別未指定時)のみ使用 |
confidence |
'high' | 'medium' | 'low' |
紐付けの確度 |
reason |
string |
紐付け・除外理由(人間可読な説明文) |
算出アルゴリズム(linkMemberToReleases)
- 実質基準日の算出:
profile.joinDateの翌月1日以降・profile.gradDate以前に発売された最初のシングル(format === 'single'かつ完全日付)の発売日を「実質基準日」とする- 該当するシングルが存在しない場合、そのメンバーは(override 指定分を除き)全リリースが対象外となる
- 在籍期間判定: 各リリースについて、発売日が実質基準日以降・卒業日以前であれば紐付け対象とする
- 発売日の精度(
YYYY-MM-DD=full /YYYY-MM=month /YYYY=year)に応じて比較粒度を揃える precision === 'full'の完全一致判定はconfidence: 'high'、部分日付での推定判定はconfidence: 'medium'- 発売日不明(
precision === 'none')のリリースは判定不能として除外
- 発売日の精度(
- override による補正:
MemberReleaseLinkOverride({ memberId, releaseId, action: 'include' | 'exclude', linkType?, reason })が指定されたペアは、上記アルゴリズムに優先して強制的に含める/除外する。ただし本書執筆時点では override を永続化するデータファイルは存在せず、呼び出し元が空配列を渡している
呼び出し箇所
app/members/[id]/page.tsx(メンバー詳細の関連リリース表示)・app/releases/[id]/page.tsx(リリース詳細の参加メンバー表示)・lib/timeline-utils.ts の buildReleaseMemberLookup(タイムライン画面・/api/events のイベント↔メンバー紐付け)から呼び出される。
6. ER 図
→ ER 図 を参照すること。Release はコア音楽データ・タイムライン統合イベントの各セクションに登場する。
7. 関連設計書
| 設計書 | 本書との関係 |
|---|---|
| トラックリスト データ設計書 | Track・ExternalLink 型定義、releases.json(移行前)のスキーマ変更経緯 |
| バッチ設計書 | sync-discography バッチの処理フロー全体(MusicBrainz 取得・ユニット照合・YouTube リンク付与) |
| ユニット一覧機能 データ設計書 | Track.unitId・Unit.releaseIds によるユニットとの紐付け |
| メンバー データ設計書 | profile.joinDate/profile.gradDate の定義 |
改訂履歴
| 版 | 更新日 | 変更内容 |
|---|---|---|
| 1.2 | 2026-09-09 | §4-4処理フロー手順4の「未一致件数」を「不一致件数」に修正(“未だ一致していない”という保留のニュアンスは不適切なため。MorningStatusApp#1609レビューで検出) |
| 1.1 | 2026-09-08 | §4-4「手動登録リリースの MusicBrainz ID による洗い替え」を新設。reconcile-manual-releases.ts(新規、対象アーティストはモーニング娘。本体に固定・手動確定モード)の照合ロジック・洗い替え先衝突時の扱い・記号表記正規化(ハイフン・三点リーダー、#1553 共通化)・Track.mbTrackId 対象外の理由・孤立イベントレコード削除ユーティリティ(#1607)との連携を記載(#1594) |
| 1.0 | 2026-07-21 | 初版作成(#1379) |