Cache hero slider artist metadata to eliminate Spotify API calls on every page load
This commit is contained in:
parent
5ee9390c05
commit
ae6fb929bf
2 changed files with 95 additions and 54 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue