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

リリース データ設計書

最終更新: 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)は移行前の実装として残るが、アプリの読み取り経路としては使われていない

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 レコードの削除のみであり、それは上記ユーティリティによる別途のフォローアップ作業とする(本スクリプトのスコープには含めない)。


Release と Member の関連は永続ファイルを持たず、lib/member-release-linker.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)

  1. 実質基準日の算出: profile.joinDate の翌月1日以降・profile.gradDate 以前に発売された最初のシングル(format === 'single' かつ完全日付)の発売日を「実質基準日」とする
    • 該当するシングルが存在しない場合、そのメンバーは(override 指定分を除き)全リリースが対象外となる
  2. 在籍期間判定: 各リリースについて、発売日が実質基準日以降・卒業日以前であれば紐付け対象とする
    • 発売日の精度(YYYY-MM-DD=full / YYYY-MM=month / YYYY=year)に応じて比較粒度を揃える
    • precision === 'full' の完全一致判定は confidence: 'high'、部分日付での推定判定は confidence: 'medium'
    • 発売日不明(precision === 'none')のリリースは判定不能として除外
  3. 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)

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