fix(web): resolve album names to the studio Album, not a same-title Single

searchReleaseGroup took the first MusicBrainz hit, so a title MB ranks with the
single first (e.g. "Thriller" ties single+album at score 100) resolved to the
single — the Last.fm album modal and the Want flow then showed a 1-track single.
Prefer a clean Album > EP > Single > album-with-secondary, keeping MB order as the
in-tier tiebreak; single-only titles still resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-13 21:06:15 +02:00
parent 146bf32f23
commit 13126addbc
2 changed files with 38 additions and 1 deletions
+18 -1
View File
@@ -58,12 +58,29 @@ function escapeLucenePhrase(s: string): string {
return s.replace(/[\\"]/g, (c) => "\\" + c);
}
// When several release groups share the searched title (e.g. MusicBrainz ties the
// "Thriller" single with the album at the same score, listing the single first), prefer
// the canonical studio album. Lower rank = better; MB's own ordering breaks ties within
// a tier. A title that only exists as a single still resolves (it's the best available).
function albumPreferenceRank(g: any): number {
const primary = g["primary-type"] ?? "";
const hasSecondary = (g["secondary-types"] ?? []).length > 0;
if (primary === "Album" && !hasSecondary) return 0; // clean studio album
if (primary === "EP" && !hasSecondary) return 1;
if (primary === "Single" && !hasSecondary) return 2;
if (primary === "Album") return 3; // album with a secondary type (compilation, live, demo…)
return 4;
}
export async function searchReleaseGroup(artist: string, album: string): Promise<ReleaseGroupMatch | null> {
const query = `releasegroup:"${escapeLucenePhrase(album)}" AND artist:"${escapeLucenePhrase(artist)}"`;
const data = await mbGet(`/release-group?query=${encodeURIComponent(query)}&fmt=json&limit=5`);
const groups: any[] = data["release-groups"] ?? [];
if (groups.length === 0) return null;
const g = groups[0];
// groups arrive in MB relevance order; keep that as the tiebreak (reduce only switches on
// a strictly better rank, so equal-ranked earlier hits win).
const g = groups.reduce((best, cur) =>
albumPreferenceRank(cur) < albumPreferenceRank(best) ? cur : best);
const credit = g["artist-credit"]?.[0]?.artist ?? {};
return { ...toReleaseGroup(g), artistMbid: credit.id ?? "", artistName: credit.name ?? artist };
}