Add iTunes fallback and improve artist/album handling

Adds iTunes fallback to SpotifyClient for search and metadata when Spotify is not authenticated. Updates album type logic to distinguish EPs, singles, and albums more accurately. Refactors watchlist database methods to support both Spotify and iTunes artist IDs. Improves deduplication and normalization of album names from iTunes. Updates web server and frontend to use new album type logic and support both ID types. Adds artist bubble snapshot example data.
pull/126/head
Broque Thomas 4 months ago
parent b790c34657
commit f12478ee70

@ -141,13 +141,13 @@ class Album:
# Determine album type from collection type
track_count = album_data.get('trackCount', 0)
# iTunes doesn't clearly distinguish EPs, but we can infer:
# Singles typically have 1-3 tracks, EPs have 4-6, Albums have 7+
if track_count <= 3:
album_type = 'single'
elif track_count <= 6:
album_type = 'single' # iTunes calls EPs "albums" but we can mark shorter ones
album_type = 'ep' # 4-6 tracks = EP
else:
album_type = 'album'
@ -371,7 +371,7 @@ class iTunesClient:
if track_count <= 3:
album_type = 'single'
elif track_count <= 6:
album_type = 'single' # EP treated as single
album_type = 'ep' # 4-6 tracks = EP
else:
album_type = 'album'
@ -433,17 +433,42 @@ class iTunesClient:
# ==================== Artist Methods ====================
def _get_artist_image_from_albums(self, artist_id: str) -> Optional[str]:
"""
Get artist image by fetching their first album's artwork.
iTunes doesn't reliably return artist images, so we use album art as fallback.
"""
try:
# Lookup is not rate-limited, so this is fast
results = self._lookup(id=artist_id, entity='album', limit=1)
for item in results:
if item.get('wrapperType') == 'collection' and item.get('artworkUrl100'):
# Return high-res version
return item['artworkUrl100'].replace('100x100bb', '600x600bb')
except Exception as e:
logger.debug(f"Could not fetch album art for artist {artist_id}: {e}")
return None
@rate_limited
def search_artists(self, query: str, limit: int = 20) -> List[Artist]:
"""Search for artists using iTunes API"""
"""Search for artists using iTunes API - includes album art fallback for images"""
results = self._search(query, 'musicArtist', limit)
artists = []
for artist_data in results:
if artist_data.get('wrapperType') == 'artist':
artist = Artist.from_itunes_artist(artist_data)
# If no artist image, try to get their first album's artwork
if not artist.image_url:
album_art = self._get_artist_image_from_albums(str(artist_data.get('artistId', '')))
if album_art:
artist.image_url = album_art
artists.append(artist)
return artists
def get_artist(self, artist_id: str) -> Optional[Dict[str, Any]]:
@ -461,14 +486,22 @@ class iTunesClient:
for artist_data in results:
if artist_data.get('wrapperType') == 'artist':
# Build images array - iTunes artist search doesn't reliably return images
# but we include the structure for compatibility
# Use album art as fallback
images = []
if artist_data.get('artworkUrl100'):
artwork_base = artist_data['artworkUrl100']
artwork_url = artist_data.get('artworkUrl100')
# If no artist artwork, try to get from their first album
if not artwork_url:
album_art = self._get_artist_image_from_albums(str(artist_data.get('artistId', '')))
if album_art:
# Convert back to base URL format for building array
artwork_url = album_art.replace('600x600bb', '100x100bb')
if artwork_url:
images = [
{'url': artwork_base.replace('100x100bb', '600x600bb'), 'height': 600, 'width': 600},
{'url': artwork_base.replace('100x100bb', '300x300bb'), 'height': 300, 'width': 300},
{'url': artwork_base, 'height': 100, 'width': 100}
{'url': artwork_url.replace('100x100bb', '600x600bb'), 'height': 600, 'width': 600},
{'url': artwork_url.replace('100x100bb', '300x300bb'), 'height': 300, 'width': 300},
{'url': artwork_url, 'height': 100, 'width': 100}
]
# Get genre
@ -494,26 +527,49 @@ class iTunesClient:
def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]:
"""
Get albums by artist ID
Note: iTunes doesn't support filtering by album_type in the same way as Spotify,
so we fetch all albums and can filter client-side if needed.
"""
import re
results = self._lookup(id=artist_id, entity='album', limit=min(limit, 200))
albums = []
seen_albums = set() # Track normalized names to prevent duplicates
def normalize_album_name(name: str) -> str:
"""Normalize album name for deduplication (removes edition suffixes, etc.)"""
normalized = name.lower().strip()
# Remove common edition suffixes
normalized = re.sub(r'\s*[\(\[]\s*(deluxe|explicit|clean|remaster|expanded|anniversary|edition|version|bonus|special|standard).*?[\)\]]', '', normalized, flags=re.IGNORECASE)
# Remove trailing edition keywords without brackets
normalized = re.sub(r'\s*[-–—]\s*(deluxe|explicit|clean|remaster|expanded|anniversary|edition|version).*$', '', normalized, flags=re.IGNORECASE)
# Normalize whitespace
normalized = re.sub(r'\s+', ' ', normalized).strip()
return normalized
for album_data in results:
if album_data.get('wrapperType') == 'collection':
album = Album.from_itunes_album(album_data)
# Filter by album_type if specified
# Filter by album_type if specified (now includes 'ep')
if album_type != 'album,single':
requested_types = album_type.split(',')
requested_types = [t.strip() for t in album_type.split(',')]
# Also accept 'ep' when 'single' is requested (for backward compat)
if album.album_type not in requested_types:
continue
if not (album.album_type == 'ep' and 'single' in requested_types):
continue
# Deduplicate by normalized name
normalized_name = normalize_album_name(album.name)
if normalized_name in seen_albums:
logger.debug(f"Skipping duplicate album: {album.name} (normalized: {normalized_name})")
continue
seen_albums.add(normalized_name)
albums.append(album)
logger.info(f"Retrieved {len(albums)} albums for artist {artist_id}")
logger.info(f"Retrieved {len(albums)} unique albums for artist {artist_id} (filtered from {len(results)} results)")
return albums[:limit]
# ==================== Playlist Methods ====================

@ -169,8 +169,18 @@ class SpotifyClient:
def __init__(self):
self.sp: Optional[spotipy.Spotify] = None
self.user_id: Optional[str] = None
self._itunes_client = None # Lazy-loaded iTunes fallback
self._setup_client()
@property
def _itunes(self):
"""Lazy-load iTunes client for fallback when Spotify not authenticated"""
if self._itunes_client is None:
from core.itunes_client import iTunesClient
self._itunes_client = iTunesClient()
logger.info("iTunes fallback client initialized")
return self._itunes_client
def reload_config(self):
"""Reload configuration and re-initialize client"""
self._setup_client()
@ -201,10 +211,23 @@ class SpotifyClient:
self.sp = None
def is_authenticated(self) -> bool:
"""Check if Spotify client is authenticated and working"""
"""
Check if client can service metadata requests.
Returns True if Spotify is authenticated OR iTunes fallback is available.
For Spotify-specific auth check, use is_spotify_authenticated().
"""
# If Spotify is authenticated, we're good
if self.is_spotify_authenticated():
return True
# iTunes fallback is always available
return True
def is_spotify_authenticated(self) -> bool:
"""Check if Spotify client is specifically authenticated (not just iTunes fallback)"""
if self.sp is None:
return False
try:
# Make a simple API call to verify authentication
self.sp.current_user()
@ -228,7 +251,7 @@ class SpotifyClient:
@rate_limited
def get_user_playlists(self) -> List[Playlist]:
if not self.is_authenticated():
if not self.is_spotify_authenticated():
logger.error("Not authenticated with Spotify")
return []
@ -262,7 +285,7 @@ class SpotifyClient:
@rate_limited
def get_user_playlists_metadata_only(self) -> List[Playlist]:
"""Get playlists without fetching all track details for faster loading"""
if not self.is_authenticated():
if not self.is_spotify_authenticated():
logger.error("Not authenticated with Spotify")
return []
@ -312,7 +335,7 @@ class SpotifyClient:
@rate_limited
def get_saved_tracks_count(self) -> int:
"""Get the total count of user's saved/liked songs without fetching all tracks"""
if not self.is_authenticated():
if not self.is_spotify_authenticated():
logger.error("Not authenticated with Spotify")
return 0
@ -331,7 +354,7 @@ class SpotifyClient:
@rate_limited
def get_saved_tracks(self) -> List[Track]:
"""Fetch all user's saved/liked songs from Spotify"""
if not self.is_authenticated():
if not self.is_spotify_authenticated():
logger.error("Not authenticated with Spotify")
return []
@ -373,7 +396,7 @@ class SpotifyClient:
@rate_limited
def _get_playlist_tracks(self, playlist_id: str) -> List[Track]:
if not self.is_authenticated():
if not self.is_spotify_authenticated():
return []
tracks = []
@ -397,7 +420,7 @@ class SpotifyClient:
@rate_limited
def get_playlist_by_id(self, playlist_id: str) -> Optional[Playlist]:
if not self.is_authenticated():
if not self.is_spotify_authenticated():
return None
try:
@ -411,104 +434,113 @@ class SpotifyClient:
@rate_limited
def search_tracks(self, query: str, limit: int = 20) -> List[Track]:
if not self.is_authenticated():
return []
try:
results = self.sp.search(q=query, type='track', limit=limit)
tracks = []
for track_data in results['tracks']['items']:
track = Track.from_spotify_track(track_data)
tracks.append(track)
return tracks
except Exception as e:
logger.error(f"Error searching tracks: {e}")
return []
"""Search for tracks - falls back to iTunes if Spotify not authenticated"""
if self.is_spotify_authenticated():
try:
results = self.sp.search(q=query, type='track', limit=limit)
tracks = []
for track_data in results['tracks']['items']:
track = Track.from_spotify_track(track_data)
tracks.append(track)
return tracks
except Exception as e:
logger.error(f"Error searching tracks via Spotify: {e}")
# Fall through to iTunes fallback
# iTunes fallback
logger.debug(f"Using iTunes fallback for track search: {query}")
return self._itunes.search_tracks(query, limit)
@rate_limited
def search_artists(self, query: str, limit: int = 20) -> List[Artist]:
"""Search for artists using Spotify API"""
if not self.is_authenticated():
return []
try:
results = self.sp.search(q=query, type='artist', limit=limit)
artists = []
for artist_data in results['artists']['items']:
artist = Artist.from_spotify_artist(artist_data)
artists.append(artist)
return artists
except Exception as e:
logger.error(f"Error searching artists: {e}")
return []
"""Search for artists - falls back to iTunes if Spotify not authenticated"""
if self.is_spotify_authenticated():
try:
results = self.sp.search(q=query, type='artist', limit=limit)
artists = []
for artist_data in results['artists']['items']:
artist = Artist.from_spotify_artist(artist_data)
artists.append(artist)
return artists
except Exception as e:
logger.error(f"Error searching artists via Spotify: {e}")
# Fall through to iTunes fallback
# iTunes fallback
logger.debug(f"Using iTunes fallback for artist search: {query}")
return self._itunes.search_artists(query, limit)
@rate_limited
def search_albums(self, query: str, limit: int = 20) -> List[Album]:
"""Search for albums using Spotify API"""
if not self.is_authenticated():
return []
try:
results = self.sp.search(q=query, type='album', limit=limit)
albums = []
for album_data in results['albums']['items']:
album = Album.from_spotify_album(album_data)
albums.append(album)
return albums
except Exception as e:
logger.error(f"Error searching albums: {e}")
return []
"""Search for albums - falls back to iTunes if Spotify not authenticated"""
if self.is_spotify_authenticated():
try:
results = self.sp.search(q=query, type='album', limit=limit)
albums = []
for album_data in results['albums']['items']:
album = Album.from_spotify_album(album_data)
albums.append(album)
return albums
except Exception as e:
logger.error(f"Error searching albums via Spotify: {e}")
# Fall through to iTunes fallback
# iTunes fallback
logger.debug(f"Using iTunes fallback for album search: {query}")
return self._itunes.search_albums(query, limit)
@rate_limited
def get_track_details(self, track_id: str) -> Optional[Dict[str, Any]]:
"""Get detailed track information including album data and track number"""
if not self.is_authenticated():
return None
try:
track_data = self.sp.track(track_id)
# Enhance with additional useful metadata for our purposes
if track_data:
enhanced_data = {
'id': track_data['id'],
'name': track_data['name'],
'track_number': track_data['track_number'],
'disc_number': track_data['disc_number'],
'duration_ms': track_data['duration_ms'],
'explicit': track_data['explicit'],
'artists': [artist['name'] for artist in track_data['artists']],
'primary_artist': track_data['artists'][0]['name'] if track_data['artists'] else None,
'album': {
'id': track_data['album']['id'],
'name': track_data['album']['name'],
'total_tracks': track_data['album']['total_tracks'],
'release_date': track_data['album']['release_date'],
'album_type': track_data['album']['album_type'],
'artists': [artist['name'] for artist in track_data['album']['artists']]
},
'is_album_track': track_data['album']['total_tracks'] > 1,
'raw_data': track_data # Keep original for fallback
}
return enhanced_data
return track_data
except Exception as e:
logger.error(f"Error fetching track details: {e}")
return None
"""Get detailed track information - falls back to iTunes if Spotify not authenticated"""
if self.is_spotify_authenticated():
try:
track_data = self.sp.track(track_id)
# Enhance with additional useful metadata for our purposes
if track_data:
enhanced_data = {
'id': track_data['id'],
'name': track_data['name'],
'track_number': track_data['track_number'],
'disc_number': track_data['disc_number'],
'duration_ms': track_data['duration_ms'],
'explicit': track_data['explicit'],
'artists': [artist['name'] for artist in track_data['artists']],
'primary_artist': track_data['artists'][0]['name'] if track_data['artists'] else None,
'album': {
'id': track_data['album']['id'],
'name': track_data['album']['name'],
'total_tracks': track_data['album']['total_tracks'],
'release_date': track_data['album']['release_date'],
'album_type': track_data['album']['album_type'],
'artists': [artist['name'] for artist in track_data['album']['artists']]
},
'is_album_track': track_data['album']['total_tracks'] > 1,
'raw_data': track_data # Keep original for fallback
}
return enhanced_data
return track_data
except Exception as e:
logger.error(f"Error fetching track details via Spotify: {e}")
# Fall through to iTunes fallback
# iTunes fallback
logger.debug(f"Using iTunes fallback for track details: {track_id}")
return self._itunes.get_track_details(track_id)
@rate_limited
def get_track_features(self, track_id: str) -> Optional[Dict[str, Any]]:
if not self.is_authenticated():
if not self.is_spotify_authenticated():
return None
try:
@ -521,83 +553,89 @@ class SpotifyClient:
@rate_limited
def get_album(self, album_id: str) -> Optional[Dict[str, Any]]:
"""Get album information including tracks"""
if not self.is_authenticated():
return None
try:
album_data = self.sp.album(album_id)
return album_data
except Exception as e:
logger.error(f"Error fetching album: {e}")
return None
"""Get album information - falls back to iTunes if Spotify not authenticated"""
if self.is_spotify_authenticated():
try:
album_data = self.sp.album(album_id)
return album_data
except Exception as e:
logger.error(f"Error fetching album via Spotify: {e}")
# Fall through to iTunes fallback
# iTunes fallback
logger.debug(f"Using iTunes fallback for album: {album_id}")
return self._itunes.get_album(album_id)
@rate_limited
def get_album_tracks(self, album_id: str) -> Optional[Dict[str, Any]]:
"""Get album tracks with pagination to fetch all tracks"""
if not self.is_authenticated():
return None
"""Get album tracks - falls back to iTunes if Spotify not authenticated"""
if self.is_spotify_authenticated():
try:
# Get first page of tracks
first_page = self.sp.album_tracks(album_id)
if not first_page or 'items' not in first_page:
return None
try:
# Get first page of tracks
first_page = self.sp.album_tracks(album_id)
if not first_page or 'items' not in first_page:
return None
# Collect all tracks starting with first page
all_tracks = first_page['items'][:]
# Fetch remaining pages if they exist
next_page = first_page
while next_page.get('next'):
next_page = self.sp.next(next_page)
if next_page and 'items' in next_page:
all_tracks.extend(next_page['items'])
# Log success
logger.info(f"Retrieved {len(all_tracks)} tracks for album {album_id}")
# Return structure with all tracks
result = first_page.copy()
result['items'] = all_tracks
result['next'] = None # No more pages
result['limit'] = len(all_tracks) # Update to reflect all tracks fetched
# Collect all tracks starting with first page
all_tracks = first_page['items'][:]
return result
# Fetch remaining pages if they exist
next_page = first_page
while next_page.get('next'):
next_page = self.sp.next(next_page)
if next_page and 'items' in next_page:
all_tracks.extend(next_page['items'])
except Exception as e:
logger.error(f"Error fetching album tracks: {e}")
return None
# Log success
logger.info(f"Retrieved {len(all_tracks)} tracks for album {album_id}")
# Return structure with all tracks
result = first_page.copy()
result['items'] = all_tracks
result['next'] = None # No more pages
result['limit'] = len(all_tracks) # Update to reflect all tracks fetched
return result
except Exception as e:
logger.error(f"Error fetching album tracks via Spotify: {e}")
# Fall through to iTunes fallback
# iTunes fallback
logger.debug(f"Using iTunes fallback for album tracks: {album_id}")
return self._itunes.get_album_tracks(album_id)
@rate_limited
def get_artist_albums(self, artist_id: str, album_type: str = 'album,single', limit: int = 50) -> List[Album]:
"""Get albums by artist ID"""
if not self.is_authenticated():
return []
try:
albums = []
results = self.sp.artist_albums(artist_id, album_type=album_type, limit=limit)
while results:
for album_data in results['items']:
album = Album.from_spotify_album(album_data)
albums.append(album)
# Get next batch if available
results = self.sp.next(results) if results['next'] else None
logger.info(f"Retrieved {len(albums)} albums for artist {artist_id}")
return albums
except Exception as e:
logger.error(f"Error fetching artist albums: {e}")
return []
"""Get albums by artist ID - falls back to iTunes if Spotify not authenticated"""
if self.is_spotify_authenticated():
try:
albums = []
results = self.sp.artist_albums(artist_id, album_type=album_type, limit=limit)
while results:
for album_data in results['items']:
album = Album.from_spotify_album(album_data)
albums.append(album)
# Get next batch if available
results = self.sp.next(results) if results['next'] else None
logger.info(f"Retrieved {len(albums)} albums for artist {artist_id}")
return albums
except Exception as e:
logger.error(f"Error fetching artist albums via Spotify: {e}")
# Fall through to iTunes fallback
# iTunes fallback
logger.debug(f"Using iTunes fallback for artist albums: {artist_id}")
return self._itunes.get_artist_albums(artist_id, album_type, limit)
@rate_limited
def get_user_info(self) -> Optional[Dict[str, Any]]:
if not self.is_authenticated():
if not self.is_spotify_authenticated():
return None
try:
@ -609,19 +647,21 @@ class SpotifyClient:
@rate_limited
def get_artist(self, artist_id: str) -> Optional[Dict[str, Any]]:
"""
Get full artist details from Spotify API.
Get full artist details - falls back to iTunes if Spotify not authenticated.
Args:
artist_id: Spotify artist ID
artist_id: Artist ID (Spotify or iTunes depending on authentication)
Returns:
Dictionary with artist data including images, genres, popularity
"""
if not self.is_authenticated():
return None
if self.is_spotify_authenticated():
try:
return self.sp.artist(artist_id)
except Exception as e:
logger.error(f"Error fetching artist via Spotify: {e}")
# Fall through to iTunes fallback
try:
return self.sp.artist(artist_id)
except Exception as e:
logger.error(f"Error fetching artist {artist_id}: {e}")
return None
# iTunes fallback
logger.debug(f"Using iTunes fallback for artist: {artist_id}")
return self._itunes.get_artist(artist_id)

@ -2700,64 +2700,89 @@ class MusicDatabase:
return 0
# Watchlist operations
def add_artist_to_watchlist(self, spotify_artist_id: str, artist_name: str) -> bool:
"""Add an artist to the watchlist for monitoring new releases"""
def add_artist_to_watchlist(self, artist_id: str, artist_name: str) -> bool:
"""Add an artist to the watchlist for monitoring new releases.
Automatically detects if artist_id is a Spotify ID (alphanumeric) or iTunes ID (numeric).
"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT OR REPLACE INTO watchlist_artists
(spotify_artist_id, artist_name, date_added, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (spotify_artist_id, artist_name))
# Detect ID type: iTunes IDs are purely numeric, Spotify IDs are alphanumeric
is_itunes_id = artist_id.isdigit()
if is_itunes_id:
cursor.execute("""
INSERT OR REPLACE INTO watchlist_artists
(itunes_artist_id, artist_name, date_added, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (artist_id, artist_name))
logger.info(f"Added artist '{artist_name}' to watchlist (iTunes ID: {artist_id})")
else:
cursor.execute("""
INSERT OR REPLACE INTO watchlist_artists
(spotify_artist_id, artist_name, date_added, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""", (artist_id, artist_name))
logger.info(f"Added artist '{artist_name}' to watchlist (Spotify ID: {artist_id})")
conn.commit()
logger.info(f"Added artist '{artist_name}' to watchlist (Spotify ID: {spotify_artist_id})")
return True
except Exception as e:
logger.error(f"Error adding artist '{artist_name}' to watchlist: {e}")
return False
def remove_artist_from_watchlist(self, spotify_artist_id: str) -> bool:
"""Remove an artist from the watchlist"""
def remove_artist_from_watchlist(self, artist_id: str) -> bool:
"""Remove an artist from the watchlist (checks both Spotify and iTunes IDs)"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
# Get artist name for logging
cursor.execute("SELECT artist_name FROM watchlist_artists WHERE spotify_artist_id = ?", (spotify_artist_id,))
# Get artist name for logging (check both ID columns)
cursor.execute("""
SELECT artist_name FROM watchlist_artists
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
""", (artist_id, artist_id))
result = cursor.fetchone()
artist_name = result['artist_name'] if result else "Unknown"
cursor.execute("DELETE FROM watchlist_artists WHERE spotify_artist_id = ?", (spotify_artist_id,))
cursor.execute("""
DELETE FROM watchlist_artists
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
""", (artist_id, artist_id))
if cursor.rowcount > 0:
conn.commit()
logger.info(f"Removed artist '{artist_name}' from watchlist (Spotify ID: {spotify_artist_id})")
logger.info(f"Removed artist '{artist_name}' from watchlist (ID: {artist_id})")
return True
else:
logger.warning(f"Artist with Spotify ID {spotify_artist_id} not found in watchlist")
logger.warning(f"Artist with ID {artist_id} not found in watchlist")
return False
except Exception as e:
logger.error(f"Error removing artist from watchlist (Spotify ID: {spotify_artist_id}): {e}")
logger.error(f"Error removing artist from watchlist (ID: {artist_id}): {e}")
return False
def is_artist_in_watchlist(self, spotify_artist_id: str) -> bool:
"""Check if an artist is currently in the watchlist"""
def is_artist_in_watchlist(self, artist_id: str) -> bool:
"""Check if an artist is currently in the watchlist (checks both Spotify and iTunes IDs)"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT 1 FROM watchlist_artists WHERE spotify_artist_id = ? LIMIT 1", (spotify_artist_id,))
# Check both spotify_artist_id and itunes_artist_id columns
cursor.execute("""
SELECT 1 FROM watchlist_artists
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
LIMIT 1
""", (artist_id, artist_id))
result = cursor.fetchone()
return result is not None
except Exception as e:
logger.error(f"Error checking if artist is in watchlist (Spotify ID: {spotify_artist_id}): {e}")
logger.error(f"Error checking if artist is in watchlist (ID: {artist_id}): {e}")
return False
def get_watchlist_artists(self) -> List[WatchlistArtist]:
@ -2839,8 +2864,8 @@ class MusicDatabase:
logger.error(f"Error getting watchlist count: {e}")
return 0
def update_watchlist_artist_image(self, spotify_artist_id: str, image_url: str) -> bool:
"""Update the image URL for a watchlist artist"""
def update_watchlist_artist_image(self, artist_id: str, image_url: str) -> bool:
"""Update the image URL for a watchlist artist (checks both Spotify and iTunes IDs)"""
try:
with self._get_connection() as conn:
cursor = conn.cursor()
@ -2856,8 +2881,8 @@ class MusicDatabase:
cursor.execute("""
UPDATE watchlist_artists
SET image_url = ?, updated_at = CURRENT_TIMESTAMP
WHERE spotify_artist_id = ?
""", (image_url, spotify_artist_id))
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
""", (image_url, artist_id, artist_id))
conn.commit()
return cursor.rowcount > 0

@ -22684,8 +22684,11 @@ def get_spotify_artist_discography(artist_name):
if album_type == 'single' or track_count <= 3:
singles.append(release_data)
elif album_type == 'compilation' or (track_count <= 8 and track_count > 3):
elif album_type == 'ep' or (track_count >= 4 and track_count <= 6):
eps.append(release_data)
elif album_type == 'compilation':
# Compilations go with albums
albums.append(release_data)
else:
albums.append(release_data)

@ -25411,7 +25411,7 @@ function createReleaseCard(release) {
name: release.title,
image_url: release.image_url,
release_date: release.year ? `${release.year}-01-01` : '',
album_type: release.type || 'album',
album_type: release.album_type || release.type || 'album',
total_tracks: (release.track_completion && typeof release.track_completion === 'object')
? release.track_completion.total_tracks : 1
};
@ -25440,8 +25440,8 @@ function createReleaseCard(release) {
throw new Error('No tracks found for this release');
}
// Determine album type based on release data
const albumType = release.type === 'single' ? 'singles' : 'albums';
// Use the actual album type from release data
const albumType = release.album_type || release.type || 'album';
// Open the Add to Wishlist modal
// Note: openAddToWishlistModal has its own loading overlay

Loading…
Cancel
Save