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

曲対比機能 データ設計書

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

処理フロー

  1. scripts/comparison-exclude-patterns.json から除外ベースタイトル一覧を読み込む
  2. RELEASES_BLOB_URL から releases.json を取得する
  3. 全リリースからトラックを収集し、Track.id で重複排除する
  4. 前方一致判定でトラックをグループ化する(§5 参照)
  5. 2 曲以上かつ除外パターンに一致しないグループを対比ペアとして採用する
  6. 各トラックの YouTube 動画 ID・代表リリース(フォーマット含む)を解決する
  7. comparison-index.jsontrack-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 のバリアントである条件:

  1. B.title.startsWith(A.title) が true
  2. B.title.length > A.title.length(同一タイトルは除外)
  3. 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 取得失敗時はフェイルセーフとして空を返す(プレイリストインデックスと同方針)。

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