soulsync/tests/matching/test_divergent_version.py
Jeff Theroux 3c06bd03c0
fix(tidal): honour version field in matching and back off on rate limits
Three related fixes to Tidal track matching and downloading:

1. version-field handling — Tidal stores remix/live/edit qualifiers in a
   dedicated `track.version` attribute (e.g. name="Emerge",
   version="Junkie XL Remix"), not in the track name. The qualifier
   filter and the matcher only looked at name/album, so the exact
   recording was discarded. Fold `version` into both the qualifier
   haystack and the candidate title passed to MusicMatchingEngine.

2. divergent-version penalty — once versions are visible, OTHER cuts of
   the same base become candidates ("(Shazam Remix)" vs "(southstar
   Remix)"). Neither title is a prefix of the other, so the prefix-based
   version check missed them and the raw ratio stayed high off the
   shared base. Apply a heavy penalty when both titles carry different
   version descriptors so the wrong cut can't outscore the threshold.

3. rate-limit backoff — the trackManifests endpoint is aggressively
   rate-limited; a bare request failed 429 instantly, burned the quality
   tier, re-queued the track and hammered again (a self-amplifying
   storm). Honour Retry-After / exponential backoff with a bounded retry
   count and shutdown-aware sleep.

Adds unit + end-to-end tests for all three.
2026-06-08 13:50:19 -04:00

100 lines
3.7 KiB
Python

"""Divergent-version matching: two DIFFERENT versions of the same base
title must NOT match.
Context: Tidal stores remix/live/edit qualifiers in a dedicated `version`
field which `_tidal_to_track_result` now folds into the candidate title so
the matcher can see it. That fix made the *correct* version win — but it
also makes OTHER versions of the same song visible ("We Are The People
(Shazam Remix)" vs "(southstar Remix)"). Neither title is a prefix of the
other, so the original prefix-based version check missed them and the raw
ratio stayed high (~0.8) off the shared base. Without discrimination, when
the requested version is absent a different remix could outscore the
threshold and the wrong cut would be downloaded.
These pin: different descriptors reject, the correct one still wins, and
the existing original-vs-version / remaster behaviour is preserved.
"""
from __future__ import annotations
import pytest
from core.matching_engine import MusicMatchingEngine
me = MusicMatchingEngine()
# ── similarity_score: divergent version tails (already-normalised input) ──
def test_different_remix_descriptors_rejected():
assert me.similarity_score(
'we are the people shazam remix',
'we are the people southstar remix',
) == 0.30
def test_different_live_performances_rejected():
assert me.similarity_score(
'all night live at pukkelpop',
'all night live at wembley',
) == 0.30
def test_different_version_types_same_base_rejected():
# Same song, different version TYPE (remix vs live) — different cut.
assert me.similarity_score('song title remix', 'song title live') == 0.30
def test_same_base_non_version_tails_not_penalised():
# "one" / "two" are not version words — leave the raw ratio alone.
assert me.similarity_score('song one', 'song two') != 0.30
# ── regression: original-vs-version + remaster behaviour preserved ──
def test_original_vs_remix_still_rejected():
assert me.similarity_score('we are the people', 'we are the people remix') == 0.30
def test_remaster_still_light_penalty():
assert me.similarity_score('song title', 'song title remastered') == 0.75
def test_identical_titles_still_perfect():
assert me.similarity_score('emerge junkie xl remix', 'emerge junkie xl remix') == 1.0
# ── end-to-end via score_track_match (raw titles, real weighting) ──
_ARTIST = 'Empire Of The Sun'
def test_wrong_remix_scored_below_threshold():
# Requested Shazam Remix, only a different remix available → must land
# well under the 0.55/0.60 acceptance gate so it is never downloaded.
conf, _ = me.score_track_match(
'We Are The People (Shazam Remix)', [_ARTIST], 344_000,
'We Are The People (southstar Remix)', [_ARTIST], 236_000,
)
assert conf < 0.55, f'wrong remix scored {conf:.2f}, should be < 0.55'
def test_correct_remix_still_wins():
conf, _ = me.score_track_match(
'We Are The People (Shazam Remix)', [_ARTIST], 344_000,
'We Are The People (Shazam Remix)', [_ARTIST], 344_000,
)
assert conf >= 0.90, f'correct remix scored {conf:.2f}, should be >= 0.90'
@pytest.mark.parametrize('wanted,candidate', [
('We Are The People (Shazam Remix)', 'We Are The People (ARTBAT Remix)'),
('All Night (Live @ Pukkelpop)', 'All Night (Umek Remix)'),
('Emerge (Junkie XL Remix)', 'Emerge (DFA Version)'),
])
def test_wrong_version_below_correct(wanted, candidate):
artist = 'X'
wrong, _ = me.score_track_match(wanted, [artist], 0, candidate, [artist], 0)
right, _ = me.score_track_match(wanted, [artist], 0, wanted, [artist], 0)
assert right > wrong
assert wrong < 0.60, f'{candidate!r} scored {wrong:.2f} vs {wanted!r}'