From 7da7f3b112c9b120653bb8b40adc0f5f3aa5ab7a Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:35:54 -0700 Subject: [PATCH] Cache similar artist metadata at scan time to eliminate redundant Spotify API calls --- core/watchlist_scanner.py | 7 ++- database/music_database.py | 44 ++++++++++++-- web_server.py | 117 +++++++++++++++++++++++++++---------- webui/static/script.js | 5 +- 4 files changed, 134 insertions(+), 39 deletions(-) diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index 4a1e119d..f189a0ad 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -1483,14 +1483,17 @@ class WatchlistScanner: stored_count = 0 for rank, similar_artist in enumerate(similar_artists, 1): try: - # similar_artist has 'name', 'spotify_id', and 'itunes_id' keys + # similar_artist has 'name', 'spotify_id', 'itunes_id', 'image_url', 'genres', 'popularity' success = self.database.add_or_update_similar_artist( source_artist_id=source_artist_id, similar_artist_name=similar_artist['name'], similar_artist_spotify_id=similar_artist.get('spotify_id'), similar_artist_itunes_id=similar_artist.get('itunes_id'), similarity_rank=rank, - profile_id=profile_id + profile_id=profile_id, + image_url=similar_artist.get('image_url'), + genres=similar_artist.get('genres'), + popularity=similar_artist.get('popularity', 0) ) if success: diff --git a/database/music_database.py b/database/music_database.py index 6d23b7c2..31b752df 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -5216,25 +5216,36 @@ class MusicDatabase: similar_artist_spotify_id: Optional[str] = None, similar_artist_itunes_id: Optional[str] = None, similarity_rank: int = 1, - profile_id: int = 1) -> bool: + profile_id: int = 1, + image_url: Optional[str] = None, + genres: Optional[list] = None, + popularity: int = 0) -> bool: """Add or update a similar artist recommendation (supports both Spotify and iTunes IDs)""" try: with self._get_connection() as conn: cursor = conn.cursor() + genres_json = json.dumps(genres) if genres else None # Use artist name as the unique key (allows storing both IDs for same artist) cursor.execute(""" INSERT INTO similar_artists - (source_artist_id, similar_artist_spotify_id, similar_artist_itunes_id, similar_artist_name, similarity_rank, occurrence_count, last_updated, profile_id) - VALUES (?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, ?) + (source_artist_id, similar_artist_spotify_id, similar_artist_itunes_id, similar_artist_name, + similarity_rank, occurrence_count, last_updated, profile_id, + image_url, genres, popularity, metadata_updated_at) + VALUES (?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, ?, ?, ?, ?, CURRENT_TIMESTAMP) ON CONFLICT(profile_id, source_artist_id, similar_artist_name) DO UPDATE SET similar_artist_spotify_id = COALESCE(excluded.similar_artist_spotify_id, similar_artist_spotify_id), similar_artist_itunes_id = COALESCE(excluded.similar_artist_itunes_id, similar_artist_itunes_id), similarity_rank = excluded.similarity_rank, occurrence_count = occurrence_count + 1, - last_updated = CURRENT_TIMESTAMP - """, (source_artist_id, similar_artist_spotify_id, similar_artist_itunes_id, similar_artist_name, similarity_rank, profile_id)) + last_updated = CURRENT_TIMESTAMP, + image_url = COALESCE(excluded.image_url, image_url), + genres = COALESCE(excluded.genres, genres), + popularity = CASE WHEN excluded.popularity > 0 THEN excluded.popularity ELSE popularity END, + metadata_updated_at = CASE WHEN excluded.image_url IS NOT NULL THEN CURRENT_TIMESTAMP ELSE metadata_updated_at END + """, (source_artist_id, similar_artist_spotify_id, similar_artist_itunes_id, similar_artist_name, + similarity_rank, profile_id, image_url, genres_json, popularity)) conn.commit() return True @@ -5338,6 +5349,29 @@ class MusicDatabase: logger.error(f"Error updating similar artist metadata: {e}") return False + def update_similar_artist_metadata_by_external_id(self, external_id: str, source: str = 'spotify', + image_url: str = None, genres: list = None, + popularity: int = None) -> bool: + """Cache artist metadata by Spotify or iTunes ID (updates all rows for that artist)""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + genres_json = json.dumps(genres) if genres else None + if source == 'spotify': + where_clause = "similar_artist_spotify_id = ?" + else: + where_clause = "similar_artist_itunes_id = ?" + cursor.execute(f""" + UPDATE similar_artists + SET image_url = ?, genres = ?, popularity = ?, metadata_updated_at = CURRENT_TIMESTAMP + WHERE {where_clause} + """, (image_url, genres_json, popularity or 0, external_id)) + conn.commit() + return cursor.rowcount > 0 + except Exception as e: + logger.error(f"Error updating similar artist metadata by external ID: {e}") + return False + def has_fresh_similar_artists(self, source_artist_id: str, days_threshold: int = 30, require_itunes: bool = True, require_spotify: bool = False, profile_id: int = 1) -> bool: """ Check if we have cached similar artists that are still fresh ( a[idKey]).filter(Boolean); + const allIds = data.artists + .filter(a => !a.image_url) // Only enrich artists without cached images + .map(a => a[idKey]).filter(Boolean); for (let i = 0; i < allIds.length; i += 50) { const batchIds = allIds.slice(i, i + 50);