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

プレイリスト機能 データ設計書

最終更新: 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

処理内容:

  1. ローカルのdata/inputs/playlists.jsonを読み込み、Playlist・PlaylistEntry(entryOrderは配列インデックス)をNeonにupsertする(Blobとの既存データマージは不要になる。Neonが唯一の格納先のため)
  2. releaseId/trackId未設定のエントリについて、Neon Release/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)

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