diff --git a/lib/mb_match.py b/lib/mb_match.py index d5fa18e..f4fe07e 100644 --- a/lib/mb_match.py +++ b/lib/mb_match.py @@ -275,7 +275,15 @@ def build_recording_query(artist, title, *, loose: bool = False) -> str: # (punctuation → spaces, diacritics stripped, & → "and"), so no # Lucene-special character survives to need escaping. Group each field's # terms and require both groups. - return " AND ".join("(%s)" % g for g in (t, a) if g) + q = " AND ".join("(%s)" % g for g in (t, a) if g) + # Keep the SAME live exclusion as the strict path: the loose query is + # lower-precision, and score_candidate doesn't penalize a live take, so + # without this a studio chart whose strict query missed could fall back + # to — and auto-confirm — a live-only recording. Skipped only when the + # source title is itself a live take (mirrors the strict path). + if q and not _LIVE_GROUP_RE.search(str(title or "")): + q += " AND -secondarytype:Live" + return q parts = [] if t: parts.append('recording:"%s"' % _lucene_escape_phrase(t)) diff --git a/tests/test_mb_match.py b/tests/test_mb_match.py index 9ae4c43..61a2421 100644 --- a/tests/test_mb_match.py +++ b/tests/test_mb_match.py @@ -195,14 +195,25 @@ def test_build_recording_query_loose_drops_field_phrases(): assert m.build_recording_query("Junko Ohashi", "Telephone Number") == \ 'recording:"telephone number" AND artist:"junko ohashi" AND -secondarytype:Live' # The loose form has no field scoping and no phrases, so MusicBrainz also - # searches artist ALIASES — rescues non-Latin-primary artists (大橋純子). + # searches artist ALIASES — rescues non-Latin-primary artists (大橋純子) — + # but keeps the same live exclusion (a studio chart must not fall back to a + # live-only recording). loose = m.build_recording_query("Junko Ohashi", "Telephone Number", loose=True) - assert loose == "(telephone number) AND (junko ohashi)" + assert loose == "(telephone number) AND (junko ohashi) AND -secondarytype:Live" assert "artist:" not in loose and '"' not in loose def test_build_recording_query_loose_missing_artist(): - assert m.build_recording_query("", "Fantasy", loose=True) == "(fantasy)" + assert m.build_recording_query("", "Fantasy", loose=True) == \ + "(fantasy) AND -secondarytype:Live" + + +def test_build_recording_query_loose_keeps_live_for_live_charts(): + # A live chart's loose fallback must NOT exclude live recordings (same gate + # as the strict path) — else its only correct recording is filtered out. + loose = m.build_recording_query("AC/DC", "Highway to Hell (Live at Donington)", loose=True) + assert "-secondarytype:Live" not in loose + assert loose == "(highway to hell) AND (ac dc)" # ── alias-aware artist scoring ────────────────────────────────────────────────