diff --git a/lib/mb_match.py b/lib/mb_match.py index faa4454..248853a 100644 --- a/lib/mb_match.py +++ b/lib/mb_match.py @@ -224,6 +224,11 @@ def _lucene_escape_phrase(s: str) -> str: return s.replace("\\", "\\\\").replace('"', '\\"') +# A parenthetical/bracketed "(Live …)" marker — the live signal denoise() strips +# from the title. Mirrors _NOISE_GROUP_RE but for the `live` term only. +_LIVE_GROUP_RE = re.compile(r"[(\[][^)\]]*\blive\b[^)\]]*[)\]]", re.IGNORECASE) + + def build_recording_query(artist, title) -> str: """Lucene query for /ws/2/recording. Built from the DENOISED fields — the noise we strip (author credits, "(Live)", "(v2)") would otherwise @@ -242,7 +247,13 @@ def build_recording_query(artist, title) -> str: # REUSE the studio recording, so filtering them would drop the very recording # we want (verified against MusicBrainz — `-secondarytype:Compilation` cut the # AC/DC studio "Highway to Hell" recording entirely). - if q: + # + # EXCEPT when the source chart is itself a live take: denoise() strips the + # "(Live at …)" qualifier from the query, so filtering Live would leave the + # genuinely-live chart with NO correct recording. Only a parenthetical marker + # counts — a bare title word ("Live and Let Die") is a real word, not a live + # tag — mirroring what denoise removes. + if q and not _LIVE_GROUP_RE.search(str(title or "")): q += " AND -secondarytype:Live" return q diff --git a/tests/test_mb_match.py b/tests/test_mb_match.py index ed793a1..cb560ea 100644 --- a/tests/test_mb_match.py +++ b/tests/test_mb_match.py @@ -154,6 +154,18 @@ def test_build_recording_query_denoises_and_quotes(): assert q == 'recording:"thunderstruck" AND artist:"acdc" AND -secondarytype:Live' +def test_build_recording_query_keeps_live_for_live_charts(): + """A chart that IS a live take must NOT get the live filter, or its only + correct recording is excluded. A bare title word ("Live and Let Die") is a + real word, not a marker, so it still filters.""" + live = m.build_recording_query("AC/DC", "Highway to Hell (Live at Donington)") + assert "-secondarytype:Live" not in live + assert 'recording:"highway to hell"' in live + # A real word "live" in the title is not a live marker → still filtered. + bare = m.build_recording_query("Wings", "Live and Let Die") + assert "-secondarytype:Live" in bare + + def test_build_recording_query_escapes_and_handles_missing_artist(): q = m.build_recording_query("", 'Say "Hello"') # Quotes are punct-stripped by denoise, so nothing to escape here — but