From 1a0fd8b95ee2a21dc6ddac22494c76bb6a1faea7 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:24:30 -0700 Subject: [PATCH] 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. --- core/deezer_worker.py | 34 ++++++++++++++++++++++++++++++++++ core/itunes_worker.py | 26 ++++++++++++++++++++++++++ core/lastfm_worker.py | 34 ++++++++++++++++++++++++++++++++++ core/musicbrainz_worker.py | 25 +++++++++++++++++++++++++ core/qobuz_worker.py | 34 ++++++++++++++++++++++++++++++++++ core/spotify_worker.py | 26 ++++++++++++++++++++++++++ core/tidal_worker.py | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 213 insertions(+) diff --git a/core/deezer_worker.py b/core/deezer_worker.py index 7d085c7c..9e216f06 100644 --- a/core/deezer_worker.py +++ b/core/deezer_worker.py @@ -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', '') diff --git a/core/itunes_worker.py b/core/itunes_worker.py index 52f101ff..723ce5b6 100644 --- a/core/itunes_worker.py +++ b/core/itunes_worker.py @@ -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') diff --git a/core/lastfm_worker.py b/core/lastfm_worker.py index 48a61908..4005e593 100644 --- a/core/lastfm_worker.py +++ b/core/lastfm_worker.py @@ -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', '') diff --git a/core/musicbrainz_worker.py b/core/musicbrainz_worker.py index 579644e6..09a0e4a5 100644 --- a/core/musicbrainz_worker.py +++ b/core/musicbrainz_worker.py @@ -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'): diff --git a/core/qobuz_worker.py b/core/qobuz_worker.py index 2bea8bbd..bf54c4e8 100644 --- a/core/qobuz_worker.py +++ b/core/qobuz_worker.py @@ -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: diff --git a/core/spotify_worker.py b/core/spotify_worker.py index 4444ff47..aef650e8 100644 --- a/core/spotify_worker.py +++ b/core/spotify_worker.py @@ -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') diff --git a/core/tidal_worker.py b/core/tidal_worker.py index 1f47959d..c02e398a 100644 --- a/core/tidal_worker.py +++ b/core/tidal_worker.py @@ -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', '')