Improve enrichment matching to pick best result instead of first (#210)

Artist/album/track matching previously took the first Spotify search
result above the 0.80 similarity threshold. If Spotify returned a
near-match before the exact match (e.g. "Brother's Keeper" before
"Brothers Keepers"), the wrong entity would be selected.

Now scores all candidates and picks the highest, so an exact match
(1.0) always wins over a near-match (0.94). No change to threshold
or batch matching logic — strictly better or equal results.
This commit is contained in:
Broque Thomas 2026-03-29 16:13:17 -07:00
parent 213736e168
commit 20452859c5

View file

@ -431,18 +431,25 @@ class SpotifyWorker:
logger.debug(f"No Spotify results for artist '{artist_name}'")
return
# Find best fuzzy match
# Find best fuzzy match — score all candidates, pick highest above threshold
best_obj = None
best_score = 0
for artist_obj in results:
if self._name_matches(artist_name, artist_obj.name):
if not self._is_spotify_id(artist_obj.id):
logger.warning(f"Rejecting non-Spotify ID '{artist_obj.id}' for artist '{artist_name}' (iTunes fallback leak)")
self._mark_status('artist', artist_id, 'error')
self.stats['errors'] += 1
return
self._update_artist(artist_id, artist_obj)
self.stats['matched'] += 1
logger.info(f"Matched artist '{artist_name}' -> Spotify ID: {artist_obj.id}")
score = self._name_similarity(artist_name, artist_obj.name)
if score >= self.name_similarity_threshold and score > best_score:
best_obj = artist_obj
best_score = score
if best_obj:
if not self._is_spotify_id(best_obj.id):
logger.warning(f"Rejecting non-Spotify ID '{best_obj.id}' for artist '{artist_name}' (iTunes fallback leak)")
self._mark_status('artist', artist_id, 'error')
self.stats['errors'] += 1
return
self._update_artist(artist_id, best_obj)
self.stats['matched'] += 1
logger.info(f"Matched artist '{artist_name}' -> Spotify ID: {best_obj.id} (score: {best_score:.2f})")
return
self._mark_status('artist', artist_id, 'not_found')
self.stats['not_found'] += 1
@ -595,17 +602,24 @@ class SpotifyWorker:
logger.debug(f"No Spotify results for album '{album_name}'")
return
best_obj = None
best_score = 0
for album_obj in results:
if self._name_matches(album_name, album_obj.name):
if not self._is_spotify_id(album_obj.id):
logger.warning(f"Rejecting non-Spotify ID '{album_obj.id}' for album '{album_name}'")
self._mark_status('album', album_id, 'error')
self.stats['errors'] += 1
return
self._update_album(album_id, album_obj)
self.stats['matched'] += 1
logger.info(f"Matched album '{album_name}' -> Spotify ID: {album_obj.id}")
score = self._name_similarity(album_name, album_obj.name)
if score >= self.name_similarity_threshold and score > best_score:
best_obj = album_obj
best_score = score
if best_obj:
if not self._is_spotify_id(best_obj.id):
logger.warning(f"Rejecting non-Spotify ID '{best_obj.id}' for album '{album_name}'")
self._mark_status('album', album_id, 'error')
self.stats['errors'] += 1
return
self._update_album(album_id, best_obj)
self.stats['matched'] += 1
logger.info(f"Matched album '{album_name}' -> Spotify ID: {best_obj.id} (score: {best_score:.2f})")
return
self._mark_status('album', album_id, 'not_found')
self.stats['not_found'] += 1
@ -625,17 +639,24 @@ class SpotifyWorker:
logger.debug(f"No Spotify results for track '{track_name}'")
return
best_obj = None
best_score = 0
for track_obj in results:
if self._name_matches(track_name, track_obj.name):
if not self._is_spotify_id(track_obj.id):
logger.warning(f"Rejecting non-Spotify ID '{track_obj.id}' for track '{track_name}'")
self._mark_status('track', track_id, 'error')
self.stats['errors'] += 1
return
self._update_track_from_search(track_id, track_obj)
self.stats['matched'] += 1
logger.info(f"Matched track '{track_name}' -> Spotify ID: {track_obj.id}")
score = self._name_similarity(track_name, track_obj.name)
if score >= self.name_similarity_threshold and score > best_score:
best_obj = track_obj
best_score = score
if best_obj:
if not self._is_spotify_id(best_obj.id):
logger.warning(f"Rejecting non-Spotify ID '{best_obj.id}' for track '{track_name}'")
self._mark_status('track', track_id, 'error')
self.stats['errors'] += 1
return
self._update_track_from_search(track_id, best_obj)
self.stats['matched'] += 1
logger.info(f"Matched track '{track_name}' -> Spotify ID: {best_obj.id} (score: {best_score:.2f})")
return
self._mark_status('track', track_id, 'not_found')
self.stats['not_found'] += 1
@ -997,9 +1018,12 @@ class SpotifyWorker:
name = re.sub(r'\s+', ' ', name).strip()
return name
def _name_matches(self, query_name: str, result_name: str) -> bool:
def _name_similarity(self, query_name: str, result_name: str) -> float:
norm_query = self._normalize_name(query_name)
norm_result = self._normalize_name(result_name)
similarity = SequenceMatcher(None, norm_query, norm_result).ratio()
return SequenceMatcher(None, norm_query, norm_result).ratio()
def _name_matches(self, query_name: str, result_name: str) -> bool:
similarity = self._name_similarity(query_name, result_name)
logger.debug(f"Name similarity: '{query_name}' vs '{result_name}' = {similarity:.2f}")
return similarity >= self.name_similarity_threshold