---
title: "プレイリスト機能 データ設計書"
---

最終更新: 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）の型定義実装時に、以下の音楽ストリーミングプラットフォームを追加した。

```typescript
// 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` インターフェース

```typescript
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](/design/discography/tracklist-data-design) を参照。

### `Playlist` インターフェース

```typescript
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 で記述し、大文字・スペース・記号（ハイフン以外）は使用しない
- プロジェクト内で一意性を担保すること（重複がないか手動で確認する）

### 記入例

```json
{
  "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コマンドで両方完結する。

```bash
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`（ローカル開発用）**

```bash
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 から取得する（リリースは Neon `fetchReleasesFromDB()` / `getReleases()` 経由）

---

## 7. 派生集計 JSON

画面レンダリング時に毎回全走査を避けるため、事前集計した派生 JSON を生成する。

### `member-playlist-index.json`

**目的**: メンバー詳細画面（`/members/[id]`）の「選曲した楽曲」セクション表示用

**構造**:

```json
{
  "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]`）の「プレイリスト掲載情報」セクション表示用

**構造**:

```json
{
  "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](/design/common/er-diagram) を参照すること。

`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） |
