Issue #607 (AfonsoG6) -- two AcoustID problems: 1. Live recordings false-quarantining as "Version mismatch: expected '... (Live at Venue)' (live) but file is '...' (original)" because MusicBrainz often stores the recording entity with a bare title -- the venue / live annotation lives on the release entity, not the recording. The audio fingerprint correctly identifies the live recording, but the title-text comparison flagged it as wrong. New pure helper `core/matching/version_mismatch.py:is_acceptable_version_mismatch` accepts the mismatch only when: - One-sided AND involves 'live': exactly one side is 'live' and the other is bare 'original'. Two-sided mismatches stay strict. - Fingerprint score >= 0.85 (stricter than the existing 0.80 minimum -- escape valve only fires when AcoustID is more confident than its own threshold). - Bare title similarity >= 0.70. - Artist similarity >= 0.60. Other version markers (instrumental, remix, acoustic, demo, etc) stay strict -- those have distinct fingerprints AND MB always annotates them in the recording title. The existing test_acoustid_version_mismatch.py suite passes unchanged. 2. Audio-mismatch failure message reported "identified as '' by '' (artist=100%)" when AcoustID returned multiple recordings -- prior code mixed `recordings[0]`'s strings (which can be empty) with `best_rec`'s scores. Now uses `matched_title` / `matched_artist` consistently in both the high-confidence-skip path and the final fail message. Issue #608 (AfonsoG6) -- quarantine modal: 3. Approve / Delete buttons silently no-op'd when the filename contained an apostrophe -- the unescaped quote broke the inline JS in the onclick handler. Now wraps the id via `escapeHtml(JSON.stringify(id))`, which round-trips quotes / backslashes / unicode / newlines safely through the HTML attribute to JS string boundary. 4. Bonus UX: quarantine entry expanded view now shows source uploader (username) and original soulseek filename when the sidecar carries that context -- helps trace which uploader the bad file came from. Backend exposes `source_username` + `source_filename` fields from `sidecar.context.original_search_result`. Degrades to '' on legacy thin sidecars. Tests: - 23 new boundary tests in tests/matching/test_version_mismatch.py pin every shape: equal versions trivial, one-sided live both directions, threshold floors (each just below default -> reject), two-sided strict, non-live one-sided strict (covers exact test_instrumental_returned_for_vocal_request_fails scenario), custom-threshold overrides. - 4 existing test_acoustid_version_mismatch.py tests pass unchanged. - 507 AcoustID / matching / imports tests pass.
231 lines
7.9 KiB
Python
231 lines
7.9 KiB
Python
"""Boundary tests for ``core.matching.version_mismatch``.
|
|
|
|
Pin every shape the version-mismatch escape valve has to handle so
|
|
future drift fails here instead of at runtime against a real download:
|
|
one-sided bare cases (the live-recording MB-metadata-gap that issue
|
|
#607 reported), two-sided real mismatches (live vs remix — keep
|
|
strict), high vs low fingerprint score gates, title/artist threshold
|
|
gates, defensive paths.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from core.matching.version_mismatch import is_acceptable_version_mismatch
|
|
|
|
|
|
class TestEqualVersions:
|
|
def test_same_version_trivially_accepted(self):
|
|
# Equal version strings — no mismatch to decide. True.
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'live',
|
|
fingerprint_score=0.0,
|
|
title_similarity=0.0,
|
|
artist_similarity=0.0,
|
|
) is True
|
|
|
|
def test_both_original_accepted(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'original', 'original',
|
|
fingerprint_score=0.0,
|
|
title_similarity=0.0,
|
|
artist_similarity=0.0,
|
|
) is True
|
|
|
|
|
|
class TestOneSidedBareMismatch:
|
|
"""Issue #607 example 2: expected has annotation, AcoustID's MB
|
|
record is bare. Accept when fingerprint + bare titles + artist
|
|
all line up."""
|
|
|
|
def test_live_vs_original_high_confidence_accepted(self):
|
|
# Reporter's exact case: "Clarity (Live at ...)" vs "Clarity"
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.95,
|
|
title_similarity=0.95,
|
|
artist_similarity=1.0,
|
|
) is True
|
|
|
|
def test_original_vs_live_high_confidence_accepted(self):
|
|
# Same case in the other direction.
|
|
assert is_acceptable_version_mismatch(
|
|
'original', 'live',
|
|
fingerprint_score=0.95,
|
|
title_similarity=0.95,
|
|
artist_similarity=1.0,
|
|
) is True
|
|
|
|
def test_live_at_thresholds_accepted(self):
|
|
# Exactly at the thresholds for the live-aware case.
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.85,
|
|
title_similarity=0.70,
|
|
artist_similarity=0.60,
|
|
) is True
|
|
|
|
|
|
class TestNonLiveOneSidedMismatchStaysStrict:
|
|
"""Other version markers (instrumental / remix / acoustic / etc)
|
|
have distinct fingerprints AND MB always annotates them in the
|
|
recording title. When AcoustID returns one of these for a bare
|
|
expected (or vice versa), the file genuinely IS that version —
|
|
the user asked for the wrong cut. Reject regardless of how high
|
|
the supporting scores are.
|
|
|
|
This narrowness is what keeps the existing
|
|
test_acoustid_version_mismatch suite passing — instrumental
|
|
vs vocal etc. stays a real mismatch."""
|
|
|
|
def test_remix_vs_original_rejected_at_high_confidence(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'remix', 'original',
|
|
fingerprint_score=0.99,
|
|
title_similarity=0.99,
|
|
artist_similarity=0.99,
|
|
) is False
|
|
|
|
def test_instrumental_vs_original_rejected_at_high_confidence(self):
|
|
# The exact case test_acoustid_version_mismatch.py:
|
|
# test_instrumental_returned_for_vocal_request_fails pins —
|
|
# vocal asked, instrumental returned, must FAIL.
|
|
assert is_acceptable_version_mismatch(
|
|
'instrumental', 'original',
|
|
fingerprint_score=0.99,
|
|
title_similarity=0.99,
|
|
artist_similarity=0.99,
|
|
) is False
|
|
|
|
def test_original_vs_instrumental_rejected_at_high_confidence(self):
|
|
# Reverse direction: caller asked for vocal, file is
|
|
# instrumental.
|
|
assert is_acceptable_version_mismatch(
|
|
'original', 'instrumental',
|
|
fingerprint_score=0.99,
|
|
title_similarity=0.99,
|
|
artist_similarity=0.99,
|
|
) is False
|
|
|
|
def test_acoustic_vs_original_rejected_at_high_confidence(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'acoustic', 'original',
|
|
fingerprint_score=0.99,
|
|
title_similarity=0.99,
|
|
artist_similarity=0.99,
|
|
) is False
|
|
|
|
def test_demo_vs_original_rejected(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'demo', 'original',
|
|
fingerprint_score=0.99,
|
|
title_similarity=0.99,
|
|
artist_similarity=0.99,
|
|
) is False
|
|
|
|
|
|
class TestTwoSidedMismatchStaysStrict:
|
|
"""Both sides have version markers but they disagree. Real
|
|
different-recording mismatch — must reject regardless of how
|
|
high the other scores are."""
|
|
|
|
def test_live_vs_remix_rejected_even_at_max(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'remix',
|
|
fingerprint_score=1.0,
|
|
title_similarity=1.0,
|
|
artist_similarity=1.0,
|
|
) is False
|
|
|
|
def test_acoustic_vs_instrumental_rejected(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'acoustic', 'instrumental',
|
|
fingerprint_score=0.99,
|
|
title_similarity=0.99,
|
|
artist_similarity=0.99,
|
|
) is False
|
|
|
|
def test_live_vs_acoustic_rejected(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'acoustic',
|
|
fingerprint_score=0.95,
|
|
title_similarity=0.90,
|
|
artist_similarity=1.0,
|
|
) is False
|
|
|
|
|
|
class TestThresholdGates:
|
|
"""One-sided + bare but one of the supporting signals is too weak.
|
|
Reject — fall through to FAIL."""
|
|
|
|
def test_low_fingerprint_score_rejected(self):
|
|
# Fingerprint score below threshold. Don't trust it enough.
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.50,
|
|
title_similarity=0.95,
|
|
artist_similarity=1.0,
|
|
) is False
|
|
|
|
def test_low_title_similarity_rejected(self):
|
|
# Bare titles disagree → different songs, not just MB metadata gap.
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.95,
|
|
title_similarity=0.30,
|
|
artist_similarity=1.0,
|
|
) is False
|
|
|
|
def test_low_artist_similarity_rejected(self):
|
|
# Wrong artist — definitely not the same recording.
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.95,
|
|
title_similarity=0.95,
|
|
artist_similarity=0.20,
|
|
) is False
|
|
|
|
def test_just_below_score_threshold_rejected(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.849, # default threshold 0.85
|
|
title_similarity=0.95,
|
|
artist_similarity=1.0,
|
|
) is False
|
|
|
|
def test_just_below_title_threshold_rejected(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.95,
|
|
title_similarity=0.699, # default threshold 0.70
|
|
artist_similarity=1.0,
|
|
) is False
|
|
|
|
def test_just_below_artist_threshold_rejected(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.95,
|
|
title_similarity=0.95,
|
|
artist_similarity=0.599, # default threshold 0.60
|
|
) is False
|
|
|
|
|
|
class TestCustomThresholds:
|
|
def test_custom_score_threshold_accepts_when_loosened(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.70,
|
|
title_similarity=0.95,
|
|
artist_similarity=1.0,
|
|
score_threshold=0.65,
|
|
) is True
|
|
|
|
def test_custom_score_threshold_rejects_when_tightened(self):
|
|
assert is_acceptable_version_mismatch(
|
|
'live', 'original',
|
|
fingerprint_score=0.90,
|
|
title_similarity=0.95,
|
|
artist_similarity=1.0,
|
|
score_threshold=0.95,
|
|
) is False
|