Skip to content

fix(enrichment): rank the canonical studio take over live/comp versions - #758

Merged
byrongamatos merged 4 commits into
mainfrom
feat/mb-canonical-ranking
Jul 4, 2026
Merged

fix(enrichment): rank the canonical studio take over live/comp versions#758
byrongamatos merged 4 commits into
mainfrom
feat/mb-canonical-ranking

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Problem

A flat MusicBrainz /recording text search ties every take of a song at the same score. Searching AC/DC — "Highway to Hell" returns ~10 live bootlegs and compilations with the canonical 1979 studio version buried below the fetch limit. This hits the editor's new "Match metadata" popup and the background library enrichment (both share lib/mb_match.py + /api/enrichment/search).

Fix (verified against live MusicBrainz)

  • build_recording_query — drop live-only recordings (-secondarytype:Live). Compilations are deliberately kept: they reuse the studio recording, so filtering them cut the very recording we want (empirically confirmed).
  • _best_release / parse_recording_doc — pick the canonical studio album (primary Album, no Live/Compilation/Remix/… secondary type) for the displayed album/year, and expose a studio flag.
  • rank_candidates — the combined score caps at 1.0, so perfect-text-match candidates tie; break ties on the studio flag and, when the caller knows the audio length, on duration proximity. The studio distinction is not scored (a live take is still the right song) — only re-ordered.
  • /api/enrichment/search — accept an optional duration param so a caller that has the audio but no library row (the editor's create modal, which holds the master track) can pass the length for the duration tiebreak.

Result: AC/DC "Highway to Hell" now returns the 1979 studio recording at #1 with the correct album + year.

Tests

tests/test_mb_match.py + tests/test_mb_enrichment.py — 48 passing (query-string assertion updated for the new -secondarytype:Live).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Release matching now treats “clean studio albums” as canonical, prioritizing official studio releases over live or mixed types.
    • MusicBrainz enrichment search can use an optional track duration to strengthen matches when duration metadata is missing locally.
  • Bug Fixes
    • Live recordings are filtered more reliably, while “(Live …)”/bracketed live chart titles still allow live takes through.
    • Candidate tie-breaking now accounts for whether the chart is live before applying duration-proximity selection.
  • Chores
    • Increased the number of MusicBrainz recording candidates considered to improve downstream ranking quality.

A flat MusicBrainz /recording text search ties every take of a song at the
same score, so "AC/DC — Highway to Hell" returns a wall of live bootlegs and
compilations with the 1979 studio version buried (or below the fetch limit).

- build_recording_query: drop live-ONLY recordings (`-secondarytype:Live`).
  Compilations are deliberately kept — they REUSE the studio recording, so
  filtering them cuts the very recording we want (verified against MB).
- _best_release / parse_recording_doc: pick the canonical studio album
  (primary Album, no Live/Compilation/Remix/... secondary type) for the
  displayed album/year, and expose a `studio` flag.
- rank_candidates: since the combined score caps at 1.0 (perfect text match
  ties), break ties on the studio flag and — when the caller knows the audio
  length — on duration proximity, so the studio take wins over live/extended
  cuts. The studio distinction is intentionally NOT scored (a live take is
  still the right SONG), only re-ordered.
- /api/enrichment/search: accept an optional `duration` param so a caller that
  has the audio but no library row (the editor's create modal) can pass the
  master-track length for the duration tiebreak.

Verified end-to-end against live MusicBrainz: AC/DC "Highway to Hell" now
returns the 1979 studio recording at #1 with the correct album + year.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8774d996-a68c-4363-98b4-05a0a660b417

📥 Commits

Reviewing files that changed from the base of the PR and between 73e25fe and 03ca3a3.

📒 Files selected for processing (2)
  • lib/mb_match.py
  • tests/test_mb_match.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/mb_match.py

📝 Walkthrough

Walkthrough

MusicBrainz matching now prefers clean studio albums and studio takes, excludes live-only recordings for non-live charts, and lets enrichment search use an optional duration hint with a larger candidate limit.

Changes

Studio vs non-studio recording preference

Layer / File(s) Summary
Clean studio album detection and release selection
lib/mb_match.py, tests/test_mb_match.py
Adds _SECONDARY_SKIP and _is_clean_studio_album, reworks _best_release to prefer clean studio albums with OFFICIAL status and earliest date, and computes/returns the studio flag from the selected release. Tests assert the selected release and studio flag.
Query filtering and ranking tie-breaks
lib/mb_match.py, tests/test_mb_match.py
Updates the scoring comment, changes rank_candidates tie-breaking to use chart live detection, duration closeness, and mb_score, and changes build_recording_query to AND denoised terms while excluding live-only recordings except for live chart titles. The query tests are updated accordingly.
Enrichment search duration support
server.py
_mb_search_recordings now defaults to a larger result limit, and /api/enrichment/search accepts an optional duration value that is copied into the reference data when missing before candidate ranking.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: prioritizing the canonical studio take over live and compilation variants in enrichment ranking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mb-canonical-ranking

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
server.py (1)

6238-6247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace ambiguous en dash flagged by Ruff (RUF002).

Line 6239's docstring uses (EN DASH) instead of a plain hyphen.

🔤 Suggested fix
-    """Text search (tier 2–4): denoised Lucene query over /recording. The query
+    """Text search (tier 2-4): denoised Lucene query over /recording. The query
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.py` around lines 6238 - 6247, The docstring in _mb_search_recordings
contains an ambiguous en dash that Ruff flags as RUF002; replace the EN DASH in
the phrase describing the tier range with a plain hyphen while keeping the rest
of the docstring unchanged. Locate the change in _mb_search_recordings and
ensure any similar punctuation in nearby docstrings is normalized to
ASCII-compatible hyphens.

Source: Linters/SAST tools

lib/mb_match.py (1)

273-286: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring understates the priority ordering between "clean studio" and "official".

The sort key treats clean as strictly higher priority than status_ok — a non-official clean studio release will beat an official but non-clean (e.g. compilation) release. The docstring's phrasing ("prefer an OFFICIAL studio Album ... then the earliest date") reads as if official-and-clean is one combined criterion, obscuring that clean strictly dominates official. Worth tightening the wording so future edits to this ordering don't accidentally couple the two criteria.

✏️ Suggested docstring clarification
 def _best_release(doc: dict) -> dict:
-    """Pick the release used for canon album/year: prefer an OFFICIAL studio
-    Album (primary Album with no Live/Compilation/… secondary type), then the
-    earliest date. Falls back to any release when none is clean. {} if none."""
+    """Pick the release used for canon album/year. Priority, in order:
+    (1) a clean studio Album (primary Album, no Live/Compilation/… secondary
+    type) over anything else, (2) OFFICIAL status, (3) earliest date.
+    Falls back to any release when none is clean. {} if none."""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mb_match.py` around lines 273 - 286, Clarify the _best_release docstring
so it matches the actual sort_key ordering: clean studio status is checked
before official status, meaning a clean non-official release can outrank an
official non-clean one. Update the wording near _best_release and sort_key to
state the priority as clean studio first, then official, then earliest date, so
the criteria are not read as a combined condition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/mb_match.py`:
- Around line 193-217: The new studio tie-break in rank_candidates only affects
freshly ranked rows, so older song_enrichment.candidates entries still use the
old stored shape and won’t benefit from the studio field. Update the read path
that uses rank_candidates (or the surrounding review-candidate loading logic) to
re-rank persisted candidates on fetch, or add a backfill/migration to populate
studio for existing review rows so they can be sorted with the new tie-break.

---

Nitpick comments:
In `@lib/mb_match.py`:
- Around line 273-286: Clarify the _best_release docstring so it matches the
actual sort_key ordering: clean studio status is checked before official status,
meaning a clean non-official release can outrank an official non-clean one.
Update the wording near _best_release and sort_key to state the priority as
clean studio first, then official, then earliest date, so the criteria are not
read as a combined condition.

In `@server.py`:
- Around line 6238-6247: The docstring in _mb_search_recordings contains an
ambiguous en dash that Ruff flags as RUF002; replace the EN DASH in the phrase
describing the tier range with a plain hyphen while keeping the rest of the
docstring unchanged. Locate the change in _mb_search_recordings and ensure any
similar punctuation in nearby docstrings is normalized to ASCII-compatible
hyphens.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7d4f974-6021-4fe5-b207-fbdda8a39a80

📥 Commits

Reviewing files that changed from the base of the PR and between b6169af and fc52d1f.

📒 Files selected for processing (3)
  • lib/mb_match.py
  • server.py
  • tests/test_mb_match.py

Comment thread lib/mb_match.py
Comment on lines 193 to 217
def rank_candidates(song: dict, candidates: list[dict]) -> list[dict]:
"""Score every candidate against the song and return them sorted by our
score (MusicBrainz's own search score is only a tiebreak). Each returned
dict is a copy carrying `score` (rounded — it's displayed and stored)."""
"""Score every candidate against the song and return them sorted best-first.
The combined `score` caps at 1.0, so a perfect-text-match query (every "AC/DC
Highway to Hell" recording) ties at the top — there the studio flag and, when
the caller knows the audio length, the duration match break the tie so the
canonical studio take wins over live/promo/extended cuts. Each returned dict
is a copy carrying `score` (rounded — it's displayed and stored)."""
sd = _duration_int(song.get("duration"))

def _dur_diff(c):
cd = _duration_int(c.get("duration"))
return abs(sd - cd) if (sd and cd) else 10 ** 6

ranked = []
for cand in candidates or []:
c = dict(cand)
c["score"] = round(score_candidate(song, cand), 4)
ranked.append(c)
ranked.sort(key=lambda c: (c["score"], c.get("mb_score") or 0), reverse=True)
ranked.sort(
key=lambda c: (c["score"],
1 if c.get("studio") else 0, # canonical studio take
-_dur_diff(c), # closest to the audio length
c.get("mb_score") or 0),
reverse=True)
return ranked

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all rank_candidates call sites and see what "candidates" argument they use
rg -nP -C4 '\brank_candidates\(' server.py lib/mb_match.py

# Check if there is any persistence/backfill logic touching song_enrichment.candidates
rg -nP -C3 'song_enrichment' server.py | rg -n 'candidates'

Repository: got-feedBack/feedBack

Length of output: 2727


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the parser/docstring and any persistence paths around `candidates`
rg -n -C4 'parse_recording_doc|candidates|song_enrichment' lib server.py

# Read the relevant code around the API endpoint that returns ranked candidates
sed -n '7390,7460p' server.py

# Read the code around the MusicBrainz search / lookup helpers that feed rank_candidates
sed -n '6580,6660p' server.py

Repository: got-feedBack/feedBack

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find every place candidates are written to or read from storage
rg -n -C3 'apply_enrichment_match|candidates\s*=|\.candidates\b|song_enrichment.*candidates|UPDATE .*candidates|INSERT .*candidates|SELECT .*candidates' server.py lib

Repository: got-feedBack/feedBack

Length of output: 8188


Backfill stored review candidates for the new studio tie-break

Rows already persisted in song_enrichment.candidates keep the old shape, so review entries created before this change still rank without studio until refreshed. Re-rank on read or add a backfill for existing review rows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/mb_match.py` around lines 193 - 217, The new studio tie-break in
rank_candidates only affects freshly ranked rows, so older
song_enrichment.candidates entries still use the old stored shape and won’t
benefit from the studio field. Update the read path that uses rank_candidates
(or the surrounding review-candidate loading logic) to re-rank persisted
candidates on fetch, or add a backfill/migration to populate studio for existing
review rows so they can be sorted with the new tie-break.

claude added 3 commits July 4, 2026 22:59
_best_release sorted (clean, status_ok, date), so an UNofficial bootleg Album
outranked an official Single/EP/comp — regressing canonical album/year and
seeding cover-art from a bootleg for single-only songs. Order status_ok before
clean: official first, then prefer a clean studio album among the official
releases (still surfaces the studio album over an official live/comp album).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_recording_query unconditionally added -secondarytype:Live, but denoise()
strips a '(Live at …)' qualifier from the query — so a chart that IS a live take
had its only correct recording filtered out (both background enrichment and
manual search). Skip the live filter when the source title carries a
parenthetical live marker; a bare title word ('Live and Let Die') still filters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-through on keeping live recordings for live charts: rank_candidates still
ranked the studio take ahead of a tied live one, so a live chart would auto-match
the studio recording. Skip the studio tiebreak when the source title has a live
marker — duration proximity + score then pick the right live version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@byrongamatos

Copy link
Copy Markdown
Contributor

Review + fix pass (Claude Code). The ranking logic is sound; pushed 3 fixes closing the live-vs-studio loop (26 mb_match tests green). CI red is the unrelated main-level starter-content bug.

  1. Official releases must outrank unofficial studio albums. _best_release sorted (clean, status_ok, date), so a bootleg Album beat an official Single/EP/comp — regressing canonical album/year and seeding cover-art from a bootleg for single-only songs. Reordered to (status_ok, clean, date): official first, then prefer a clean studio album among official releases (still surfaces the studio album over an official live/comp album).
  2. Live charts kept their live recordings. build_recording_query unconditionally added -secondarytype:Live, but denoise() strips a '(Live at …)' qualifier — so a genuinely-live chart had its only correct recording filtered out. Now skips the live filter when the source title has a parenthetical live marker (a bare word like 'Live and Let Die' still filters).
  3. Live charts no longer forced to the studio take. Consequence of (2): rank_candidates still preferred the studio flag, so a live chart would auto-match the studio recording. Now drops the studio tiebreak for live charts — duration/score pick the right live version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants