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:
parent
213736e168
commit
20452859c5
1 changed files with 54 additions and 30 deletions
|
|
@ -431,18 +431,25 @@ class SpotifyWorker:
|
||||||
logger.debug(f"No Spotify results for artist '{artist_name}'")
|
logger.debug(f"No Spotify results for artist '{artist_name}'")
|
||||||
return
|
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:
|
for artist_obj in results:
|
||||||
if self._name_matches(artist_name, artist_obj.name):
|
score = self._name_similarity(artist_name, artist_obj.name)
|
||||||
if not self._is_spotify_id(artist_obj.id):
|
if score >= self.name_similarity_threshold and score > best_score:
|
||||||
logger.warning(f"Rejecting non-Spotify ID '{artist_obj.id}' for artist '{artist_name}' (iTunes fallback leak)")
|
best_obj = artist_obj
|
||||||
self._mark_status('artist', artist_id, 'error')
|
best_score = score
|
||||||
self.stats['errors'] += 1
|
|
||||||
return
|
if best_obj:
|
||||||
self._update_artist(artist_id, artist_obj)
|
if not self._is_spotify_id(best_obj.id):
|
||||||
self.stats['matched'] += 1
|
logger.warning(f"Rejecting non-Spotify ID '{best_obj.id}' for artist '{artist_name}' (iTunes fallback leak)")
|
||||||
logger.info(f"Matched artist '{artist_name}' -> Spotify ID: {artist_obj.id}")
|
self._mark_status('artist', artist_id, 'error')
|
||||||
|
self.stats['errors'] += 1
|
||||||
return
|
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._mark_status('artist', artist_id, 'not_found')
|
||||||
self.stats['not_found'] += 1
|
self.stats['not_found'] += 1
|
||||||
|
|
@ -595,17 +602,24 @@ class SpotifyWorker:
|
||||||
logger.debug(f"No Spotify results for album '{album_name}'")
|
logger.debug(f"No Spotify results for album '{album_name}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
best_obj = None
|
||||||
|
best_score = 0
|
||||||
for album_obj in results:
|
for album_obj in results:
|
||||||
if self._name_matches(album_name, album_obj.name):
|
score = self._name_similarity(album_name, album_obj.name)
|
||||||
if not self._is_spotify_id(album_obj.id):
|
if score >= self.name_similarity_threshold and score > best_score:
|
||||||
logger.warning(f"Rejecting non-Spotify ID '{album_obj.id}' for album '{album_name}'")
|
best_obj = album_obj
|
||||||
self._mark_status('album', album_id, 'error')
|
best_score = score
|
||||||
self.stats['errors'] += 1
|
|
||||||
return
|
if best_obj:
|
||||||
self._update_album(album_id, album_obj)
|
if not self._is_spotify_id(best_obj.id):
|
||||||
self.stats['matched'] += 1
|
logger.warning(f"Rejecting non-Spotify ID '{best_obj.id}' for album '{album_name}'")
|
||||||
logger.info(f"Matched album '{album_name}' -> Spotify ID: {album_obj.id}")
|
self._mark_status('album', album_id, 'error')
|
||||||
|
self.stats['errors'] += 1
|
||||||
return
|
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._mark_status('album', album_id, 'not_found')
|
||||||
self.stats['not_found'] += 1
|
self.stats['not_found'] += 1
|
||||||
|
|
@ -625,17 +639,24 @@ class SpotifyWorker:
|
||||||
logger.debug(f"No Spotify results for track '{track_name}'")
|
logger.debug(f"No Spotify results for track '{track_name}'")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
best_obj = None
|
||||||
|
best_score = 0
|
||||||
for track_obj in results:
|
for track_obj in results:
|
||||||
if self._name_matches(track_name, track_obj.name):
|
score = self._name_similarity(track_name, track_obj.name)
|
||||||
if not self._is_spotify_id(track_obj.id):
|
if score >= self.name_similarity_threshold and score > best_score:
|
||||||
logger.warning(f"Rejecting non-Spotify ID '{track_obj.id}' for track '{track_name}'")
|
best_obj = track_obj
|
||||||
self._mark_status('track', track_id, 'error')
|
best_score = score
|
||||||
self.stats['errors'] += 1
|
|
||||||
return
|
if best_obj:
|
||||||
self._update_track_from_search(track_id, track_obj)
|
if not self._is_spotify_id(best_obj.id):
|
||||||
self.stats['matched'] += 1
|
logger.warning(f"Rejecting non-Spotify ID '{best_obj.id}' for track '{track_name}'")
|
||||||
logger.info(f"Matched track '{track_name}' -> Spotify ID: {track_obj.id}")
|
self._mark_status('track', track_id, 'error')
|
||||||
|
self.stats['errors'] += 1
|
||||||
return
|
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._mark_status('track', track_id, 'not_found')
|
||||||
self.stats['not_found'] += 1
|
self.stats['not_found'] += 1
|
||||||
|
|
@ -997,9 +1018,12 @@ class SpotifyWorker:
|
||||||
name = re.sub(r'\s+', ' ', name).strip()
|
name = re.sub(r'\s+', ' ', name).strip()
|
||||||
return name
|
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_query = self._normalize_name(query_name)
|
||||||
norm_result = self._normalize_name(result_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}")
|
logger.debug(f"Name similarity: '{query_name}' vs '{result_name}' = {similarity:.2f}")
|
||||||
return similarity >= self.name_similarity_threshold
|
return similarity >= self.name_similarity_threshold
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue