Cache hero slider artist metadata to eliminate Spotify API calls on every page load

This commit is contained in:
Broque Thomas 2026-03-13 13:13:31 -07:00
parent 5ee9390c05
commit ae6fb929bf
2 changed files with 95 additions and 54 deletions

View file

@ -107,6 +107,9 @@ class SimilarArtist:
similarity_rank: int # 1-10, where 1 is most similar
occurrence_count: int # How many watchlist artists share this similar artist
last_updated: datetime
image_url: Optional[str] = None # Cached artist image
genres: Optional[List[str]] = None # Cached genres
popularity: int = 0 # Cached popularity score
@dataclass
class DiscoveryTrack:
@ -1054,8 +1057,16 @@ class MusicDatabase:
cursor.execute("ALTER TABLE similar_artists ADD COLUMN last_featured TIMESTAMP")
logger.info("Added last_featured column to similar_artists table for hero cycling")
# Migration: Add cached metadata columns to avoid API calls on every page load
if 'image_url' not in columns:
cursor.execute("ALTER TABLE similar_artists ADD COLUMN image_url TEXT")
cursor.execute("ALTER TABLE similar_artists ADD COLUMN genres TEXT")
cursor.execute("ALTER TABLE similar_artists ADD COLUMN popularity INTEGER DEFAULT 0")
cursor.execute("ALTER TABLE similar_artists ADD COLUMN metadata_updated_at TIMESTAMP")
logger.info("Added image_url, genres, popularity, metadata_updated_at columns to similar_artists for hero caching")
except Exception as e:
logger.error(f"Error adding last_featured column to similar_artists: {e}")
logger.error(f"Error adding columns to similar_artists: {e}")
# Don't raise - this is a migration, database can still function
def _fix_watchlist_spotify_id_nullable(self, cursor):
@ -5309,6 +5320,24 @@ class MusicDatabase:
logger.error(f"Error updating similar artist iTunes ID: {e}")
return False
def update_similar_artist_metadata(self, similar_artist_id: int, image_url: str = None,
genres: list = None, popularity: int = None) -> bool:
"""Cache artist metadata (image, genres, popularity) to avoid repeated API calls"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
genres_json = json.dumps(genres) if genres else None
cursor.execute("""
UPDATE similar_artists
SET image_url = ?, genres = ?, popularity = ?, metadata_updated_at = CURRENT_TIMESTAMP
WHERE id = ?
""", (image_url, genres_json, popularity or 0, similar_artist_id))
conn.commit()
return cursor.rowcount > 0
except Exception as e:
logger.error(f"Error updating similar artist metadata: {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 (<days_threshold old).
@ -5401,7 +5430,10 @@ class MusicDatabase:
sa.similar_artist_name,
AVG(sa.similarity_rank) as similarity_rank,
SUM(sa.occurrence_count) as occurrence_count,
MAX(sa.last_updated) as last_updated
MAX(sa.last_updated) as last_updated,
MAX(sa.image_url) as image_url,
MAX(sa.genres) as genres,
MAX(sa.popularity) as popularity
FROM similar_artists sa
LEFT JOIN watchlist_artists wa ON (
(sa.similar_artist_spotify_id IS NOT NULL AND sa.similar_artist_spotify_id = wa.spotify_artist_id)
@ -5419,16 +5451,27 @@ class MusicDatabase:
""", (profile_id, profile_id, limit))
rows = cursor.fetchall()
return [SimilarArtist(
id=row['id'],
source_artist_id=row['source_artist_id'],
similar_artist_spotify_id=row['similar_artist_spotify_id'],
similar_artist_itunes_id=row['similar_artist_itunes_id'] if 'similar_artist_itunes_id' in row.keys() else None,
similar_artist_name=row['similar_artist_name'],
similarity_rank=int(row['similarity_rank']),
occurrence_count=row['occurrence_count'],
last_updated=datetime.fromisoformat(row['last_updated'])
) for row in rows]
results = []
for row in rows:
genres_raw = row['genres'] if 'genres' in row.keys() else None
try:
genres_list = json.loads(genres_raw) if genres_raw else None
except (json.JSONDecodeError, TypeError):
genres_list = None
results.append(SimilarArtist(
id=row['id'],
source_artist_id=row['source_artist_id'],
similar_artist_spotify_id=row['similar_artist_spotify_id'],
similar_artist_itunes_id=row['similar_artist_itunes_id'] if 'similar_artist_itunes_id' in row.keys() else None,
similar_artist_name=row['similar_artist_name'],
similarity_rank=int(row['similarity_rank']),
occurrence_count=row['occurrence_count'],
last_updated=datetime.fromisoformat(row['last_updated']),
image_url=row['image_url'] if 'image_url' in row.keys() else None,
genres=genres_list,
popularity=row['popularity'] if 'popularity' in row.keys() else 0,
))
return results
except Exception as e:
logger.error(f"Error getting top similar artists: {e}")

View file

@ -28994,25 +28994,9 @@ def get_discover_hero():
"is_watchlist": True
}
# Try to get artist image
try:
if active_source == 'itunes' and artist.itunes_artist_id:
itunes_artist = itunes_client.get_artist(artist.itunes_artist_id)
if itunes_artist:
# Use canonical name from iTunes API (normalized to 'name' field)
artist_data['artist_name'] = itunes_artist.get('name', artist.artist_name)
artist_data['image_url'] = itunes_artist.get('images', [{}])[0].get('url') if itunes_artist.get('images') else None
artist_data['genres'] = itunes_artist.get('genres', [])
elif active_source == 'spotify' and artist.spotify_artist_id:
if spotify_client and spotify_client.is_authenticated():
sp_artist = spotify_client.get_artist(artist.spotify_artist_id)
if sp_artist and sp_artist.get('images'):
# Use canonical name from Spotify API
artist_data['artist_name'] = sp_artist.get('name', artist.artist_name)
artist_data['image_url'] = sp_artist['images'][0]['url'] if sp_artist['images'] else None
artist_data['genres'] = sp_artist.get('genres', [])
except Exception as img_err:
print(f"Could not fetch watchlist artist image: {img_err}")
# Use cached image from watchlist — no API call needed
if hasattr(artist, 'image_url') and artist.image_url:
artist_data['image_url'] = artist.image_url
hero_artists.append(artist_data)
@ -29062,7 +29046,7 @@ def get_discover_hero():
# Take top 10 (already ordered by least-recently-featured, then quality)
similar_artists = valid_artists[:10]
# Convert to JSON format with data enrichment from appropriate source
# Convert to JSON format — use cached metadata, only fetch from API if missing
hero_artists = []
for artist in similar_artists:
# Use the ID for the active source, falling back to the other if needed
@ -29074,34 +29058,48 @@ def get_discover_hero():
artist_data = {
"spotify_artist_id": artist.similar_artist_spotify_id,
"itunes_artist_id": artist.similar_artist_itunes_id,
"artist_id": artist_id, # The ID for the current active source
"artist_id": artist_id,
"artist_name": artist.similar_artist_name,
"occurrence_count": artist.occurrence_count,
"similarity_rank": artist.similarity_rank,
"source": active_source
}
# Try to get artist image from the active source
try:
if active_source == 'spotify' and artist.similar_artist_spotify_id:
if spotify_client and spotify_client.is_authenticated():
sp_artist = spotify_client.get_artist(artist.similar_artist_spotify_id)
if sp_artist and sp_artist.get('images'):
# Use canonical name from Spotify API to ensure it matches the image
artist_data['artist_name'] = sp_artist.get('name', artist.similar_artist_name)
artist_data['image_url'] = sp_artist['images'][0]['url'] if sp_artist['images'] else None
artist_data['genres'] = sp_artist.get('genres', [])
artist_data['popularity'] = sp_artist.get('popularity', 0)
elif active_source == 'itunes' and artist.similar_artist_itunes_id:
itunes_artist = itunes_client.get_artist(artist.similar_artist_itunes_id)
if itunes_artist:
# iTunes client normalizes to Spotify format, so use 'name' not 'artistName'
artist_data['artist_name'] = itunes_artist.get('name', artist.similar_artist_name)
artist_data['image_url'] = itunes_artist.get('images', [{}])[0].get('url') if itunes_artist.get('images') else None
artist_data['genres'] = itunes_artist.get('genres', [])
artist_data['popularity'] = itunes_artist.get('popularity', 0)
except Exception as img_err:
print(f"Could not fetch artist image: {img_err}")
# Use cached metadata if available
if artist.image_url:
artist_data['image_url'] = artist.image_url
artist_data['genres'] = artist.genres or []
artist_data['popularity'] = artist.popularity or 0
else:
# No cached metadata — fetch from API and cache for next time
try:
if active_source == 'spotify' and artist.similar_artist_spotify_id:
if spotify_client and spotify_client.is_authenticated():
sp_artist = spotify_client.get_artist(artist.similar_artist_spotify_id)
if sp_artist and sp_artist.get('images'):
artist_data['artist_name'] = sp_artist.get('name', artist.similar_artist_name)
artist_data['image_url'] = sp_artist['images'][0]['url'] if sp_artist['images'] else None
artist_data['genres'] = sp_artist.get('genres', [])
artist_data['popularity'] = sp_artist.get('popularity', 0)
# Cache it
database.update_similar_artist_metadata(
artist.id, artist_data.get('image_url'),
artist_data.get('genres'), artist_data.get('popularity')
)
elif active_source == 'itunes' and artist.similar_artist_itunes_id:
itunes_artist = itunes_client.get_artist(artist.similar_artist_itunes_id)
if itunes_artist:
artist_data['artist_name'] = itunes_artist.get('name', artist.similar_artist_name)
artist_data['image_url'] = itunes_artist.get('images', [{}])[0].get('url') if itunes_artist.get('images') else None
artist_data['genres'] = itunes_artist.get('genres', [])
artist_data['popularity'] = itunes_artist.get('popularity', 0)
# Cache it
database.update_similar_artist_metadata(
artist.id, artist_data.get('image_url'),
artist_data.get('genres'), artist_data.get('popularity')
)
except Exception as img_err:
print(f"Could not fetch artist image: {img_err}")
hero_artists.append(artist_data)