Fix enrichment overwriting manual match status (#221)

When a user manually matched an artist to a service ID then triggered
enrichment, the worker re-searched by name, failed to find a match,
and overwrote the status back to not_found — despite the ID being
valid. Now both Genius and AudioDB workers check for existing service
IDs before searching by name. If an ID exists (from manual match),
the worker uses it for a direct API lookup to enrich metadata while
preserving the matched status. Added AudioDB lookup-by-ID client
methods for artist, album, and track.
pull/253/head
Broque Thomas 3 months ago
parent edaa55ae82
commit f2e24a36df

@ -152,3 +152,60 @@ class AudioDBClient:
except Exception as e:
logger.error(f"Error searching for track '{artist_name} - {track_title}': {e}")
return None
@rate_limited
def lookup_artist_by_id(self, artist_id: str) -> Optional[Dict[str, Any]]:
"""Lookup an artist directly by AudioDB ID."""
try:
response = self.session.get(
f"{self.BASE_URL}/artist.php",
params={'i': artist_id},
timeout=10
)
response.raise_for_status()
data = response.json()
artists = data.get('artists')
if artists and len(artists) > 0:
return artists[0]
return None
except Exception as e:
logger.error(f"Error looking up artist by ID {artist_id}: {e}")
return None
@rate_limited
def lookup_album_by_id(self, album_id: str) -> Optional[Dict[str, Any]]:
"""Lookup an album directly by AudioDB ID."""
try:
response = self.session.get(
f"{self.BASE_URL}/album.php",
params={'m': album_id},
timeout=10
)
response.raise_for_status()
data = response.json()
albums = data.get('album')
if albums and len(albums) > 0:
return albums[0]
return None
except Exception as e:
logger.error(f"Error looking up album by ID {album_id}: {e}")
return None
@rate_limited
def lookup_track_by_id(self, track_id: str) -> Optional[Dict[str, Any]]:
"""Lookup a track directly by AudioDB ID."""
try:
response = self.session.get(
f"{self.BASE_URL}/track.php",
params={'m': track_id},
timeout=10
)
response.raise_for_status()
data = response.json()
tracks = data.get('track')
if tracks and len(tracks) > 0:
return tracks[0]
return None
except Exception as e:
logger.error(f"Error looking up track by ID {track_id}: {e}")
return None

@ -315,8 +315,29 @@ class AudioDBWorker:
logger.debug(f"Name similarity: '{query_name}' vs '{result_name}' = {similarity:.2f}")
return similarity >= self.name_similarity_threshold
def _get_existing_id(self, entity_type: str, entity_id: int) -> Optional[str]:
"""Check if an entity already has an audiodb_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 audiodb_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)"""
"""Process a single item (artist, album, or track).
If the entity already has an audiodb_id (e.g. from manual match),
uses it for direct lookup instead of searching by name."""
try:
item_type = item['type']
item_id = item['id']
@ -324,6 +345,35 @@ class AudioDBWorker:
logger.debug(f"Processing {item_type} #{item_id}: {item_name}")
# Check for existing ID (manual match) — use direct lookup instead of name search
existing_id = self._get_existing_id(item_type, item_id)
if existing_id:
lookup_methods = {
'artist': self.client.lookup_artist_by_id,
'album': self.client.lookup_album_by_id,
'track': self.client.lookup_track_by_id,
}
update_methods = {
'artist': lambda r: self._update_artist(item_id, r),
'album': lambda r: (self._verify_artist_id(item, r), self._update_album(item_id, r)),
'track': lambda r: (self._verify_artist_id(item, r), self._update_track(item_id, r)),
}
lookup = lookup_methods.get(item_type)
update = update_methods.get(item_type)
if lookup and update:
try:
result = lookup(existing_id)
if result:
update(result)
self.stats['matched'] += 1
logger.info(f"Enriched {item_type} '{item_name}' from existing AudioDB ID: {existing_id}")
return
except Exception as e:
logger.warning(f"Direct lookup failed for existing AudioDB ID {existing_id}: {e}")
# Direct lookup failed — don't overwrite manual match
logger.debug(f"Preserving manual match for {item_type} '{item_name}' (AudioDB ID: {existing_id})")
return
if item_type == 'artist':
result = self.client.search_artist(item_name)
if result:

@ -276,8 +276,46 @@ class GeniusWorker:
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 genius_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 genius_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 Genius, get full artist details"""
"""Process an artist: search Genius, get full artist details.
If the artist already has a genius_id (e.g. from manual match),
uses it for direct lookup instead of searching by name."""
# Check for existing ID (manual match) — use direct lookup instead of name search
existing_id = self._get_existing_id('artist', artist_id)
if existing_id:
try:
full_artist = self.client.get_artist(int(existing_id))
if full_artist:
self._update_artist(artist_id, full_artist, full_artist)
self.stats['matched'] += 1
logger.info(f"Enriched artist '{artist_name}' from existing Genius ID: {existing_id}")
return
except Exception as e:
logger.warning(f"Direct lookup failed for existing Genius ID {existing_id}: {e}")
# Direct lookup failed — don't overwrite manual match, just return
logger.debug(f"Preserving manual match for artist '{artist_name}' (Genius ID: {existing_id})")
return
result = self.client.search_artist(artist_name)
if result:
result_name = result.get('name', '')
@ -310,7 +348,32 @@ class GeniusWorker:
logger.debug(f"No match for artist '{artist_name}'")
def _process_track(self, track_id: int, track_name: str, artist_name: str):
"""Process a track: search Genius, get full song details + lyrics"""
"""Process a track: search Genius, get full song details + lyrics.
If the track already has a genius_id (e.g. from manual match),
uses it for direct lookup instead of searching by name."""
# Check for existing ID (manual match) — use direct lookup instead of name search
existing_id = self._get_existing_id('track', track_id)
if existing_id:
try:
full_song = self.client.get_song(int(existing_id))
if full_song:
lyrics = None
song_url = full_song.get('url')
if song_url:
try:
lyrics = self.client.get_lyrics(song_url)
except Exception:
pass
self._update_track(track_id, full_song, full_song, lyrics)
self.stats['matched'] += 1
logger.info(f"Enriched track '{track_name}' from existing Genius ID: {existing_id}")
return
except Exception as e:
logger.warning(f"Direct lookup failed for existing Genius ID {existing_id}: {e}")
logger.debug(f"Preserving manual match for track '{track_name}' (Genius ID: {existing_id})")
return
result = self.client.search_song(artist_name, track_name)
if result:
result_title = result.get('title', '')

@ -19136,6 +19136,17 @@ def get_version_info():
"title": "What's New in SoulSync",
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"sections": [
{
"title": "🔧 Fix Enrichment Overwriting Manual Matches (#221)",
"description": "Enriching an entity that was manually matched no longer reverts the status to not_found",
"features": [
"• Genius and AudioDB workers now check for existing service IDs before searching by name",
"• Manual matches are used for direct API lookup instead of re-searching by name",
"• If the direct lookup succeeds, metadata is enriched and match status is preserved",
"• If the direct lookup fails, the manual match status is preserved (not overwritten to not_found)",
"• Added AudioDB lookup-by-ID methods for artist, album, and track"
]
},
{
"title": "🔧 Fix Spotify OAuth ERR_EMPTY_RESPONSE in Docker (#220)",
"description": "OAuth callback server hardened for Docker/SSH tunnel setups",

@ -3403,6 +3403,7 @@ function closeHelperSearch() {
const WHATS_NEW = {
'2.1': [
// Newest features first
{ title: 'Fix Enrichment Breaking Manual Matches', desc: 'Enriching a manually matched artist no longer reverts status to not_found — uses stored ID for direct lookup' },
{ title: 'Fix Spotify OAuth Empty Response', desc: 'OAuth callback server now always sends a response in Docker — added health check and proper logging' },
{ title: 'All Services on Dashboard', desc: 'Dashboard shows all enrichment services as live-status chips — click unconfigured ones to jump to Settings. Spotify card no longer shows "Apple Music"', page: 'dashboard' },
{ title: 'Qobuz on Connections Tab', desc: 'Qobuz credentials now on Settings → Connections for metadata enrichment without needing it as download source' },

Loading…
Cancel
Save