曲対比機能 データ設計書
最終更新: 2026-03-29
1. 概要
同名曲のオリジナルとリメイク(バリアント)を比較できるようにするためのデータ設計。
releases.json からトラックを抽出し、曲名の前方一致でペアリングした対比インデックスを生成する。
2. 背景・目的
| 目的 | 詳細 |
|---|---|
| オリジナル/リメイクの対比 | 同一曲名をベースとする複数バリアントを1組のペアとして管理する |
| 動画の横断比較 | YouTube 動画 ID をインデックス生成時に解決し、対比画面での即時表示を実現する |
| 曲詳細画面との連携 | track-comparison-map.json で O(1) 検索を可能にし、曲詳細画面での対比リンク表示に利用する |
3. 型定義
ComparisonTrack インターフェース(types/comparison.ts)
/** 対比ペア内の 1 曲 */
export interface ComparisonTrack {
/** Track.id(MusicBrainz recording UUID) */
trackId: string;
/** 曲タイトル(バリアント名を含む) */
title: string;
/** YouTube 動画 ID(Track.links に youtube リンクがある場合。なければ null) */
youtubeVideoId: string | null;
/** 代表リリース ID(findPreferredRelease による解決結果。なければ null) */
releaseId: string | null;
/** 代表リリースタイトル(表示用。なければ null) */
releaseTitle: string | null;
/** 代表リリースのフォーマット("single" / "ep" / "album" / "other"。なければ null) */
releaseFormat: string | null;
}
ComparisonPair インターフェース(types/comparison.ts)
/** 対比ペア(1 組)*/
export interface ComparisonPair {
/**
* ペア ID。
* グループ内でタイトルが baseTitle と一致するトラックの Track.id を使用する。
* 該当トラックが存在しない場合(バリアントのみのグループ)は、
* タイトル長昇順・辞書順で先頭のトラックの Track.id を使用する。
*/
id: string;
/** 共通ベースタイトル(グループ内の最短タイトル、例: "XXX") */
baseTitle: string;
/** 収録曲の配列。ベース曲が先頭、以降タイトル長昇順・辞書順 */
tracks: ComparisonTrack[];
}
4. 生成スクリプト
スクリプト: scripts/build-comparison-index.ts
releases.json から全トラックを収集し、対比インデックス 2 ファイルを Vercel Blob に書き込む。
実行コマンド
BLOB_READ_WRITE_TOKEN=... RELEASES_BLOB_URL=... bun run scripts/build-comparison-index.ts
処理フロー
scripts/comparison-exclude-patterns.jsonから除外ベースタイトル一覧を読み込むRELEASES_BLOB_URLからreleases.jsonを取得する- 全リリースからトラックを収集し、
Track.idで重複排除する - 前方一致判定でトラックをグループ化する(§5 参照)
- 2 曲以上かつ除外パターンに一致しないグループを対比ペアとして採用する
- 各トラックの YouTube 動画 ID・代表リリース(フォーマット含む)を解決する
comparison-index.jsonとtrack-comparison-map.jsonを Blob に書き込む
除外パターン設定
scripts/comparison-exclude-patterns.json に除外するベースタイトルを列挙する(完全一致)。
{
"baseTitles": ["除外したい曲名A", "除外したい曲名B"]
}
ファイルが存在しない・フォーマットが不正な場合は除外なしとして処理する。
出力ファイル
| ファイル名 | 構造 | 環境変数 |
|---|---|---|
comparison-index.json |
{ "pairs": ComparisonPair[] } |
COMPARISON_INDEX_BLOB_URL |
track-comparison-map.json |
{ [trackId: string]: pairId } |
TRACK_COMPARISON_MAP_BLOB_URL |
必要な環境変数
| 変数名 | 説明 |
|---|---|
BLOB_READ_WRITE_TOKEN |
Vercel Blob の書き込みトークン |
RELEASES_BLOB_URL |
releases.json の Blob URL |
5. 前方一致判定ロジック
定義
トラック B が トラック A のバリアントである条件:
B.title.startsWith(A.title)が trueB.title.length > A.title.length(同一タイトルは除外)B.title[A.title.length]がセパレータ文字(/[\s((〜\-]/)に一致する
例
| ベース | バリアント | 判定 |
|---|---|---|
"XXX" |
"XXX (A Ver.)" |
✅ セパレータ "(" |
"XXX" |
"XXX〜ライブ〜" |
✅ セパレータ "〜" |
"XXX" |
"XXX -リミックス-" |
✅ セパレータ "-" |
"XXX" |
"XXXYYY" |
❌ セパレータなし |
"XXX" |
"XXX" |
❌ 同一タイトル |
ベースタイトルの算出
各タイトルについて、上記条件を満たす最短の他タイトルを「ベースタイトル」とする。 該当するものが存在しない場合は自身がベースタイトル。
titles = ["A", "A (B Ver.)", "A (B Ver.) [C Mix]"]
"A" → 自身がベース → group["A"]
"A (B Ver.)" → "A" が前方一致の最短候補 → group["A"]
"A (B Ver.) [C Mix]" → "A" が前方一致の最短候補 → group["A"]
6. Blob 読み取り関数
lib/blob.ts に以下の関数を追加する。
/** comparison-index.json から対比ペア一覧を取得する。未設定・失敗時は空配列を返す */
export async function getComparisonIndexFromBlob(): Promise<ComparisonPair[]>
/** track-comparison-map.json から trackId → { pairId, baseTitle } マップを取得する。未設定・失敗時は空オブジェクトを返す */
export async function getTrackComparisonMapFromBlob(): Promise<Record<string, { pairId: string; baseTitle: string }>>
両関数とも環境変数未設定・Blob 取得失敗時はフェイルセーフとして空を返す(プレイリストインデックスと同方針)。