Apply manual match protection to all enrichment workers (#226)

The original #221 fix only covered Genius and AudioDB. All other
workers (Spotify, iTunes, Last.fm, MusicBrainz, Deezer, Tidal,
Qobuz) had the same bug: enrichment overwrites manual match status
to not_found when name search fails. Each worker now checks for an
existing service ID before searching by name and returns early if
one exists, preserving the manual match.
This commit is contained in:
Broque Thomas 2026-03-31 08:24:30 -07:00
parent 6c4d8d9348
commit 1a0fd8b95e
7 changed files with 213 additions and 0 deletions

View file

@ -337,8 +337,32 @@ class DeezerWorker:
except Exception as e2:
logger.error(f"Error updating item status: {e2}")
def _get_existing_id(self, entity_type: str, entity_id: int) -> Optional[str]:
"""Check if an entity already has a deezer_id (e.g. from manual match)."""
table_map = {'artist': 'artists', 'album': 'albums', 'track': 'tracks'}
table = table_map.get(entity_type)
if not table:
return None
conn = None
try:
conn = self.db._get_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT deezer_id FROM {table} WHERE id = ?", (entity_id,))
row = cursor.fetchone()
return row[0] if row and row[0] else None
except Exception:
return None
finally:
if conn:
conn.close()
def _process_artist(self, artist_id: int, artist_name: str):
"""Process an artist: search Deezer, verify, store metadata"""
existing_id = self._get_existing_id('artist', artist_id)
if existing_id:
logger.debug(f"Preserving existing Deezer ID for artist '{artist_name}': {existing_id}")
return
result = self.client.search_artist(artist_name)
if result:
result_name = result.get('name', '')
@ -357,6 +381,11 @@ class DeezerWorker:
def _process_album(self, album_id: int, album_name: str, artist_name: str, item: Dict[str, Any]):
"""Process an album: search Deezer, verify, fetch full details, store metadata"""
existing_id = self._get_existing_id('album', album_id)
if existing_id:
logger.debug(f"Preserving existing Deezer ID for album '{album_name}': {existing_id}")
return
result = self.client.search_album(artist_name, album_name)
if result:
result_name = result.get('title', '')
@ -397,6 +426,11 @@ class DeezerWorker:
def _process_track(self, track_id: int, track_name: str, artist_name: str, item: Dict[str, Any]):
"""Process a track: search Deezer, verify, fetch full details for BPM, store metadata"""
existing_id = self._get_existing_id('track', track_id)
if existing_id:
logger.debug(f"Preserving existing Deezer ID for track '{track_name}': {existing_id}")
return
result = self.client.search_track(artist_name, track_name)
if result:
result_name = result.get('title', '')

View file

@ -338,10 +338,36 @@ class iTunesWorker:
# ── Artist processing ──────────────────────────────────────────────
def _get_existing_id(self, entity_type: str, entity_id: int) -> Optional[str]:
"""Check if an entity already has an itunes_artist_id/itunes_album_id/itunes_track_id."""
col_map = {'artist': 'itunes_artist_id', 'album': 'itunes_album_id', 'track': 'itunes_track_id'}
table_map = {'artist': 'artists', 'album': 'albums', 'track': 'tracks'}
col = col_map.get(entity_type)
table = table_map.get(entity_type)
if not col or not table:
return None
conn = None
try:
conn = self.db._get_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT {col} FROM {table} WHERE id = ?", (entity_id,))
row = cursor.fetchone()
return row[0] if row and row[0] else None
except Exception:
return None
finally:
if conn:
conn.close()
def _process_artist(self, item: Dict[str, Any]):
artist_id = item['id']
artist_name = item['name']
existing_id = self._get_existing_id('artist', artist_id)
if existing_id:
logger.debug(f"Preserving existing iTunes ID for artist '{artist_name}': {existing_id}")
return
results = self.client.search_artists(artist_name, limit=5)
if not results:
self._mark_status('artist', artist_id, 'not_found')

View file

@ -302,8 +302,32 @@ class LastFMWorker:
except Exception as e2:
logger.error(f"Error updating item status: {e2}")
def _get_existing_id(self, entity_type: str, entity_id: int) -> Optional[str]:
"""Check if an entity already has a lastfm_id (e.g. from manual match)."""
table_map = {'artist': 'artists', 'album': 'albums', 'track': 'tracks'}
table = table_map.get(entity_type)
if not table:
return None
conn = None
try:
conn = self.db._get_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT lastfm_id FROM {table} WHERE id = ?", (entity_id,))
row = cursor.fetchone()
return row[0] if row and row[0] else None
except Exception:
return None
finally:
if conn:
conn.close()
def _process_artist(self, artist_id: int, artist_name: str):
"""Process an artist: get full info from Last.fm"""
existing_id = self._get_existing_id('artist', artist_id)
if existing_id:
logger.debug(f"Preserving existing Last.fm ID for artist '{artist_name}': {existing_id}")
return
# Use get_artist_info for detailed data (includes stats, bio, tags, similar)
result = self.client.get_artist_info(artist_name)
if result:
@ -323,6 +347,11 @@ class LastFMWorker:
def _process_album(self, album_id: int, album_name: str, artist_name: str):
"""Process an album: get full info from Last.fm"""
existing_id = self._get_existing_id('album', album_id)
if existing_id:
logger.debug(f"Preserving existing Last.fm ID for album '{album_name}': {existing_id}")
return
result = self.client.get_album_info(artist_name, album_name)
if result:
result_name = result.get('name', '')
@ -341,6 +370,11 @@ class LastFMWorker:
def _process_track(self, track_id: int, track_name: str, artist_name: str):
"""Process a track: get full info from Last.fm"""
existing_id = self._get_existing_id('track', track_id)
if existing_id:
logger.debug(f"Preserving existing Last.fm ID for track '{track_name}': {existing_id}")
return
result = self.client.get_track_info(artist_name, track_name)
if result:
result_name = result.get('name', '')

View file

@ -249,6 +249,25 @@ class MusicBrainzWorker:
if conn:
conn.close()
def _get_existing_id(self, entity_type: str, entity_id: int) -> Optional[str]:
"""Check if an entity already has a musicbrainz_id (e.g. from manual match)."""
table_map = {'artist': 'artists', 'album': 'albums', 'track': 'tracks'}
table = table_map.get(entity_type)
if not table:
return None
conn = None
try:
conn = self.db._get_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT musicbrainz_id FROM {table} WHERE id = ?", (entity_id,))
row = cursor.fetchone()
return row[0] if row and row[0] else None
except Exception:
return None
finally:
if conn:
conn.close()
def _process_item(self, item: Dict[str, Any]):
"""Process a single item (artist, album, or track)"""
try:
@ -258,6 +277,12 @@ class MusicBrainzWorker:
logger.debug(f"Processing {item_type} #{item_id}: {item_name}")
# Preserve existing manual matches
existing_id = self._get_existing_id(item_type, item_id)
if existing_id:
logger.debug(f"Preserving existing MusicBrainz ID for {item_type} '{item_name}': {existing_id}")
return
if item_type == 'artist':
result = self.mb_service.match_artist(item_name)
if result and result.get('mbid'):

View file

@ -360,8 +360,32 @@ class QobuzWorker:
except Exception as e2:
logger.error(f"Error updating item status: {e2}")
def _get_existing_id(self, entity_type: str, entity_id: int) -> Optional[str]:
"""Check if an entity already has a qobuz_id (e.g. from manual match)."""
table_map = {'artist': 'artists', 'album': 'albums', 'track': 'tracks'}
table = table_map.get(entity_type)
if not table:
return None
conn = None
try:
conn = self.db._get_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT qobuz_id FROM {table} WHERE id = ?", (entity_id,))
row = cursor.fetchone()
return row[0] if row and row[0] else None
except Exception:
return None
finally:
if conn:
conn.close()
def _process_artist(self, artist_id: int, artist_name: str):
"""Process an artist: search Qobuz, verify, store metadata"""
existing_id = self._get_existing_id('artist', artist_id)
if existing_id:
logger.debug(f"Preserving existing Qobuz ID for artist '{artist_name}': {existing_id}")
return
result = self.client.search_artist(artist_name)
if result:
@ -398,6 +422,11 @@ class QobuzWorker:
def _process_album(self, album_id: int, album_name: str, artist_name: str, item: Dict[str, Any]):
"""Process an album: search Qobuz, verify, fetch full details, store metadata"""
existing_id = self._get_existing_id('album', album_id)
if existing_id:
logger.debug(f"Preserving existing Qobuz ID for album '{album_name}': {existing_id}")
return
result = self.client.search_album(artist_name, album_name)
if result:
@ -448,6 +477,11 @@ class QobuzWorker:
def _process_track(self, track_id: int, track_name: str, artist_name: str, item: Dict[str, Any]):
"""Process a track: search Qobuz, verify, fetch full details, store metadata"""
existing_id = self._get_existing_id('track', track_id)
if existing_id:
logger.debug(f"Preserving existing Qobuz ID for track '{track_name}': {existing_id}")
return
result = self.client.search_track(artist_name, track_name)
if result:

View file

@ -423,10 +423,36 @@ class SpotifyWorker:
# ── Artist processing ──────────────────────────────────────────────
def _get_existing_id(self, entity_type: str, entity_id: int) -> Optional[str]:
"""Check if an entity already has a spotify_artist_id/spotify_album_id/spotify_track_id."""
col_map = {'artist': 'spotify_artist_id', 'album': 'spotify_album_id', 'track': 'spotify_track_id'}
table_map = {'artist': 'artists', 'album': 'albums', 'track': 'tracks'}
col = col_map.get(entity_type)
table = table_map.get(entity_type)
if not col or not table:
return None
conn = None
try:
conn = self.db._get_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT {col} FROM {table} WHERE id = ?", (entity_id,))
row = cursor.fetchone()
return row[0] if row and row[0] else None
except Exception:
return None
finally:
if conn:
conn.close()
def _process_artist(self, item: Dict[str, Any]):
artist_id = item['id']
artist_name = item['name']
existing_id = self._get_existing_id('artist', artist_id)
if existing_id:
logger.debug(f"Preserving existing Spotify ID for artist '{artist_name}': {existing_id}")
return
results = self.client.search_artists(artist_name, limit=5)
if not results:
self._mark_status('artist', artist_id, 'not_found')

View file

@ -373,8 +373,32 @@ class TidalWorker:
except Exception as e2:
logger.error(f"Error updating item status: {e2}")
def _get_existing_id(self, entity_type: str, entity_id: int) -> Optional[str]:
"""Check if an entity already has a tidal_id (e.g. from manual match)."""
table_map = {'artist': 'artists', 'album': 'albums', 'track': 'tracks'}
table = table_map.get(entity_type)
if not table:
return None
conn = None
try:
conn = self.db._get_connection()
cursor = conn.cursor()
cursor.execute(f"SELECT tidal_id FROM {table} WHERE id = ?", (entity_id,))
row = cursor.fetchone()
return row[0] if row and row[0] else None
except Exception:
return None
finally:
if conn:
conn.close()
def _process_artist(self, artist_id: int, artist_name: str):
"""Process an artist: search Tidal, verify, store metadata"""
existing_id = self._get_existing_id('artist', artist_id)
if existing_id:
logger.debug(f"Preserving existing Tidal ID for artist '{artist_name}': {existing_id}")
return
result = self.client.search_artist(artist_name)
if result:
result_name = result.get('name', '')
@ -407,6 +431,11 @@ class TidalWorker:
def _process_album(self, album_id: int, album_name: str, artist_name: str, item: Dict[str, Any]):
"""Process an album: search Tidal, verify, fetch full details, store metadata"""
existing_id = self._get_existing_id('album', album_id)
if existing_id:
logger.debug(f"Preserving existing Tidal ID for album '{album_name}': {existing_id}")
return
result = self.client.search_album(artist_name, album_name)
if result:
result_name = result.get('title', '')
@ -450,6 +479,11 @@ class TidalWorker:
def _process_track(self, track_id: int, track_name: str, artist_name: str, item: Dict[str, Any]):
"""Process a track: search Tidal, verify, fetch full details, store metadata"""
existing_id = self._get_existing_id('track', track_id)
if existing_id:
logger.debug(f"Preserving existing Tidal ID for track '{track_name}': {existing_id}")
return
result = self.client.search_track(artist_name, track_name)
if result:
result_name = result.get('title', '')