プレイリスト機能 データ設計書
最終更新: 2026-03-21
1. 概要
playlists.json にメンバーが選曲したプレイリストデータを管理するためのデータ設計。
プレイリスト一覧・詳細ページ(#332)、メンバー詳細画面への選曲セクション追加(#333)、
リリース詳細画面へのプレイリスト掲載情報追加(#334)の前提基盤となる。
2. 背景・目的
| 目的 | 詳細 |
|---|---|
| メンバー選曲データの管理 | 各メンバーが選曲した楽曲をプレイリスト単位で管理する |
| 楽曲とリリース・トラックの紐付け | releaseId / trackId により releases.json の既存データと連携する |
| 外部サービスリンクの集約 | Spotify・Apple Music・YouTube Music 等の複数プラットフォームへのリンクを一元管理する |
3. 型定義
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(releases.json の正規表記)の2種類の曲名が存在する。
| 状況 | UI での表示 | 備考 |
|---|---|---|
trackId あり |
Track.title を優先して表示する |
releases.json から引いた正規表記を使用 |
trackId なし |
songTitle を表示する |
ライブ音源など releases.json に存在しない楽曲 |
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. JSON スキーマと Vercel Blob 格納方針
ファイル配置
| ファイル | 用途 | 更新方法 |
|---|---|---|
data/inputs/playlists.json |
ビルド凍結用の静的データ(Git 管理) | 手動編集 |
Vercel Blob(playlists.json) |
本番環境での読み取り元 | bun run upload-playlists でアップロード |
Vercel Blob(member-playlist-index.json) |
メンバー詳細画面の選曲セクション用 | bun run build-playlist-index で再生成 |
Vercel Blob(track-playlist-index.json) |
曲詳細画面のプレイリスト掲載情報用 | bun run build-playlist-index で再生成 |
プレイリスト更新手順
data/inputs/playlists.json を編集した後、以下を実行する。upload-playlists は内部で build-playlist-index も実行するため、1コマンドで両方完結する。
bun run upload-playlists
編集内容をコミット・プッシュすると、GitHub Actions の sync-playlist-links.yml ワークフローが自動実行される。このワークフローは各エントリの trackId / releaseId をもとに Neon track_links テーブルの該当トラックに YouTube Music URL を付与し(YOUTUBE_API_KEY を使用)、その後 build-playlist-index による派生インデックスの再生成も行われる。
環境変数の準備:
BLOB_READ_WRITE_TOKENは Vercel ダッシュボード → Blob ストア → Tokens から取得する。 ローカルで実行する場合は.env.localに設定する。PLAYLISTS_BLOB_URLも設定されている場合、upload-playlistsは Blob の既存データとマージしてから上書きする。
派生インデックスの環境変数設定
bun run upload-playlists(または bun run build-playlist-index)を初めて実行すると、コンソールに以下の URL が出力される。
member-playlist-index.json URL: https://xxxx.public.blob.vercel-storage.com/member-playlist-index.json
→ MEMBER_PLAYLIST_INDEX_BLOB_URL 環境変数に設定すること
track-playlist-index.json URL: https://xxxx.public.blob.vercel-storage.com/track-playlist-index.json
→ TRACK_PLAYLIST_INDEX_BLOB_URL 環境変数に設定すること
出力された URL を以下の2か所に設定する。
.env.local(ローカル開発用)
MEMBER_PLAYLIST_INDEX_BLOB_URL=https://xxxx.public.blob.vercel-storage.com/member-playlist-index.json
TRACK_PLAYLIST_INDEX_BLOB_URL=https://xxxx.public.blob.vercel-storage.com/track-playlist-index.json
Vercel 環境変数(本番・プレビュー用)
Vercel ダッシュボード → プロジェクト設定 → Environment Variables に追加する。 設定時は Environments のチェックボックスで Production・Preview・Development すべてを有効にすること(特定の Environment のみに限定すると、その環境以外では未設定扱いになる)。
フェイルセーフ: 環境変数が未設定の場合、該当セクションは非表示になる(エラーにはならない)。
読み取り方針
- 読み取り専用: プレイリストデータは編集 API(PUT)を持たない。
data/inputs/playlists.jsonを直接編集してbun run upload-playlistsでアップロードする運用とする - プレイリスト本体は
fetch+cache: "no-store"で Vercel Blob から取得する(リリースは NeonfetchReleasesFromDB()/getReleases()経由)
7. 派生集計 JSON
画面レンダリング時に毎回全走査を避けるため、事前集計した派生 JSON を生成する。
member-playlist-index.json
目的: メンバー詳細画面(/members/[id])の「選曲した楽曲」セクション表示用
構造:
{
"fukumura-mizuki": [
{
"playlistId": "fukumura-mizuki-morning-musume-fav-2026",
"playlistName": "譜久村聖セレクション:モーニング娘。名曲集",
"songTitle": "LOVEマシーン",
"releaseId": "fcd58b7d-77c0-37db-9334-5f50d92b09f1",
"trackId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"links": [...]
}
]
}
8. (将来的な拡張性)
生成タイミング: playlists.json 更新時(scripts/build-playlist-index.ts 実行時)
格納先: Vercel Blob(member-playlist-index.json)
track-playlist-index.json
目的: 曲詳細画面(/songs/[id])の「プレイリスト掲載情報」セクション表示用
構造:
{
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": [
{
"playlistId": "fukumura-mizuki-morning-musume-fav-2026",
"playlistName": "譜久村聖セレクション:モーニング娘。名曲集",
"memberId": "fukumura-mizuki",
"memberName": "譜久村聖",
"songTitle": "LOVEマシーン"
}
]
}
生成タイミング: playlists.json 更新時(scripts/build-playlist-index.ts 実行時)
格納先: Vercel Blob(track-playlist-index.json)
8. ER 図
→ docs/design/er-diagram.mdx を参照すること。
Playlist / PlaylistEntry エンティティの位置づけは ER 図の「v0.6.0 完成時点・全体」セクションに記載。
9. バージョン位置づけ
| バージョン | 機能 | 対応 Issue |
|---|---|---|
| v0.5.0 | トラックリスト管理機能 | #341(#338・#339・#340) |
| v0.6.0 | プレイリスト管理機能 | #336(#328〜#335・#349・#351) |
10. 後続 Issue との関係
| Issue | 本設計書への依存内容 |
|---|---|
#330 Playlist / PlaylistEntry 型定義・playlists.json 基盤追加 |
§3 の型定義・§6 のファイル配置に基づいて実装 |
| #331 派生集計 JSON 生成スクリプト追加 | §7 の集計 JSON 構造に基づいて実装 |
| #332 プレイリスト一覧・詳細ページ実装 | 型定義・Blob 読み取り方針(§6)に基づいて実装 |
| #333 メンバー詳細画面への選曲楽曲一覧セクション追加 | member-playlist-index.json(§7)に基づいて実装 |
| #401 曲詳細画面へのプレイリスト掲載情報セクション追加 | track-playlist-index.json(§7)に基づいて実装 |
#351 PlaylistEntry への YouTube Music URL 自動取得スクリプト追加 |
links フィールドの構造(§3)に基づいて実装 |
#364 PlaylistEntry への trackId フィールド追加 |
§3 の PlaylistEntry.trackId 仕様に基づいて実装 |
改訂履歴
| 版 | 更新日 | 変更内容 |
|---|---|---|
| 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) |