プレイリスト機能 データ設計書
最終更新: 2026-09-29
1. 概要
メンバーが選曲したプレイリストデータの管理方式を、playlists.json(Vercel Blob)から Playlist/PlaylistEntry(Neon、Vercel Postgres)へ移行するためのデータ設計。
data/inputs/playlists.json を手動編集する入力運用は維持し、格納先(読み取り元)のみをNeonに変更する。
プレイリスト一覧・詳細ページ(#332)、メンバー詳細画面への選曲セクション追加(#333)、 リリース詳細画面へのプレイリスト掲載情報追加(#334)の前提基盤となる。
2. 背景・目的
| 目的 | 詳細 |
|---|---|
| メンバー選曲データの管理 | 各メンバーが選曲した楽曲をプレイリスト単位で管理する |
| 楽曲とリリース・トラックの紐付け | releaseId / trackId により Release/Track(Neon)の既存データと連携する |
| 外部サービスリンクの集約 | Spotify・Apple Music・YouTube Music 等の複数プラットフォームへのリンクを一元管理する |
| Blob → Neon 移行の経緯の記録 | §7で、なぜ移行するか(既存の不整合)と移行後の構成を明示する |
備考: 旧方式(Vercel Blob)からの変更経緯
sync-playlist-track-ids.ts・build-playlist-index.ts(releaseId/trackId自動紐付け・派生インデックス生成)が参照していたRELEASES_BLOB_URLは、リリースのNeon移行(v4.1.0、MorningStatusApp#853)時点で更新が止まったスナップショットのままで、現在のリリーススキーマにも適合しなかった(MorningStatusApp#1618で発覚)。プレイリスト参照の大半(sync-playlist-links.ts・check-playlist-integrity.ts・playlist-link.ts・app/playlists/[id]/page.tsx)は既にNeon(fetchReleasesFromDB()/getReleases())経由に移行済だったが、playlists.json本体・派生インデックスがVercel Blobのままだったため、上記2スクリプトだけがstaleなBlobスナップショットを参照し続ける状態になっていた(MorningStatusApp#1673・#1677のthink-issueで判明)。Playlist/PlaylistEntry自体をNeonに移行すれば、リリース参照も含めて同じNeon上で完結し、この種の参照元の不整合が構造的に起きなくなる。
3. 型定義
data/inputs/playlists.json の手動編集フォーマットとしては、以下の型定義を引き続き使用する(入力運用は変更しない。Neonへの格納方式は§7参照)。
ExternalLinkType の拡張
types/external-link.ts の ExternalLinkType は現在 Track 向けに 'youtube' | 'youtube-music' のみ定義されている。
プレイリスト機能(#330)の型定義実装時に、以下の音楽ストリーミングプラットフォームを追加した。
// types/external-link.ts(#330 で更新済)
export type ExternalLinkType =
| 'youtube'
| 'youtube-music'
| 'spotify'
| 'apple-music'
| 'line-music'
| 'recochoku'
| 'mora'
| 'other';
Track レベルのリンク(
Track.links)は引き続き'youtube' | 'youtube-music'のみを想定する。otherは上記以外のプラットフォームへのリンクを登録するための汎用値。
PlaylistEntry インターフェース
export interface PlaylistEntry {
/** 選曲したメンバーの ID(members.json の id フィールドと一致)。グループ曲プレイリスト等では省略可 */
memberId?: string;
/** 表示用の楽曲タイトル(trackId 未設定時も含め常に必須) */
songTitle: string;
/** releases.json の id フィールド(リリースが特定できる場合に付与。省略可) */
releaseId?: string;
/**
* Release.tracks 内の Track.id(MusicBrainz recording UUID)。
* releaseId と組み合わせて使用する。releaseId なしで trackId のみの指定は不正。
* アルバム収録曲・シングルカップリングを明示したい場合に設定する。省略可。
*/
trackId?: string;
}
v0.7.6 変更:
links: ExternalLink[]フィールドを廃止。外部リンクはreleases.jsonのTrack.links[]に一元管理する。詳細は tracklist-data-design.md を参照。
Playlist インターフェース
export interface Playlist {
/** プレイリストの識別子(kebab-case。例: fukumura-mizuki-playlist-2026) */
id: string;
/** プレイリスト名 */
name: string;
/** 説明文(省略可) */
description?: string;
/** 公開日(YYYY-MM-DD 形式) */
publishedAt: string;
/** 収録楽曲のリスト */
entries: PlaylistEntry[];
}
4. songTitle と Track.title の関係
PlaylistEntry には songTitle(手動入力)と、trackId が設定されている場合に参照できる Track.title(Neon上の正規表記)の2種類の曲名が存在する。
| 状況 | UI での表示 | 備考 |
|---|---|---|
trackId あり |
Track.title を優先して表示する |
Neonから引いた正規表記を使用 |
trackId なし |
songTitle を表示する |
ライブ音源などTrackに存在しない楽曲 |
trackId あり・Track.title と songTitle が食い違う |
Track.title を表示する |
表記揺れは Track.title 側に統一 |
songTitleの役割:playlists.jsonの手動入力データとして常に必須。trackIdが設定されている場合でも、UI はTrack.titleを表示するがsongTitleは検索・フォールバック用途に保持する。意図的な表記差が必要な場合はtrackIdを設定しないこと。
5. playlists.json 入力フォーマット仕様
フィールド一覧
| フィールド | 型 | 必須 | 制約・説明 |
|---|---|---|---|
playlists[].id |
string |
必須 | kebab-case。小文字英数字とハイフンのみ。プロジェクト内で一意 |
playlists[].name |
string |
必須 | プレイリスト名(表示用) |
playlists[].description |
string |
省略可 | プレイリストの説明文 |
playlists[].publishedAt |
string |
必須 | YYYY-MM-DD 形式 |
playlists[].entries[] |
PlaylistEntry[] |
必須 | 楽曲リスト(空配列も可) |
entries[].memberId |
string |
省略可 | members.json の id フィールドと一致させること。グループ曲プレイリスト等、個人に帰属しない場合は省略可 |
entries[].songTitle |
string |
必須 | 表示用の楽曲タイトル |
entries[].releaseId |
string |
省略可 | releases.json の id フィールドと一致させること |
entries[].trackId |
string |
省略可 | Track.id(recording UUID)。releaseId が設定されている場合のみ指定可 |
プラットフォーム列挙値
| 値 | 対応サービス | URL 形式例 |
|---|---|---|
spotify |
Spotify | https://open.spotify.com/track/{id} |
apple-music |
Apple Music | https://music.apple.com/jp/album/{title}/{id}?i={track_id} |
youtube-music |
YouTube Music | https://music.youtube.com/watch?v={id} |
youtube |
YouTube | https://www.youtube.com/watch?v={id} |
line-music |
LINE MUSIC | https://music.line.me/webapp/track/{id} |
recochoku |
レコチョク | https://recochoku.jp/song/{id} |
mora |
mora | https://mora.jp/package/{id} |
other |
上記以外 | 任意の URL |
id 命名規則
- 形式:
{memberId}-{playlist-description}-{year}(例:fukumura-mizuki-morning-musume-fav-2026) - kebab-case で記述し、大文字・スペース・記号(ハイフン以外)は使用しない
- プロジェクト内で一意性を担保すること(重複がないか手動で確認する)
記入例
{
"playlists": [
{
"id": "fukumura-mizuki-morning-musume-fav-2026",
"name": "譜久村聖セレクション:モーニング娘。名曲集",
"description": "譜久村聖が選ぶモーニング娘。の思い出の名曲",
"publishedAt": "2026-01-15",
"entries": [
{
"memberId": "fukumura-mizuki",
"songTitle": "LOVEマシーン",
"releaseId": "fcd58b7d-77c0-37db-9334-5f50d92b09f1",
"trackId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
},
{
"memberId": "fukumura-mizuki",
"songTitle": "ザ☆ピ〜ス!",
"releaseId": "another-release-id"
},
{
"memberId": "fukumura-mizuki",
"songTitle": "ライブ限定曲"
}
]
}
]
}
6. songTitle・エントリ順の運用
entries[]の配列順序がそのままプレイリスト内の表示順になる。Neon格納後はPlaylistEntry.entryOrder(§7-1)に配列インデックスをそのまま書き込むことで順序を保持する- 楽曲の追加・削除・並び替えは常に
data/inputs/playlists.jsonを編集してから反映する(§7-3の更新手順)。Neon上のレコードを直接編集する運用は想定しない
7. Blob → Neon 移行(MorningStatusApp#1677、morning-status-blume#166)
7-1. Prisma モデル(prisma/schema.prisma)
/// プレイリスト(メンバー選曲。data/inputs/playlists.json を手動編集し反映する運用は維持)
model Playlist {
playlistId String @id @db.VarChar(255)
name String @db.VarChar(255)
description String? @db.Text
publishedAt String @db.VarChar(20)
entries PlaylistEntry[]
@@map("playlists")
}
/// プレイリスト収録楽曲(entryOrder昇順 = 表示順)
model PlaylistEntry {
id Int @id @default(autoincrement())
playlistId String @db.VarChar(255)
memberId String? @db.VarChar(50)
songTitle String @db.VarChar(255)
releaseId String? @db.VarChar(255)
trackId String? @db.VarChar(255)
entryOrder Int
playlist Playlist @relation(fields: [playlistId], references: [playlistId], onDelete: Cascade)
release Release? @relation(fields: [releaseId], references: [releaseId])
track Track? @relation(fields: [trackId], references: [trackId])
@@index([playlistId])
@@index([memberId])
@@index([trackId])
@@map("playlist_entries")
}
Playlist.playlistIdは既存のid(kebab-case、§5参照)をそのまま使うPlaylistEntry.memberIdは他のNeonモデル(MemberFestival・MemberLive等)と同じく、Member(Neonの出身地照合用テーブル)への実FKは張らない。正の紐付け先はあくまでBlobのmembers.jsonであり、NeonのMemberテーブルは別目的の最小限のテーブルのため(リリース データ設計書 参照)release・trackは実FKを張る(Release・TrackはNeonが正のため)。Release・Trackモデルに逆参照フィールドplaylistEntries PlaylistEntry[]を追加する
7-2. 派生インデックスは持たない(直接クエリ方式)
旧member-playlist-index.json(メンバー別)・track-playlist-index.json(トラック別)は別テーブルを作らず、PlaylistEntryへの直接クエリで代替する。
// メンバー詳細画面(/members/[id])の「選曲した楽曲」セクション用
const entries = await prisma.playlistEntry.findMany({
where: { memberId },
include: { playlist: true, track: { include: { links: true } } },
orderBy: { playlist: { publishedAt: 'desc' } },
});
// 曲詳細画面(/songs/[id])の「プレイリスト掲載情報」セクション用
const entries = await prisma.playlistEntry.findMany({
where: { trackId },
include: { playlist: true },
});
根拠: 対比機能(ComparisonGroup/ComparisonGroupTrack、MorningStatusApp#1633)のNeon移行でも、旧comparison-index.json・track-comparison-map.jsonという同種の派生インデックスファイルは廃止され、直接クエリに一本化されている(データ設計書(全体概要) 改訂履歴1.20)。プレイリスト楽曲は数百件程度の規模であり、事前集計なしでも性能上の問題はないと考えられる。
build-playlist-index.ts(派生インデックス生成スクリプト)自体が不要になるため廃止する(§7-4)。
7-3. 更新手順(Neon移行後)
data/inputs/playlists.json を編集した後、以下を実行する運用は維持する(コマンド名・内部処理はNeon書き込みに置き換える)。
bun run upload-playlists
処理内容:
- ローカルの
data/inputs/playlists.jsonを読み込み、Playlist・PlaylistEntry(entryOrderは配列インデックス)をNeonにupsertする(Blobとの既存データマージは不要になる。Neonが唯一の格納先のため) releaseId/trackId未設定のエントリについて、NeonRelease/Track(fetchReleasesFromDB())を参照して自動紐付けを試みる(旧sync-playlist-track-ids.ts相当。RELEASES_BLOB_URLは参照しない)
編集内容をコミット・プッシュすると、GitHub Actionsのsync-playlist-links.ymlワークフローが自動実行される。このワークフローは各エントリのtrackId/releaseIdをもとにNeon track_linksテーブルの該当トラックにYouTube Music URLを付与する処理(sync-playlist-links.ts、変更なし)を引き続き行う。派生インデックス再生成のステップは廃止する(§7-2)。
環境変数の準備:
DATABASE_URL(Neon接続文字列)を使用する。BLOB_READ_WRITE_TOKEN・PLAYLISTS_BLOB_URLは不要になる(§7-5)。
7-4. 既存Blob設定・スクリプトの廃止・改修範囲
| 対象 | 種別 | 対応 |
|---|---|---|
scripts/workflow/sync-playlists-to-blob.ts |
書き込みスクリプト | Neonへの書き込みに変更(スクリプト名も実態に合わせて改名を検討) |
scripts/workflow/sync-playlist-track-ids.ts |
自動紐付けスクリプト | 読み書き先をNeonに変更。リリース参照をfetchReleasesFromDB()経由に変更しRELEASES_BLOB_URL依存を解消する |
scripts/workflow/build-playlist-index.ts |
派生インデックス生成スクリプト | 廃止(§7-2、直接クエリ方式に置き換えるため不要) |
scripts/workflow/sync-playlist-links.ts |
YouTube Music URL自動取得 | プレイリスト取得元をPLAYLISTS_BLOB_URLからNeonクエリに変更 |
scripts/console/check-playlist-integrity.ts |
整合性チェックCLI | 同上 |
scripts/console/playlist-link.ts |
手動リンク修正CLI | 同上 |
scripts/workflow/sync-discography.tsのfetchPlaylistTrackIds() |
staleトラック保護判定 | プレイリスト参照元をNeonクエリに変更 |
scripts/workflow/check-blob-secrets.ts(W16診断バッチ) |
Blob URLシークレット診断 | PLAYLISTS_BLOB_URL・MEMBER_PLAYLIST_INDEX_BLOB_URL・TRACK_PLAYLIST_INDEX_BLOB_URLを診断対象から除外 |
lib/blob.tsのgetPlaylistsFromBlob()・getMemberPlaylistIndexFromBlob()・getTrackPlaylistIndexFromBlob() |
読み取り関数 | Prismaクエリ(新設するlib/playlists.ts等)に置き換える |
app/playlists/page.tsx・app/playlists/[id]/page.tsx・app/members/[id]/page.tsx・app/songs/[id]/page.tsx |
画面 | 上記読み取り関数の呼び出し元を差し替える |
PLAYLISTS_BLOB_URL・MEMBER_PLAYLIST_INDEX_BLOB_URL・TRACK_PLAYLIST_INDEX_BLOB_URL |
環境変数 | 削除(環境変数一覧) |
.github/workflows/sync-playlist-links.yml |
ワークフロー | Blob関連ステップをNeon向けに書き換える。pushトリガー再開要否は実装時に判断する(コメントアウト理由だった#854は完了済) |
7-5. データ取得層の適用判定(BFF判定)
データ設計書(全体概要) §4の適用基準に基づき判定した結果、プレイリスト機能(/playlists・/playlists/[id]・/members/[id]選曲セクション・/songs/[id]掲載情報セクション)は「適用しない」。理由:
- いずれの画面もServer Componentでの直接取得のままで、クライアント側の再取得・複数画面での共有取得は不要
/playlists/[id]はplaylists・releases・members・unitsを結合するが、これは「結合して派生データを算出する」パターン(世代マトリックス画面等と同型)であり、判別共用体としての統合(適用基準②)には該当しない
判定ログは統合フィードアイテム データ設計書 §3に記載する。
8. ER 図
→ docs/design/er-diagram.mdx を参照すること。
Playlist / PlaylistEntry エンティティの格納場所は、Neon(Vercel Postgres)移行後の記載に更新済。
9. バージョン位置づけ
| バージョン | 機能 | 対応 Issue |
|---|---|---|
| v0.5.0 | トラックリスト管理機能 | #341(#338・#339・#340) |
| v0.6.0 | プレイリスト管理機能 | #336(#328〜#335・#349・#351) |
| 実装時に追記 | プレイリストデータのBlob→Neon移行 | MorningStatusApp#1677(morning-status-blume#166・MorningStatusApp#1680) |
10. 後続 Issue との関係
| Issue | 本設計書への依存内容 |
|---|---|
#330 Playlist / PlaylistEntry 型定義・playlists.json 基盤追加 |
§3 の型定義に基づいて実装 |
| #332 プレイリスト一覧・詳細ページ実装 | 型定義・読み取り方針に基づいて実装 |
| #333 メンバー詳細画面への選曲楽曲一覧セクション追加 | §7-2 の直接クエリに基づいて実装 |
| #401 曲詳細画面へのプレイリスト掲載情報セクション追加 | §7-2 の直接クエリに基づいて実装 |
#351 PlaylistEntry への YouTube Music URL 自動取得スクリプト追加 |
§3 の型定義に基づいて実装 |
#364 PlaylistEntry への trackId フィールド追加 |
§3 の PlaylistEntry.trackId 仕様に基づいて実装 |
| MorningStatusApp#1680 プレイリストデータのNeon移行 実装 | §7 のPrismaモデル・移行範囲に基づいて実装 |
改訂履歴
| 版 | 更新日 | 変更内容 |
|---|---|---|
| 1.3 | 2026-09-29 | 【対応する実装: 実装時に追記(MorningStatusApp#1680)】playlists.json(Vercel Blob)からPlaylist/PlaylistEntry(Neon)への移行を反映。Prismaモデル設計(§7-1)・派生インデックス廃止と直接クエリ方式への転換(§7-2)・既存Blob設定/スクリプトの廃止範囲(§7-4)・BFF適用判定「適用しない」(§7-5)を追加。旧§6「JSONスキーマとVercel Blob格納方針」・§7「派生集計JSON」は§7へ統合・置き換え(重複していた見出し番号8の誤りもあわせて解消) |
| 1.2 | 2026-03-27 | 派生集計 JSON セクションの構成説明を修正 |
| 1.1 | 2026-03-22 | release-playlist-index.json を track-playlist-index.json(trackId キー)に変更(#402)。リリース詳細画面のプレイリスト掲載情報セクション削除(#400)。 |
| 1.0 | 2026-03-21 | 初版作成(#328) |