rebuild discovery pool flow to allow multiprocessing of itunes and spotfiy. each getting their own pool.
This commit is contained in:
parent
fecd371b5e
commit
1560726bbc
5 changed files with 856 additions and 455 deletions
|
|
@ -100,6 +100,39 @@ class PersonalizedPlaylistsService:
|
|||
self.database = database
|
||||
self.spotify_client = spotify_client
|
||||
|
||||
def _get_active_source(self) -> str:
|
||||
"""
|
||||
Determine which music source is active for discovery.
|
||||
Returns 'spotify' if Spotify is authenticated, 'itunes' otherwise.
|
||||
"""
|
||||
if self.spotify_client and hasattr(self.spotify_client, 'is_spotify_authenticated'):
|
||||
if self.spotify_client.is_spotify_authenticated():
|
||||
return 'spotify'
|
||||
return 'itunes'
|
||||
|
||||
def _build_track_dict(self, row, source: str) -> Dict:
|
||||
"""Build a standardized track dictionary from a database row."""
|
||||
track_data = row['track_data_json']
|
||||
if isinstance(track_data, str):
|
||||
try:
|
||||
track_data = json.loads(track_data)
|
||||
except:
|
||||
track_data = None
|
||||
|
||||
return {
|
||||
'track_id': row.get('spotify_track_id') or row.get('itunes_track_id'),
|
||||
'spotify_track_id': row.get('spotify_track_id'),
|
||||
'itunes_track_id': row.get('itunes_track_id'),
|
||||
'track_name': row['track_name'],
|
||||
'artist_name': row['artist_name'],
|
||||
'album_name': row['album_name'],
|
||||
'album_cover_url': row['album_cover_url'],
|
||||
'duration_ms': row['duration_ms'],
|
||||
'popularity': row.get('popularity', 0),
|
||||
'track_data_json': track_data,
|
||||
'source': source
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_parent_genre(spotify_genre: str) -> str:
|
||||
"""
|
||||
|
|
@ -166,25 +199,30 @@ class PersonalizedPlaylistsService:
|
|||
logger.error(f"Error getting forgotten favorites: {e}")
|
||||
return []
|
||||
|
||||
def get_decade_playlist(self, decade: int, limit: int = 100) -> List[Dict]:
|
||||
def get_decade_playlist(self, decade: int, limit: int = 100, source: str = None) -> List[Dict]:
|
||||
"""
|
||||
Get tracks from a specific decade from discovery pool with diversity filtering.
|
||||
|
||||
Args:
|
||||
decade: Decade year (e.g., 2020 for 2020s, 2010 for 2010s)
|
||||
limit: Maximum tracks to return
|
||||
source: Optional source filter ('spotify' or 'itunes'), auto-detects if not provided
|
||||
"""
|
||||
try:
|
||||
start_year = decade
|
||||
end_year = decade + 9
|
||||
|
||||
# Determine active source if not specified
|
||||
active_source = source or self._get_active_source()
|
||||
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Query discovery_pool - get 10x more for diversity filtering
|
||||
# Query discovery_pool - get 10x more for diversity filtering, filtered by source
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
spotify_track_id,
|
||||
itunes_track_id,
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
|
|
@ -192,26 +230,20 @@ class PersonalizedPlaylistsService:
|
|||
duration_ms,
|
||||
popularity,
|
||||
release_date,
|
||||
track_data_json
|
||||
track_data_json,
|
||||
source
|
||||
FROM discovery_pool
|
||||
WHERE release_date IS NOT NULL
|
||||
AND CAST(SUBSTR(release_date, 1, 4) AS INTEGER) BETWEEN ? AND ?
|
||||
AND source = ?
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
""", (start_year, end_year, limit * 10))
|
||||
""", (start_year, end_year, active_source, limit * 10))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
all_tracks = []
|
||||
for row in rows:
|
||||
track_dict = dict(row)
|
||||
# Parse track_data_json if available
|
||||
if track_dict.get('track_data_json'):
|
||||
try:
|
||||
import json
|
||||
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
||||
except:
|
||||
pass
|
||||
all_tracks.append(track_dict)
|
||||
all_tracks.append(self._build_track_dict(row, active_source))
|
||||
|
||||
if not all_tracks:
|
||||
logger.warning(f"No tracks found for {decade}s")
|
||||
|
|
@ -268,22 +300,25 @@ class PersonalizedPlaylistsService:
|
|||
logger.error(f"Error getting decade playlist for {decade}s: {e}")
|
||||
return []
|
||||
|
||||
def get_available_genres(self) -> List[Dict]:
|
||||
def get_available_genres(self, source: str = None) -> List[Dict]:
|
||||
"""
|
||||
Get list of consolidated parent genres with track counts from discovery pool.
|
||||
Uses cached artist genres from database (populated during discovery scan).
|
||||
Consolidates specific Spotify genres into broader parent categories.
|
||||
"""
|
||||
try:
|
||||
# Determine active source if not specified
|
||||
active_source = source or self._get_active_source()
|
||||
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all tracks with genres from discovery pool
|
||||
# Get all tracks with genres from discovery pool, filtered by source
|
||||
cursor.execute("""
|
||||
SELECT artist_genres
|
||||
FROM discovery_pool
|
||||
WHERE artist_genres IS NOT NULL
|
||||
""")
|
||||
WHERE artist_genres IS NOT NULL AND source = ?
|
||||
""", (active_source,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if not rows:
|
||||
|
|
@ -327,20 +362,24 @@ class PersonalizedPlaylistsService:
|
|||
logger.error(f"Error getting available genres: {e}")
|
||||
return []
|
||||
|
||||
def get_genre_playlist(self, genre: str, limit: int = 50) -> List[Dict]:
|
||||
def get_genre_playlist(self, genre: str, limit: int = 50, source: str = None) -> List[Dict]:
|
||||
"""
|
||||
Get tracks from a specific genre with diversity filtering.
|
||||
Uses cached artist genres from database (populated during discovery scan).
|
||||
Supports both parent genres (e.g., "Electronic/Dance") and specific genres (e.g., "house").
|
||||
"""
|
||||
try:
|
||||
# Determine active source if not specified
|
||||
active_source = source or self._get_active_source()
|
||||
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all tracks with genres from discovery pool
|
||||
# Get all tracks with genres from discovery pool, filtered by source
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
spotify_track_id,
|
||||
itunes_track_id,
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
|
|
@ -348,10 +387,12 @@ class PersonalizedPlaylistsService:
|
|||
duration_ms,
|
||||
popularity,
|
||||
artist_genres,
|
||||
track_data_json
|
||||
track_data_json,
|
||||
source
|
||||
FROM discovery_pool
|
||||
WHERE artist_genres IS NOT NULL
|
||||
""")
|
||||
AND source = ?
|
||||
""", (active_source,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Determine if this is a parent genre or specific genre
|
||||
|
|
@ -372,7 +413,7 @@ class PersonalizedPlaylistsService:
|
|||
|
||||
for row in rows:
|
||||
try:
|
||||
artist_genres_json = row[7] # artist_genres column
|
||||
artist_genres_json = row['artist_genres']
|
||||
if artist_genres_json:
|
||||
genres = json.loads(artist_genres_json)
|
||||
|
||||
|
|
@ -388,23 +429,7 @@ class PersonalizedPlaylistsService:
|
|||
break
|
||||
|
||||
if genre_match:
|
||||
# Convert row to dict (exclude artist_genres from output)
|
||||
track_dict = {
|
||||
'spotify_track_id': row[0],
|
||||
'track_name': row[1],
|
||||
'artist_name': row[2],
|
||||
'album_name': row[3],
|
||||
'album_cover_url': row[4],
|
||||
'duration_ms': row[5],
|
||||
'popularity': row[6]
|
||||
}
|
||||
# Parse track_data_json if available
|
||||
if row[8]: # track_data_json column
|
||||
try:
|
||||
track_dict['track_data_json'] = json.loads(row[8])
|
||||
except:
|
||||
pass
|
||||
matching_tracks.append(track_dict)
|
||||
matching_tracks.append(self._build_track_dict(row, active_source))
|
||||
except Exception as e:
|
||||
logger.debug(f"Error parsing genres for track: {e}")
|
||||
continue
|
||||
|
|
@ -475,39 +500,34 @@ class PersonalizedPlaylistsService:
|
|||
|
||||
def get_popular_picks(self, limit: int = 50) -> List[Dict]:
|
||||
"""Get high popularity tracks from discovery pool with diversity (max 2 tracks per album/artist)"""
|
||||
# Determine active source
|
||||
active_source = self._get_active_source()
|
||||
|
||||
try:
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get more tracks than needed to allow for filtering
|
||||
# Get more tracks than needed to allow for filtering, filtered by source
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
spotify_track_id,
|
||||
itunes_track_id,
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
album_cover_url,
|
||||
duration_ms,
|
||||
popularity,
|
||||
track_data_json
|
||||
track_data_json,
|
||||
source
|
||||
FROM discovery_pool
|
||||
WHERE popularity >= 60
|
||||
WHERE popularity >= 60 AND source = ?
|
||||
ORDER BY popularity DESC, RANDOM()
|
||||
LIMIT ?
|
||||
""", (limit * 3,)) # Get 3x more for diversity filtering
|
||||
""", (active_source, limit * 3))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
all_tracks = []
|
||||
for row in rows:
|
||||
track_dict = dict(row)
|
||||
# Parse track_data_json if available
|
||||
if track_dict.get('track_data_json'):
|
||||
try:
|
||||
import json
|
||||
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
||||
except:
|
||||
pass
|
||||
all_tracks.append(track_dict)
|
||||
all_tracks = [self._build_track_dict(row, active_source) for row in rows]
|
||||
|
||||
# Apply diversity constraint: max 2 tracks per album, max 3 per artist
|
||||
tracks_by_album = {}
|
||||
|
|
@ -531,7 +551,7 @@ class PersonalizedPlaylistsService:
|
|||
if len(diverse_tracks) >= limit:
|
||||
break
|
||||
|
||||
logger.info(f"Popular Picks: Selected {len(diverse_tracks)} tracks with diversity")
|
||||
logger.info(f"Popular Picks ({active_source}): Selected {len(diverse_tracks)} tracks with diversity")
|
||||
return diverse_tracks[:limit]
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -540,6 +560,9 @@ class PersonalizedPlaylistsService:
|
|||
|
||||
def get_hidden_gems(self, limit: int = 50) -> List[Dict]:
|
||||
"""Get low popularity (underground/indie) tracks from discovery pool"""
|
||||
# Determine active source
|
||||
active_source = self._get_active_source()
|
||||
|
||||
try:
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
|
@ -547,32 +570,23 @@ class PersonalizedPlaylistsService:
|
|||
cursor.execute("""
|
||||
SELECT
|
||||
spotify_track_id,
|
||||
itunes_track_id,
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
album_cover_url,
|
||||
duration_ms,
|
||||
popularity,
|
||||
track_data_json
|
||||
track_data_json,
|
||||
source
|
||||
FROM discovery_pool
|
||||
WHERE popularity < 40
|
||||
WHERE popularity < 40 AND source = ?
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
""", (active_source, limit))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
tracks = []
|
||||
for row in rows:
|
||||
track_dict = dict(row)
|
||||
# Parse track_data_json if available
|
||||
if track_dict.get('track_data_json'):
|
||||
try:
|
||||
import json
|
||||
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
||||
except:
|
||||
pass
|
||||
tracks.append(track_dict)
|
||||
return tracks
|
||||
return [self._build_track_dict(row, active_source) for row in rows]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting hidden gems: {e}")
|
||||
|
|
@ -584,6 +598,9 @@ class PersonalizedPlaylistsService:
|
|||
|
||||
Different every time you call it!
|
||||
"""
|
||||
# Determine active source
|
||||
active_source = self._get_active_source()
|
||||
|
||||
try:
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
|
@ -591,31 +608,23 @@ class PersonalizedPlaylistsService:
|
|||
cursor.execute("""
|
||||
SELECT
|
||||
spotify_track_id,
|
||||
itunes_track_id,
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
album_cover_url,
|
||||
duration_ms,
|
||||
popularity,
|
||||
track_data_json
|
||||
track_data_json,
|
||||
source
|
||||
FROM discovery_pool
|
||||
WHERE source = ?
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
""", (active_source, limit))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
tracks = []
|
||||
for row in rows:
|
||||
track_dict = dict(row)
|
||||
# Parse track_data_json if available
|
||||
if track_dict.get('track_data_json'):
|
||||
try:
|
||||
import json
|
||||
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
||||
except:
|
||||
pass
|
||||
tracks.append(track_dict)
|
||||
return tracks
|
||||
return [self._build_track_dict(row, active_source) for row in rows]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting discovery shuffle: {e}")
|
||||
|
|
@ -769,6 +778,9 @@ class PersonalizedPlaylistsService:
|
|||
|
||||
def _get_discovery_tracks_by_category(self, category: str, limit: int) -> List[Dict]:
|
||||
"""Get tracks from discovery pool matching genre or artist"""
|
||||
# Determine active source
|
||||
active_source = self._get_active_source()
|
||||
|
||||
try:
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
|
@ -776,32 +788,23 @@ class PersonalizedPlaylistsService:
|
|||
cursor.execute("""
|
||||
SELECT
|
||||
spotify_track_id,
|
||||
itunes_track_id,
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
album_cover_url,
|
||||
duration_ms,
|
||||
popularity,
|
||||
track_data_json
|
||||
track_data_json,
|
||||
source
|
||||
FROM discovery_pool
|
||||
WHERE artist_name LIKE ? OR track_name LIKE ?
|
||||
WHERE (artist_name LIKE ? OR track_name LIKE ?) AND source = ?
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
""", (f'%{category}%', f'%{category}%', limit))
|
||||
""", (f'%{category}%', f'%{category}%', active_source, limit))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
tracks = []
|
||||
for row in rows:
|
||||
track_dict = dict(row)
|
||||
# Parse track_data_json if available
|
||||
if track_dict.get('track_data_json'):
|
||||
try:
|
||||
import json
|
||||
track_dict['track_data_json'] = json.loads(track_dict['track_data_json'])
|
||||
except:
|
||||
pass
|
||||
tracks.append(track_dict)
|
||||
return tracks
|
||||
return [self._build_track_dict(row, active_source) for row in rows]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting discovery tracks by category: {e}")
|
||||
|
|
|
|||
|
|
@ -557,20 +557,18 @@ class WatchlistScanner:
|
|||
self.update_artist_scan_timestamp(db_artist_id)
|
||||
|
||||
# Fetch and store similar artists for discovery feature (with caching to avoid over-polling)
|
||||
# Note: Similar artists feature only works with Spotify
|
||||
if provider == 'spotify' and watchlist_artist.spotify_artist_id:
|
||||
try:
|
||||
# Check if we have fresh similar artists cached (< 30 days old)
|
||||
if self.database.has_fresh_similar_artists(watchlist_artist.spotify_artist_id, days_threshold=30):
|
||||
logger.info(f"Similar artists for {watchlist_artist.artist_name} are cached and fresh, skipping fetch")
|
||||
else:
|
||||
logger.info(f"Fetching similar artists for {watchlist_artist.artist_name}...")
|
||||
self.update_similar_artists(watchlist_artist)
|
||||
logger.info(f"Similar artists updated for {watchlist_artist.artist_name}")
|
||||
except Exception as similar_error:
|
||||
logger.warning(f"Failed to update similar artists for {watchlist_artist.artist_name}: {similar_error}")
|
||||
else:
|
||||
logger.debug(f"Skipping similar artists for {watchlist_artist.artist_name} (iTunes doesn't support this feature)")
|
||||
# Similar artists are fetched from MusicMap (works with any source) and matched to both Spotify and iTunes
|
||||
source_artist_id = watchlist_artist.spotify_artist_id or watchlist_artist.itunes_artist_id or str(watchlist_artist.id)
|
||||
try:
|
||||
# Check if we have fresh similar artists cached (< 30 days old)
|
||||
if self.database.has_fresh_similar_artists(source_artist_id, days_threshold=30):
|
||||
logger.info(f"Similar artists for {watchlist_artist.artist_name} are cached and fresh, skipping fetch")
|
||||
else:
|
||||
logger.info(f"Fetching similar artists for {watchlist_artist.artist_name}...")
|
||||
self.update_similar_artists(watchlist_artist)
|
||||
logger.info(f"Similar artists updated for {watchlist_artist.artist_name}")
|
||||
except Exception as similar_error:
|
||||
logger.warning(f"Failed to update similar artists for {watchlist_artist.artist_name}: {similar_error}")
|
||||
|
||||
return ScanResult(
|
||||
artist_name=watchlist_artist.artist_name,
|
||||
|
|
@ -1100,14 +1098,14 @@ class WatchlistScanner:
|
|||
|
||||
def _fetch_similar_artists_from_musicmap(self, artist_name: str, limit: int = 20) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Fetch similar artists from MusicMap and match them to Spotify.
|
||||
Fetch similar artists from MusicMap and match them to both Spotify and iTunes.
|
||||
|
||||
Args:
|
||||
artist_name: The artist name to find similar artists for
|
||||
limit: Maximum number of similar artists to return (default: 20)
|
||||
|
||||
Returns:
|
||||
List of matched artist dictionaries with Spotify data
|
||||
List of matched artist dictionaries with both Spotify and iTunes IDs when available
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Fetching similar artists from MusicMap for: {artist_name}")
|
||||
|
|
@ -1151,52 +1149,94 @@ class WatchlistScanner:
|
|||
|
||||
logger.info(f"Found {len(similar_artist_names)} similar artists from MusicMap")
|
||||
|
||||
# Get the searched artist's Spotify ID to exclude them
|
||||
searched_artist_id = None
|
||||
try:
|
||||
searched_results = self.spotify_client.search_artists(artist_name, limit=1)
|
||||
if searched_results and len(searched_results) > 0:
|
||||
searched_artist_id = searched_results[0].id
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get searched artist ID: {e}")
|
||||
# Get iTunes client for matching
|
||||
from core.itunes_client import iTunesClient
|
||||
itunes_client = iTunesClient()
|
||||
|
||||
# Match each artist to Spotify
|
||||
# Get the searched artist's IDs to exclude them
|
||||
searched_spotify_id = None
|
||||
searched_itunes_id = None
|
||||
try:
|
||||
# Try Spotify search
|
||||
if self.spotify_client and self.spotify_client.is_spotify_authenticated():
|
||||
searched_results = self.spotify_client.search_artists(artist_name, limit=1)
|
||||
if searched_results and len(searched_results) > 0:
|
||||
searched_spotify_id = searched_results[0].id
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not get searched artist Spotify ID: {e}")
|
||||
|
||||
try:
|
||||
# Try iTunes search
|
||||
itunes_results = itunes_client.search_artists(artist_name, limit=1)
|
||||
if itunes_results and len(itunes_results) > 0:
|
||||
searched_itunes_id = itunes_results[0].id
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not get searched artist iTunes ID: {e}")
|
||||
|
||||
# Match each artist to both Spotify and iTunes
|
||||
matched_artists = []
|
||||
seen_artist_ids = set() # Track seen artist IDs to prevent duplicates
|
||||
seen_names = set() # Track seen artist names to prevent duplicates
|
||||
|
||||
for artist_name_to_match in similar_artist_names[:limit]:
|
||||
try:
|
||||
# Search Spotify for the artist
|
||||
results = self.spotify_client.search_artists(artist_name_to_match, limit=1)
|
||||
# Skip if we've already matched this artist name
|
||||
name_lower = artist_name_to_match.lower().strip()
|
||||
if name_lower in seen_names:
|
||||
continue
|
||||
|
||||
if results and len(results) > 0:
|
||||
spotify_artist = results[0]
|
||||
artist_data = {
|
||||
'name': artist_name_to_match,
|
||||
'spotify_id': None,
|
||||
'itunes_id': None,
|
||||
'image_url': None,
|
||||
'genres': [],
|
||||
'popularity': 0
|
||||
}
|
||||
|
||||
# Skip if this is the searched artist
|
||||
if spotify_artist.id == searched_artist_id:
|
||||
continue
|
||||
# Try to match on Spotify
|
||||
if self.spotify_client and self.spotify_client.is_spotify_authenticated():
|
||||
try:
|
||||
spotify_results = self.spotify_client.search_artists(artist_name_to_match, limit=1)
|
||||
if spotify_results and len(spotify_results) > 0:
|
||||
spotify_artist = spotify_results[0]
|
||||
# Skip if this is the searched artist
|
||||
if spotify_artist.id != searched_spotify_id:
|
||||
artist_data['spotify_id'] = spotify_artist.id
|
||||
artist_data['name'] = spotify_artist.name # Use canonical name
|
||||
artist_data['image_url'] = spotify_artist.image_url if hasattr(spotify_artist, 'image_url') else None
|
||||
artist_data['genres'] = spotify_artist.genres if hasattr(spotify_artist, 'genres') else []
|
||||
artist_data['popularity'] = spotify_artist.popularity if hasattr(spotify_artist, 'popularity') else 0
|
||||
except Exception as e:
|
||||
logger.debug(f"Spotify match failed for {artist_name_to_match}: {e}")
|
||||
|
||||
# Skip if we've already seen this artist ID (deduplication)
|
||||
if spotify_artist.id in seen_artist_ids:
|
||||
continue
|
||||
# Try to match on iTunes
|
||||
try:
|
||||
itunes_results = itunes_client.search_artists(artist_name_to_match, limit=1)
|
||||
if itunes_results and len(itunes_results) > 0:
|
||||
itunes_artist = itunes_results[0]
|
||||
# Skip if this is the searched artist
|
||||
if itunes_artist.id != searched_itunes_id:
|
||||
artist_data['itunes_id'] = itunes_artist.id
|
||||
# Use iTunes name if we don't have Spotify
|
||||
if not artist_data['spotify_id']:
|
||||
artist_data['name'] = itunes_artist.name
|
||||
# Use iTunes genres if we don't have Spotify genres
|
||||
if not artist_data['genres'] and hasattr(itunes_artist, 'genres'):
|
||||
artist_data['genres'] = itunes_artist.genres
|
||||
except Exception as e:
|
||||
logger.debug(f"iTunes match failed for {artist_name_to_match}: {e}")
|
||||
|
||||
seen_artist_ids.add(spotify_artist.id)
|
||||
|
||||
matched_artists.append({
|
||||
'id': spotify_artist.id,
|
||||
'name': spotify_artist.name,
|
||||
'image_url': spotify_artist.image_url if hasattr(spotify_artist, 'image_url') else None,
|
||||
'genres': spotify_artist.genres if hasattr(spotify_artist, 'genres') else [],
|
||||
'popularity': spotify_artist.popularity if hasattr(spotify_artist, 'popularity') else 0
|
||||
})
|
||||
|
||||
logger.debug(f" Matched: {spotify_artist.name}")
|
||||
# Only add if we got at least one ID
|
||||
if artist_data['spotify_id'] or artist_data['itunes_id']:
|
||||
seen_names.add(name_lower)
|
||||
matched_artists.append(artist_data)
|
||||
logger.debug(f" Matched: {artist_data['name']} (Spotify: {artist_data['spotify_id']}, iTunes: {artist_data['itunes_id']})")
|
||||
|
||||
except Exception as match_error:
|
||||
logger.debug(f"Error matching {artist_name_to_match}: {match_error}")
|
||||
continue
|
||||
|
||||
logger.info(f"Matched {len(matched_artists)} similar artists to Spotify")
|
||||
logger.info(f"Matched {len(matched_artists)} similar artists (Spotify + iTunes)")
|
||||
return matched_artists
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
|
|
@ -1210,12 +1250,12 @@ class WatchlistScanner:
|
|||
"""
|
||||
Fetch and store similar artists for a watchlist artist.
|
||||
Called after each artist scan to build discovery pool.
|
||||
Uses MusicMap to find similar artists and matches them to Spotify.
|
||||
Uses MusicMap to find similar artists and matches them to both Spotify and iTunes.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Fetching similar artists for {watchlist_artist.artist_name}")
|
||||
|
||||
# Get similar artists from MusicMap (returns list of artist dicts)
|
||||
# Get similar artists from MusicMap (returns list of artist dicts with both IDs)
|
||||
similar_artists = self._fetch_similar_artists_from_musicmap(watchlist_artist.artist_name, limit=limit)
|
||||
|
||||
if not similar_artists:
|
||||
|
|
@ -1224,21 +1264,25 @@ class WatchlistScanner:
|
|||
|
||||
logger.info(f"Found {len(similar_artists)} similar artists for {watchlist_artist.artist_name}")
|
||||
|
||||
# Use consistent source artist ID (prefer Spotify, fall back to iTunes or internal ID)
|
||||
source_artist_id = watchlist_artist.spotify_artist_id or watchlist_artist.itunes_artist_id or str(watchlist_artist.id)
|
||||
|
||||
# Store each similar artist in database
|
||||
stored_count = 0
|
||||
for rank, similar_artist in enumerate(similar_artists, 1):
|
||||
try:
|
||||
# similar_artist is a dict with 'id' and 'name' keys
|
||||
# similar_artist has 'name', 'spotify_id', and 'itunes_id' keys
|
||||
success = self.database.add_or_update_similar_artist(
|
||||
source_artist_id=watchlist_artist.spotify_artist_id,
|
||||
similar_artist_spotify_id=similar_artist['id'],
|
||||
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
|
||||
)
|
||||
|
||||
if success:
|
||||
stored_count += 1
|
||||
logger.debug(f" #{rank}: {similar_artist['name']} (Spotify ID: {similar_artist['id']})")
|
||||
logger.debug(f" #{rank}: {similar_artist['name']} (Spotify: {similar_artist.get('spotify_id')}, iTunes: {similar_artist.get('itunes_id')})")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error storing similar artist {similar_artist.get('name', 'Unknown')}: {e}")
|
||||
|
|
@ -1256,8 +1300,8 @@ class WatchlistScanner:
|
|||
Populate discovery pool with tracks from top similar artists.
|
||||
Called after watchlist scan completes.
|
||||
|
||||
IMPROVED: Larger pool for better discovery (50 artists x 10 releases = ~500 releases)
|
||||
- Checks if pool was updated in last 24 hours (prevents over-polling Spotify)
|
||||
Supports both Spotify and iTunes sources - populates for whichever is available.
|
||||
- Checks if pool was updated in last 24 hours (prevents over-polling)
|
||||
- Includes albums, singles, and EPs for comprehensive coverage
|
||||
- Appends to existing pool instead of replacing it
|
||||
- Cleans up tracks older than 365 days (maintains 1 year rolling window)
|
||||
|
|
@ -1266,13 +1310,27 @@ class WatchlistScanner:
|
|||
from datetime import datetime, timedelta
|
||||
import random
|
||||
|
||||
# Check if we should run (prevents over-polling Spotify)
|
||||
# Check if we should run (prevents over-polling)
|
||||
if not self.database.should_populate_discovery_pool(hours_threshold=24):
|
||||
logger.info("Discovery pool was populated recently (< 24 hours ago). Skipping to avoid over-polling Spotify.")
|
||||
logger.info("Discovery pool was populated recently (< 24 hours ago). Skipping.")
|
||||
return
|
||||
|
||||
logger.info("Populating discovery pool from similar artists...")
|
||||
|
||||
# Determine which sources are available
|
||||
spotify_available = self.spotify_client and self.spotify_client.is_spotify_authenticated()
|
||||
|
||||
# Import iTunes client for fallback
|
||||
from core.itunes_client import iTunesClient
|
||||
itunes_client = iTunesClient()
|
||||
itunes_available = True # iTunes is always available (no auth needed)
|
||||
|
||||
if not spotify_available and not itunes_available:
|
||||
logger.warning("No music sources available to populate discovery pool")
|
||||
return
|
||||
|
||||
logger.info(f"Sources available - Spotify: {spotify_available}, iTunes: {itunes_available}")
|
||||
|
||||
# Get top similar artists across all watchlist (ordered by occurrence_count)
|
||||
similar_artists = self.database.get_top_similar_artists(limit=top_artists_limit)
|
||||
|
||||
|
|
@ -1288,129 +1346,182 @@ class WatchlistScanner:
|
|||
try:
|
||||
logger.info(f"[{artist_idx}/{len(similar_artists)}] Processing {similar_artist.similar_artist_name} (occurrence: {similar_artist.occurrence_count})")
|
||||
|
||||
# Get artist's albums from Spotify
|
||||
all_albums = self.spotify_client.get_artist_albums(
|
||||
similar_artist.similar_artist_spotify_id,
|
||||
album_type='album,single,ep', # Include albums, singles, and EPs for comprehensive discovery
|
||||
limit=50
|
||||
)
|
||||
# Build list of sources to process for this artist
|
||||
# iTunes is ALWAYS processed (baseline), Spotify is added if authenticated
|
||||
sources_to_process = []
|
||||
|
||||
if not all_albums:
|
||||
logger.debug(f"No albums found for {similar_artist.similar_artist_name}")
|
||||
# Always add iTunes first (baseline source)
|
||||
if similar_artist.similar_artist_itunes_id:
|
||||
sources_to_process.append(('itunes', similar_artist.similar_artist_itunes_id))
|
||||
|
||||
# Add Spotify if authenticated and we have an ID
|
||||
if spotify_available and similar_artist.similar_artist_spotify_id:
|
||||
sources_to_process.append(('spotify', similar_artist.similar_artist_spotify_id))
|
||||
|
||||
if not sources_to_process:
|
||||
logger.debug(f"No valid IDs for {similar_artist.similar_artist_name}, skipping")
|
||||
continue
|
||||
|
||||
# Fetch artist genres once for all tracks of this artist
|
||||
artist_genres = []
|
||||
try:
|
||||
artist_data = self.spotify_client.get_artist(similar_artist.similar_artist_spotify_id)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch genres for {similar_artist.similar_artist_name}: {e}")
|
||||
logger.debug(f" Processing {len(sources_to_process)} source(s): {[s[0] for s in sources_to_process]}")
|
||||
|
||||
# IMPROVED: Smart selection mixing albums, singles, and EPs
|
||||
# Prioritize recent releases and popular content
|
||||
|
||||
# Separate by type for balanced selection
|
||||
albums = [a for a in all_albums if hasattr(a, 'album_type') and a.album_type == 'album']
|
||||
singles_eps = [a for a in all_albums if hasattr(a, 'album_type') and a.album_type in ['single', 'ep']]
|
||||
other = [a for a in all_albums if not hasattr(a, 'album_type')]
|
||||
|
||||
# Select albums: latest releases + popular older content
|
||||
selected_albums = []
|
||||
|
||||
# Always include 3 most recent releases (any type) - this captures new singles/EPs
|
||||
latest_releases = all_albums[:3]
|
||||
selected_albums.extend(latest_releases)
|
||||
|
||||
# Add remaining slots with balanced mix
|
||||
remaining_slots = albums_per_artist - len(selected_albums)
|
||||
if remaining_slots > 0:
|
||||
# Combine remaining albums and singles
|
||||
remaining_content = all_albums[3:]
|
||||
|
||||
if len(remaining_content) > remaining_slots:
|
||||
# Randomly select from remaining content
|
||||
random_selection = random.sample(remaining_content, remaining_slots)
|
||||
selected_albums.extend(random_selection)
|
||||
else:
|
||||
selected_albums.extend(remaining_content)
|
||||
|
||||
logger.info(f" Selected {len(selected_albums)} releases from {len(all_albums)} available (albums: {len(albums)}, singles/EPs: {len(singles_eps)})")
|
||||
|
||||
# Process each selected album
|
||||
for album_idx, album in enumerate(selected_albums, 1):
|
||||
# Process each source for this artist
|
||||
for source, artist_id in sources_to_process:
|
||||
try:
|
||||
# Get full album data with tracks
|
||||
album_data = self.spotify_client.get_album(album.id)
|
||||
# Get artist's albums from this source
|
||||
if source == 'spotify':
|
||||
all_albums = self.spotify_client.get_artist_albums(
|
||||
artist_id,
|
||||
album_type='album,single,ep',
|
||||
limit=50
|
||||
)
|
||||
else: # itunes
|
||||
all_albums = itunes_client.get_artist_albums(
|
||||
artist_id,
|
||||
album_type='album,single',
|
||||
limit=50
|
||||
)
|
||||
|
||||
if not album_data or 'tracks' not in album_data:
|
||||
if not all_albums:
|
||||
logger.debug(f"No albums found for {similar_artist.similar_artist_name} on {source}")
|
||||
continue
|
||||
|
||||
tracks = album_data['tracks'].get('items', [])
|
||||
logger.debug(f" Album {album_idx}: {album_data.get('name', 'Unknown')} ({len(tracks)} tracks)")
|
||||
|
||||
# Determine if this is a new release (within last 30 days)
|
||||
is_new = False
|
||||
# Fetch artist genres for this source
|
||||
artist_genres = []
|
||||
try:
|
||||
release_date_str = album_data.get('release_date', '')
|
||||
if release_date_str:
|
||||
if len(release_date_str) == 10: # Full date
|
||||
release_date = datetime.strptime(release_date_str, "%Y-%m-%d")
|
||||
days_old = (datetime.now() - release_date).days
|
||||
is_new = days_old <= 30
|
||||
except:
|
||||
pass
|
||||
if source == 'spotify':
|
||||
artist_data = self.spotify_client.get_artist(artist_id)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
else: # iTunes - genres from artist lookup
|
||||
artist_data = itunes_client.get_artist(artist_id)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch genres for {similar_artist.similar_artist_name} on {source}: {e}")
|
||||
|
||||
# Add each track to discovery pool
|
||||
for track in tracks:
|
||||
# IMPROVED: Smart selection mixing albums, singles, and EPs
|
||||
# Prioritize recent releases and popular content
|
||||
|
||||
# Separate by type for balanced selection
|
||||
albums = [a for a in all_albums if hasattr(a, 'album_type') and a.album_type == 'album']
|
||||
singles_eps = [a for a in all_albums if hasattr(a, 'album_type') and a.album_type in ['single', 'ep']]
|
||||
other = [a for a in all_albums if not hasattr(a, 'album_type')]
|
||||
|
||||
# Select albums: latest releases + popular older content
|
||||
selected_albums = []
|
||||
|
||||
# Always include 3 most recent releases (any type) - this captures new singles/EPs
|
||||
latest_releases = all_albums[:3]
|
||||
selected_albums.extend(latest_releases)
|
||||
|
||||
# Add remaining slots with balanced mix
|
||||
remaining_slots = albums_per_artist - len(selected_albums)
|
||||
if remaining_slots > 0:
|
||||
# Combine remaining albums and singles
|
||||
remaining_content = all_albums[3:]
|
||||
|
||||
if len(remaining_content) > remaining_slots:
|
||||
# Randomly select from remaining content
|
||||
random_selection = random.sample(remaining_content, remaining_slots)
|
||||
selected_albums.extend(random_selection)
|
||||
else:
|
||||
selected_albums.extend(remaining_content)
|
||||
|
||||
logger.info(f" [{source}] Selected {len(selected_albums)} releases from {len(all_albums)} available (albums: {len(albums)}, singles/EPs: {len(singles_eps)})")
|
||||
|
||||
# Process each selected album
|
||||
for album_idx, album in enumerate(selected_albums, 1):
|
||||
try:
|
||||
# Enhance track object with full album data (including album_type)
|
||||
enhanced_track = {
|
||||
**track,
|
||||
'album': {
|
||||
'id': album_data['id'],
|
||||
'name': album_data.get('name', 'Unknown Album'),
|
||||
'images': album_data.get('images', []),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'album_type': album_data.get('album_type', 'album'),
|
||||
'total_tracks': album_data.get('total_tracks', 0)
|
||||
}
|
||||
}
|
||||
# Get full album data with tracks from appropriate source
|
||||
if source == 'spotify':
|
||||
album_data = self.spotify_client.get_album(album.id)
|
||||
if not album_data or 'tracks' not in album_data:
|
||||
continue
|
||||
tracks = album_data['tracks'].get('items', [])
|
||||
else: # itunes
|
||||
album_data = itunes_client.get_album(album.id)
|
||||
if not album_data:
|
||||
continue
|
||||
# iTunes get_album doesn't include tracks inline, need separate call
|
||||
tracks_data = itunes_client.get_album_tracks(album.id)
|
||||
tracks = tracks_data.get('items', []) if tracks_data else []
|
||||
|
||||
# Build track data for discovery pool
|
||||
track_data = {
|
||||
'spotify_track_id': track['id'],
|
||||
'spotify_album_id': album_data['id'],
|
||||
'spotify_artist_id': similar_artist.similar_artist_spotify_id,
|
||||
'track_name': track['name'],
|
||||
'artist_name': similar_artist.similar_artist_name,
|
||||
'album_name': album_data.get('name', 'Unknown Album'),
|
||||
'album_cover_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None,
|
||||
'duration_ms': track.get('duration_ms', 0),
|
||||
'popularity': album_data.get('popularity', 0),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'is_new_release': is_new,
|
||||
'track_data_json': enhanced_track, # Store enhanced track with full album data
|
||||
'artist_genres': artist_genres # Add cached genres
|
||||
}
|
||||
logger.debug(f" Album {album_idx}: {album_data.get('name', 'Unknown')} ({len(tracks)} tracks)")
|
||||
|
||||
# Add to discovery pool
|
||||
if self.database.add_to_discovery_pool(track_data):
|
||||
total_tracks_added += 1
|
||||
# Determine if this is a new release (within last 30 days)
|
||||
is_new = False
|
||||
try:
|
||||
release_date_str = album_data.get('release_date', '')
|
||||
if release_date_str:
|
||||
# Handle full date or year-only
|
||||
if len(release_date_str) >= 10:
|
||||
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
|
||||
days_old = (datetime.now() - release_date).days
|
||||
is_new = days_old <= 30
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as track_error:
|
||||
logger.debug(f"Error adding track to discovery pool: {track_error}")
|
||||
# Add each track to discovery pool
|
||||
for track in tracks:
|
||||
try:
|
||||
# Enhance track object with full album data (including album_type)
|
||||
enhanced_track = {
|
||||
**track,
|
||||
'album': {
|
||||
'id': album_data['id'],
|
||||
'name': album_data.get('name', 'Unknown Album'),
|
||||
'images': album_data.get('images', []),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'album_type': album_data.get('album_type', 'album'),
|
||||
'total_tracks': album_data.get('total_tracks', 0)
|
||||
},
|
||||
'_source': source
|
||||
}
|
||||
|
||||
# Build track data for discovery pool with source-specific IDs
|
||||
track_data = {
|
||||
'track_name': track.get('name', 'Unknown Track'),
|
||||
'artist_name': similar_artist.similar_artist_name,
|
||||
'album_name': album_data.get('name', 'Unknown Album'),
|
||||
'album_cover_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None,
|
||||
'duration_ms': track.get('duration_ms', 0),
|
||||
'popularity': album_data.get('popularity', 0),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'is_new_release': is_new,
|
||||
'track_data_json': enhanced_track,
|
||||
'artist_genres': artist_genres
|
||||
}
|
||||
|
||||
# Add source-specific IDs
|
||||
if source == 'spotify':
|
||||
track_data['spotify_track_id'] = track.get('id')
|
||||
track_data['spotify_album_id'] = album_data.get('id')
|
||||
track_data['spotify_artist_id'] = similar_artist.similar_artist_spotify_id
|
||||
else: # itunes
|
||||
track_data['itunes_track_id'] = track.get('id')
|
||||
track_data['itunes_album_id'] = album_data.get('id')
|
||||
track_data['itunes_artist_id'] = similar_artist.similar_artist_itunes_id
|
||||
|
||||
# Add to discovery pool with source
|
||||
if self.database.add_to_discovery_pool(track_data, source=source):
|
||||
total_tracks_added += 1
|
||||
|
||||
except Exception as track_error:
|
||||
logger.debug(f"Error adding track to discovery pool: {track_error}")
|
||||
continue
|
||||
|
||||
# Small delay between albums
|
||||
time.sleep(DELAY_BETWEEN_ALBUMS)
|
||||
|
||||
except Exception as album_error:
|
||||
logger.warning(f"Error processing album on {source}: {album_error}")
|
||||
continue
|
||||
|
||||
# Small delay between albums
|
||||
time.sleep(DELAY_BETWEEN_ALBUMS)
|
||||
|
||||
except Exception as album_error:
|
||||
logger.warning(f"Error processing album: {album_error}")
|
||||
except Exception as source_error:
|
||||
logger.warning(f"Error processing {source} source for {similar_artist.similar_artist_name}: {source_error}")
|
||||
continue
|
||||
|
||||
# Delay between artists
|
||||
# Delay between artists (after processing all sources for this artist)
|
||||
if artist_idx < len(similar_artists):
|
||||
time.sleep(DELAY_BETWEEN_ARTISTS)
|
||||
|
||||
|
|
@ -1441,76 +1552,115 @@ class WatchlistScanner:
|
|||
|
||||
for db_idx, album_row in enumerate(db_albums, 1):
|
||||
try:
|
||||
# Search for album on Spotify
|
||||
query = f"album:{album_row['title']} artist:{album_row['artist_name']}"
|
||||
search_results = self.spotify_client.search_albums(query, limit=1)
|
||||
query = f"{album_row['title']} {album_row['artist_name']}"
|
||||
album_data = None
|
||||
tracks = []
|
||||
db_source = None
|
||||
artist_id_for_genres = None
|
||||
|
||||
if search_results and len(search_results) > 0:
|
||||
spotify_album = search_results[0]
|
||||
album_data = self.spotify_client.get_album(spotify_album.id)
|
||||
# Try Spotify first if available
|
||||
if spotify_available:
|
||||
try:
|
||||
search_results = self.spotify_client.search_albums(f"album:{album_row['title']} artist:{album_row['artist_name']}", limit=1)
|
||||
if search_results and len(search_results) > 0:
|
||||
spotify_album = search_results[0]
|
||||
album_data = self.spotify_client.get_album(spotify_album.id)
|
||||
if album_data and 'tracks' in album_data:
|
||||
tracks = album_data['tracks'].get('items', [])
|
||||
db_source = 'spotify'
|
||||
if album_data.get('artists'):
|
||||
artist_id_for_genres = album_data['artists'][0]['id']
|
||||
except Exception as e:
|
||||
logger.debug(f"Spotify search failed for {album_row['title']}: {e}")
|
||||
|
||||
if album_data and 'tracks' in album_data:
|
||||
tracks = album_data['tracks'].get('items', [])
|
||||
# Fall back to iTunes if Spotify didn't work
|
||||
if not tracks and itunes_available:
|
||||
try:
|
||||
search_results = itunes_client.search_albums(query, limit=1)
|
||||
if search_results and len(search_results) > 0:
|
||||
itunes_album = search_results[0]
|
||||
album_data = itunes_client.get_album(itunes_album.id)
|
||||
if album_data:
|
||||
tracks_data = itunes_client.get_album_tracks(itunes_album.id)
|
||||
tracks = tracks_data.get('items', []) if tracks_data else []
|
||||
db_source = 'itunes'
|
||||
# For iTunes, artist ID is in the album data
|
||||
if album_data.get('artists'):
|
||||
artist_id_for_genres = album_data['artists'][0].get('id')
|
||||
except Exception as e:
|
||||
logger.debug(f"iTunes search failed for {album_row['title']}: {e}")
|
||||
|
||||
# Fetch artist genres
|
||||
artist_genres = []
|
||||
try:
|
||||
if album_data.get('artists') and len(album_data['artists']) > 0:
|
||||
artist_id = album_data['artists'][0]['id']
|
||||
artist_data = self.spotify_client.get_artist(artist_id)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch genres for album artist: {e}")
|
||||
if not tracks or not album_data:
|
||||
continue
|
||||
|
||||
# Check if new release
|
||||
is_new = False
|
||||
try:
|
||||
release_date_str = album_data.get('release_date', '')
|
||||
if release_date_str and len(release_date_str) == 10:
|
||||
release_date = datetime.strptime(release_date_str, "%Y-%m-%d")
|
||||
days_old = (datetime.now() - release_date).days
|
||||
is_new = days_old <= 30
|
||||
except:
|
||||
pass
|
||||
# Fetch artist genres
|
||||
artist_genres = []
|
||||
try:
|
||||
if artist_id_for_genres:
|
||||
if db_source == 'spotify':
|
||||
artist_data = self.spotify_client.get_artist(artist_id_for_genres)
|
||||
else:
|
||||
artist_data = itunes_client.get_artist(artist_id_for_genres)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch genres for album artist: {e}")
|
||||
|
||||
for track in tracks:
|
||||
try:
|
||||
# Enhance track object with full album data (including album_type)
|
||||
enhanced_track = {
|
||||
**track,
|
||||
'album': {
|
||||
'id': album_data['id'],
|
||||
'name': album_row['title'],
|
||||
'images': album_data.get('images', []),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'album_type': album_data.get('album_type', 'album'),
|
||||
'total_tracks': album_data.get('total_tracks', 0)
|
||||
}
|
||||
}
|
||||
# Check if new release
|
||||
is_new = False
|
||||
try:
|
||||
release_date_str = album_data.get('release_date', '')
|
||||
if release_date_str and len(release_date_str) >= 10:
|
||||
release_date = datetime.strptime(release_date_str[:10], "%Y-%m-%d")
|
||||
days_old = (datetime.now() - release_date).days
|
||||
is_new = days_old <= 30
|
||||
except:
|
||||
pass
|
||||
|
||||
track_data = {
|
||||
'spotify_track_id': track['id'],
|
||||
'spotify_album_id': album_data['id'],
|
||||
'spotify_artist_id': album_data['artists'][0]['id'] if album_data.get('artists') else '',
|
||||
'track_name': track['name'],
|
||||
'artist_name': album_row['artist_name'],
|
||||
'album_name': album_row['title'],
|
||||
'album_cover_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None,
|
||||
'duration_ms': track.get('duration_ms', 0),
|
||||
'popularity': album_data.get('popularity', 0),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'is_new_release': is_new,
|
||||
'track_data_json': enhanced_track, # Store enhanced track with full album data
|
||||
'artist_genres': artist_genres
|
||||
}
|
||||
for track in tracks:
|
||||
try:
|
||||
enhanced_track = {
|
||||
**track,
|
||||
'album': {
|
||||
'id': album_data['id'],
|
||||
'name': album_row['title'],
|
||||
'images': album_data.get('images', []),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'album_type': album_data.get('album_type', 'album'),
|
||||
'total_tracks': album_data.get('total_tracks', 0)
|
||||
},
|
||||
'_source': db_source
|
||||
}
|
||||
|
||||
if self.database.add_to_discovery_pool(track_data):
|
||||
total_tracks_added += 1
|
||||
except Exception as track_error:
|
||||
continue
|
||||
track_data = {
|
||||
'track_name': track.get('name', 'Unknown Track'),
|
||||
'artist_name': album_row['artist_name'],
|
||||
'album_name': album_row['title'],
|
||||
'album_cover_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None,
|
||||
'duration_ms': track.get('duration_ms', 0),
|
||||
'popularity': album_data.get('popularity', 0),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'is_new_release': is_new,
|
||||
'track_data_json': enhanced_track,
|
||||
'artist_genres': artist_genres
|
||||
}
|
||||
|
||||
time.sleep(DELAY_BETWEEN_ALBUMS)
|
||||
# Add source-specific IDs
|
||||
if db_source == 'spotify':
|
||||
track_data['spotify_track_id'] = track.get('id')
|
||||
track_data['spotify_album_id'] = album_data.get('id')
|
||||
track_data['spotify_artist_id'] = artist_id_for_genres or ''
|
||||
else: # itunes
|
||||
track_data['itunes_track_id'] = track.get('id')
|
||||
track_data['itunes_album_id'] = album_data.get('id')
|
||||
track_data['itunes_artist_id'] = artist_id_for_genres or ''
|
||||
|
||||
if self.database.add_to_discovery_pool(track_data, source=db_source):
|
||||
total_tracks_added += 1
|
||||
except Exception as track_error:
|
||||
continue
|
||||
|
||||
time.sleep(DELAY_BETWEEN_ALBUMS)
|
||||
except Exception as album_error:
|
||||
logger.debug(f"Error processing database album {album_row['title']}: {album_error}")
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -97,10 +97,11 @@ class WatchlistArtist:
|
|||
|
||||
@dataclass
|
||||
class SimilarArtist:
|
||||
"""Similar artist recommendation from Spotify"""
|
||||
"""Similar artist recommendation from Spotify/iTunes"""
|
||||
id: int
|
||||
source_artist_id: str # Watchlist artist's database ID
|
||||
similar_artist_spotify_id: str
|
||||
similar_artist_spotify_id: Optional[str] # Spotify artist ID (may be None if iTunes-only)
|
||||
similar_artist_itunes_id: Optional[str] # iTunes artist ID (may be None if Spotify-only)
|
||||
similar_artist_name: str
|
||||
similarity_rank: int # 1-10, where 1 is most similar
|
||||
occurrence_count: int # How many watchlist artists share this similar artist
|
||||
|
|
@ -110,9 +111,13 @@ class SimilarArtist:
|
|||
class DiscoveryTrack:
|
||||
"""Track in the discovery pool for recommendations"""
|
||||
id: int
|
||||
spotify_track_id: str
|
||||
spotify_album_id: str
|
||||
spotify_artist_id: str
|
||||
spotify_track_id: Optional[str] # Spotify track ID (None if iTunes source)
|
||||
spotify_album_id: Optional[str] # Spotify album ID (None if iTunes source)
|
||||
spotify_artist_id: Optional[str] # Spotify artist ID (None if iTunes source)
|
||||
itunes_track_id: Optional[str] # iTunes track ID (None if Spotify source)
|
||||
itunes_album_id: Optional[str] # iTunes album ID (None if Spotify source)
|
||||
itunes_artist_id: Optional[str] # iTunes artist ID (None if Spotify source)
|
||||
source: str # 'spotify' or 'itunes'
|
||||
track_name: str
|
||||
artist_name: str
|
||||
album_name: str
|
||||
|
|
@ -121,7 +126,7 @@ class DiscoveryTrack:
|
|||
popularity: int
|
||||
release_date: str
|
||||
is_new_release: bool # Released within last 30 days
|
||||
track_data_json: str # Full Spotify track object for modal
|
||||
track_data_json: str # Full track object for modal (Spotify or iTunes format)
|
||||
added_date: datetime
|
||||
|
||||
@dataclass
|
||||
|
|
@ -129,7 +134,9 @@ class RecentRelease:
|
|||
"""Recent album release from watchlist artist"""
|
||||
id: int
|
||||
watchlist_artist_id: int
|
||||
album_spotify_id: str
|
||||
album_spotify_id: Optional[str] # Spotify album ID (None if iTunes source)
|
||||
album_itunes_id: Optional[str] # iTunes album ID (None if Spotify source)
|
||||
source: str # 'spotify' or 'itunes'
|
||||
album_name: str
|
||||
release_date: str
|
||||
album_cover_url: Optional[str]
|
||||
|
|
@ -450,26 +457,33 @@ class MusicDatabase:
|
|||
"""Add tables for discovery feature: similar artists, discovery pool, and recent releases"""
|
||||
try:
|
||||
# Similar Artists table - stores similar artists for each watchlist artist
|
||||
# Supports both Spotify and iTunes IDs for dual-source discovery
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS similar_artists (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source_artist_id TEXT NOT NULL,
|
||||
similar_artist_spotify_id TEXT NOT NULL,
|
||||
similar_artist_spotify_id TEXT,
|
||||
similar_artist_itunes_id TEXT,
|
||||
similar_artist_name TEXT NOT NULL,
|
||||
similarity_rank INTEGER DEFAULT 1,
|
||||
occurrence_count INTEGER DEFAULT 1,
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(source_artist_id, similar_artist_spotify_id)
|
||||
UNIQUE(source_artist_id, similar_artist_name)
|
||||
)
|
||||
""")
|
||||
|
||||
# Discovery Pool table - rotating pool of 1000-2000 tracks for recommendations
|
||||
# Supports both Spotify and iTunes sources for dual-source discovery
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS discovery_pool (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
spotify_track_id TEXT UNIQUE NOT NULL,
|
||||
spotify_album_id TEXT NOT NULL,
|
||||
spotify_artist_id TEXT NOT NULL,
|
||||
spotify_track_id TEXT,
|
||||
spotify_album_id TEXT,
|
||||
spotify_artist_id TEXT,
|
||||
itunes_track_id TEXT,
|
||||
itunes_album_id TEXT,
|
||||
itunes_artist_id TEXT,
|
||||
source TEXT NOT NULL DEFAULT 'spotify',
|
||||
track_name TEXT NOT NULL,
|
||||
artist_name TEXT NOT NULL,
|
||||
album_name TEXT NOT NULL,
|
||||
|
|
@ -479,38 +493,47 @@ class MusicDatabase:
|
|||
release_date TEXT,
|
||||
is_new_release BOOLEAN DEFAULT 0,
|
||||
track_data_json TEXT NOT NULL,
|
||||
added_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
added_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(spotify_track_id, itunes_track_id, source)
|
||||
)
|
||||
""")
|
||||
|
||||
# Recent Releases table - tracks new releases from watchlist artists
|
||||
# Supports both Spotify and iTunes sources for dual-source discovery
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS recent_releases (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
watchlist_artist_id INTEGER NOT NULL,
|
||||
album_spotify_id TEXT NOT NULL,
|
||||
album_spotify_id TEXT,
|
||||
album_itunes_id TEXT,
|
||||
source TEXT NOT NULL DEFAULT 'spotify',
|
||||
album_name TEXT NOT NULL,
|
||||
release_date TEXT NOT NULL,
|
||||
album_cover_url TEXT,
|
||||
track_count INTEGER DEFAULT 0,
|
||||
added_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(watchlist_artist_id, album_spotify_id),
|
||||
UNIQUE(watchlist_artist_id, album_spotify_id, album_itunes_id),
|
||||
FOREIGN KEY (watchlist_artist_id) REFERENCES watchlist_artists (id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
|
||||
# Discovery Recent Albums cache - for discover page recent releases section
|
||||
# Supports both Spotify and iTunes sources for dual-source discovery
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS discovery_recent_albums (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
album_spotify_id TEXT NOT NULL UNIQUE,
|
||||
album_spotify_id TEXT,
|
||||
album_itunes_id TEXT,
|
||||
artist_spotify_id TEXT,
|
||||
artist_itunes_id TEXT,
|
||||
source TEXT NOT NULL DEFAULT 'spotify',
|
||||
album_name TEXT NOT NULL,
|
||||
artist_name TEXT NOT NULL,
|
||||
artist_spotify_id TEXT NOT NULL,
|
||||
album_cover_url TEXT,
|
||||
release_date TEXT NOT NULL,
|
||||
album_type TEXT DEFAULT 'album',
|
||||
cached_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
cached_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(album_spotify_id, album_itunes_id, source)
|
||||
)
|
||||
""")
|
||||
|
||||
|
|
@ -571,13 +594,20 @@ class MusicDatabase:
|
|||
# Create indexes for performance
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_source ON similar_artists (source_artist_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_spotify ON similar_artists (similar_artist_spotify_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_itunes ON similar_artists (similar_artist_itunes_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_occurrence ON similar_artists (occurrence_count)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_similar_artists_name ON similar_artists (similar_artist_name)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_spotify_track ON discovery_pool (spotify_track_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_itunes_track ON discovery_pool (itunes_track_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_artist ON discovery_pool (spotify_artist_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_itunes_artist ON discovery_pool (itunes_artist_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_source ON discovery_pool (source)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_added_date ON discovery_pool (added_date)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_pool_is_new ON discovery_pool (is_new_release)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_watchlist ON recent_releases (watchlist_artist_id)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_date ON recent_releases (release_date)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_recent_releases_source ON recent_releases (source)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_source ON discovery_recent_albums (source)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_date ON discovery_recent_albums (release_date)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_playlists_type ON listenbrainz_playlists (playlist_type)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_listenbrainz_playlists_mbid ON listenbrainz_playlists (playlist_mbid)")
|
||||
|
|
@ -593,6 +623,41 @@ class MusicDatabase:
|
|||
cursor.execute("ALTER TABLE discovery_pool ADD COLUMN artist_genres TEXT")
|
||||
logger.info("Added artist_genres column to discovery_pool table")
|
||||
|
||||
# Migration: Add iTunes columns to discovery_pool for dual-source discovery
|
||||
if 'itunes_track_id' not in discovery_pool_columns:
|
||||
cursor.execute("ALTER TABLE discovery_pool ADD COLUMN itunes_track_id TEXT")
|
||||
cursor.execute("ALTER TABLE discovery_pool ADD COLUMN itunes_album_id TEXT")
|
||||
cursor.execute("ALTER TABLE discovery_pool ADD COLUMN itunes_artist_id TEXT")
|
||||
cursor.execute("ALTER TABLE discovery_pool ADD COLUMN source TEXT DEFAULT 'spotify'")
|
||||
logger.info("Added iTunes columns to discovery_pool table for dual-source discovery")
|
||||
|
||||
# Migration: Add iTunes ID to similar_artists for dual-source discovery
|
||||
cursor.execute("PRAGMA table_info(similar_artists)")
|
||||
similar_artists_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
if 'similar_artist_itunes_id' not in similar_artists_columns:
|
||||
cursor.execute("ALTER TABLE similar_artists ADD COLUMN similar_artist_itunes_id TEXT")
|
||||
logger.info("Added similar_artist_itunes_id column to similar_artists table")
|
||||
|
||||
# Migration: Add iTunes columns to recent_releases for dual-source discovery
|
||||
cursor.execute("PRAGMA table_info(recent_releases)")
|
||||
recent_releases_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
if 'album_itunes_id' not in recent_releases_columns:
|
||||
cursor.execute("ALTER TABLE recent_releases ADD COLUMN album_itunes_id TEXT")
|
||||
cursor.execute("ALTER TABLE recent_releases ADD COLUMN source TEXT DEFAULT 'spotify'")
|
||||
logger.info("Added iTunes columns to recent_releases table for dual-source discovery")
|
||||
|
||||
# Migration: Add iTunes columns to discovery_recent_albums for dual-source discovery
|
||||
cursor.execute("PRAGMA table_info(discovery_recent_albums)")
|
||||
discovery_recent_albums_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
if 'album_itunes_id' not in discovery_recent_albums_columns:
|
||||
cursor.execute("ALTER TABLE discovery_recent_albums ADD COLUMN album_itunes_id TEXT")
|
||||
cursor.execute("ALTER TABLE discovery_recent_albums ADD COLUMN artist_itunes_id TEXT")
|
||||
cursor.execute("ALTER TABLE discovery_recent_albums ADD COLUMN source TEXT DEFAULT 'spotify'")
|
||||
logger.info("Added iTunes columns to discovery_recent_albums table for dual-source discovery")
|
||||
|
||||
logger.info("Discovery tables created successfully")
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -2933,23 +2998,28 @@ class MusicDatabase:
|
|||
|
||||
# === Discovery Feature Methods ===
|
||||
|
||||
def add_or_update_similar_artist(self, source_artist_id: str, similar_artist_spotify_id: str,
|
||||
similar_artist_name: str, similarity_rank: int = 1) -> bool:
|
||||
"""Add or update a similar artist recommendation"""
|
||||
def add_or_update_similar_artist(self, source_artist_id: str, similar_artist_name: str,
|
||||
similar_artist_spotify_id: Optional[str] = None,
|
||||
similar_artist_itunes_id: Optional[str] = None,
|
||||
similarity_rank: int = 1) -> bool:
|
||||
"""Add or update a similar artist recommendation (supports both Spotify and iTunes IDs)"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 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_name, similarity_rank, occurrence_count, last_updated)
|
||||
VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(source_artist_id, similar_artist_spotify_id)
|
||||
(source_artist_id, similar_artist_spotify_id, similar_artist_itunes_id, similar_artist_name, similarity_rank, occurrence_count, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(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_name, similarity_rank))
|
||||
""", (source_artist_id, similar_artist_spotify_id, similar_artist_itunes_id, similar_artist_name, similarity_rank))
|
||||
|
||||
conn.commit()
|
||||
return True
|
||||
|
|
@ -2975,6 +3045,7 @@ class MusicDatabase:
|
|||
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=row['similarity_rank'],
|
||||
occurrence_count=row['occurrence_count'],
|
||||
|
|
@ -3026,13 +3097,14 @@ class MusicDatabase:
|
|||
SELECT
|
||||
MAX(id) as id,
|
||||
MAX(source_artist_id) as source_artist_id,
|
||||
similar_artist_spotify_id,
|
||||
MAX(similar_artist_spotify_id) as similar_artist_spotify_id,
|
||||
MAX(similar_artist_itunes_id) as similar_artist_itunes_id,
|
||||
similar_artist_name,
|
||||
AVG(similarity_rank) as similarity_rank,
|
||||
SUM(occurrence_count) as occurrence_count,
|
||||
MAX(last_updated) as last_updated
|
||||
FROM similar_artists
|
||||
GROUP BY similar_artist_spotify_id, similar_artist_name
|
||||
GROUP BY similar_artist_name
|
||||
ORDER BY occurrence_count DESC, similarity_rank ASC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
|
|
@ -3042,6 +3114,7 @@ class MusicDatabase:
|
|||
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'],
|
||||
|
|
@ -3052,15 +3125,24 @@ class MusicDatabase:
|
|||
logger.error(f"Error getting top similar artists: {e}")
|
||||
return []
|
||||
|
||||
def add_to_discovery_pool(self, track_data: Dict[str, Any]) -> bool:
|
||||
"""Add a track to the discovery pool"""
|
||||
def add_to_discovery_pool(self, track_data: Dict[str, Any], source: str = 'spotify') -> bool:
|
||||
"""Add a track to the discovery pool (supports both Spotify and iTunes sources)"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if track already exists
|
||||
cursor.execute("SELECT COUNT(*) as count FROM discovery_pool WHERE spotify_track_id = ?",
|
||||
(track_data['spotify_track_id'],))
|
||||
# Check if track already exists based on source
|
||||
if source == 'spotify' and track_data.get('spotify_track_id'):
|
||||
cursor.execute("SELECT COUNT(*) as count FROM discovery_pool WHERE spotify_track_id = ? AND source = 'spotify'",
|
||||
(track_data['spotify_track_id'],))
|
||||
elif source == 'itunes' and track_data.get('itunes_track_id'):
|
||||
cursor.execute("SELECT COUNT(*) as count FROM discovery_pool WHERE itunes_track_id = ? AND source = 'itunes'",
|
||||
(track_data['itunes_track_id'],))
|
||||
else:
|
||||
# Fallback check by track name and artist
|
||||
cursor.execute("SELECT COUNT(*) as count FROM discovery_pool WHERE track_name = ? AND artist_name = ? AND source = ?",
|
||||
(track_data['track_name'], track_data['artist_name'], source))
|
||||
|
||||
if cursor.fetchone()['count'] > 0:
|
||||
return True # Already in pool
|
||||
|
||||
|
|
@ -3070,14 +3152,19 @@ class MusicDatabase:
|
|||
|
||||
cursor.execute("""
|
||||
INSERT INTO discovery_pool
|
||||
(spotify_track_id, spotify_album_id, spotify_artist_id, track_name, artist_name,
|
||||
album_name, album_cover_url, duration_ms, popularity, release_date,
|
||||
is_new_release, track_data_json, artist_genres, added_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
(spotify_track_id, spotify_album_id, spotify_artist_id,
|
||||
itunes_track_id, itunes_album_id, itunes_artist_id,
|
||||
source, track_name, artist_name, album_name, album_cover_url,
|
||||
duration_ms, popularity, release_date, is_new_release, track_data_json, artist_genres, added_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
""", (
|
||||
track_data['spotify_track_id'],
|
||||
track_data['spotify_album_id'],
|
||||
track_data['spotify_artist_id'],
|
||||
track_data.get('spotify_track_id'),
|
||||
track_data.get('spotify_album_id'),
|
||||
track_data.get('spotify_artist_id'),
|
||||
track_data.get('itunes_track_id'),
|
||||
track_data.get('itunes_album_id'),
|
||||
track_data.get('itunes_artist_id'),
|
||||
source,
|
||||
track_data['track_name'],
|
||||
track_data['artist_name'],
|
||||
track_data['album_name'],
|
||||
|
|
@ -3124,32 +3211,45 @@ class MusicDatabase:
|
|||
except Exception as e:
|
||||
logger.error(f"Error rotating discovery pool: {e}")
|
||||
|
||||
def get_discovery_pool_tracks(self, limit: int = 100, new_releases_only: bool = False) -> List[DiscoveryTrack]:
|
||||
"""Get tracks from discovery pool"""
|
||||
def get_discovery_pool_tracks(self, limit: int = 100, new_releases_only: bool = False, source: Optional[str] = None) -> List[DiscoveryTrack]:
|
||||
"""Get tracks from discovery pool, optionally filtered by source ('spotify' or 'itunes')"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build query with optional source filter
|
||||
where_clauses = []
|
||||
params = []
|
||||
|
||||
if new_releases_only:
|
||||
cursor.execute("""
|
||||
SELECT * FROM discovery_pool
|
||||
WHERE is_new_release = 1
|
||||
ORDER BY added_date DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT * FROM discovery_pool
|
||||
ORDER BY added_date DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
where_clauses.append("is_new_release = 1")
|
||||
|
||||
if source:
|
||||
where_clauses.append("source = ?")
|
||||
params.append(source)
|
||||
|
||||
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
|
||||
params.append(limit)
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT * FROM discovery_pool
|
||||
{where_sql}
|
||||
ORDER BY added_date DESC
|
||||
LIMIT ?
|
||||
""", params)
|
||||
|
||||
rows = cursor.fetchall()
|
||||
row_keys = rows[0].keys() if rows else []
|
||||
|
||||
return [DiscoveryTrack(
|
||||
id=row['id'],
|
||||
spotify_track_id=row['spotify_track_id'],
|
||||
spotify_album_id=row['spotify_album_id'],
|
||||
spotify_artist_id=row['spotify_artist_id'],
|
||||
itunes_track_id=row['itunes_track_id'] if 'itunes_track_id' in row_keys else None,
|
||||
itunes_album_id=row['itunes_album_id'] if 'itunes_album_id' in row_keys else None,
|
||||
itunes_artist_id=row['itunes_artist_id'] if 'itunes_artist_id' in row_keys else None,
|
||||
source=row['source'] if 'source' in row_keys else 'spotify',
|
||||
track_name=row['track_name'],
|
||||
artist_name=row['artist_name'],
|
||||
album_name=row['album_name'],
|
||||
|
|
@ -3166,21 +3266,25 @@ class MusicDatabase:
|
|||
logger.error(f"Error getting discovery pool tracks: {e}")
|
||||
return []
|
||||
|
||||
def cache_discovery_recent_album(self, album_data: Dict[str, Any]) -> bool:
|
||||
"""Cache a recent album for the discover page (from watchlist or similar artists)"""
|
||||
def cache_discovery_recent_album(self, album_data: Dict[str, Any], source: str = 'spotify') -> bool:
|
||||
"""Cache a recent album for the discover page (supports both Spotify and iTunes sources)"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO discovery_recent_albums
|
||||
(album_spotify_id, album_name, artist_name, artist_spotify_id, album_cover_url, release_date, album_type, cached_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
(album_spotify_id, album_itunes_id, artist_spotify_id, artist_itunes_id, source,
|
||||
album_name, artist_name, album_cover_url, release_date, album_type, cached_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
""", (
|
||||
album_data['album_spotify_id'],
|
||||
album_data.get('album_spotify_id'),
|
||||
album_data.get('album_itunes_id'),
|
||||
album_data.get('artist_spotify_id'),
|
||||
album_data.get('artist_itunes_id'),
|
||||
source,
|
||||
album_data['album_name'],
|
||||
album_data['artist_name'],
|
||||
album_data['artist_spotify_id'],
|
||||
album_data.get('album_cover_url'),
|
||||
album_data['release_date'],
|
||||
album_data.get('album_type', 'album')
|
||||
|
|
@ -3193,27 +3297,40 @@ class MusicDatabase:
|
|||
logger.error(f"Error caching discovery recent album: {e}")
|
||||
return False
|
||||
|
||||
def get_discovery_recent_albums(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""Get cached recent albums for discover page"""
|
||||
def get_discovery_recent_albums(self, limit: int = 10, source: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Get cached recent albums for discover page, optionally filtered by source"""
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT * FROM discovery_recent_albums
|
||||
ORDER BY release_date DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
if source:
|
||||
cursor.execute("""
|
||||
SELECT * FROM discovery_recent_albums
|
||||
WHERE source = ?
|
||||
ORDER BY release_date DESC
|
||||
LIMIT ?
|
||||
""", (source, limit))
|
||||
else:
|
||||
cursor.execute("""
|
||||
SELECT * FROM discovery_recent_albums
|
||||
ORDER BY release_date DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
row_keys = rows[0].keys() if rows else []
|
||||
|
||||
return [{
|
||||
'album_spotify_id': row['album_spotify_id'],
|
||||
'album_itunes_id': row['album_itunes_id'] if 'album_itunes_id' in row_keys else None,
|
||||
'album_name': row['album_name'],
|
||||
'artist_name': row['artist_name'],
|
||||
'artist_spotify_id': row['artist_spotify_id'],
|
||||
'artist_itunes_id': row['artist_itunes_id'] if 'artist_itunes_id' in row_keys else None,
|
||||
'album_cover_url': row['album_cover_url'],
|
||||
'release_date': row['release_date'],
|
||||
'album_type': row['album_type']
|
||||
'album_type': row['album_type'],
|
||||
'source': row['source'] if 'source' in row_keys else 'spotify'
|
||||
} for row in rows]
|
||||
|
||||
except Exception as e:
|
||||
|
|
|
|||
239
web_server.py
239
web_server.py
|
|
@ -5046,19 +5046,73 @@ def get_artist_image(artist_id):
|
|||
def get_artist_discography(artist_id):
|
||||
"""Get an artist's complete discography (albums and singles)"""
|
||||
try:
|
||||
if not spotify_client or not spotify_client.is_authenticated():
|
||||
return jsonify({"error": "Spotify not authenticated"}), 401
|
||||
|
||||
print(f"🎤 Fetching discography for artist: {artist_id}")
|
||||
|
||||
# Get artist's albums and singles (temporarily include appears_on for debugging)
|
||||
albums = spotify_client.get_artist_albums(artist_id, album_type='album,single', limit=50)
|
||||
print(f"📊 Raw albums returned from Spotify: {len(albums)}")
|
||||
# Get optional artist name for fallback searches
|
||||
artist_name = request.args.get('artist_name', '')
|
||||
|
||||
# Determine which source to use
|
||||
spotify_available = spotify_client and spotify_client.is_spotify_authenticated()
|
||||
|
||||
# Import iTunes client for fallback
|
||||
from core.itunes_client import iTunesClient
|
||||
itunes_client = iTunesClient()
|
||||
|
||||
print(f"🎤 Fetching discography for artist: {artist_id} (name: {artist_name}, spotify: {spotify_available})")
|
||||
|
||||
albums = []
|
||||
active_source = None
|
||||
|
||||
# Try to get albums from the appropriate source
|
||||
# Check if the ID looks like Spotify (alphanumeric) or iTunes (numeric only)
|
||||
is_numeric_id = artist_id.isdigit()
|
||||
|
||||
if spotify_available and not is_numeric_id:
|
||||
# Try Spotify first for alphanumeric IDs
|
||||
try:
|
||||
albums = spotify_client.get_artist_albums(artist_id, album_type='album,single', limit=50)
|
||||
if albums:
|
||||
active_source = 'spotify'
|
||||
print(f"📊 Got {len(albums)} albums from Spotify")
|
||||
except Exception as e:
|
||||
print(f"Spotify lookup failed: {e}")
|
||||
|
||||
# Try iTunes if Spotify didn't work or if it's a numeric ID
|
||||
if not albums:
|
||||
try:
|
||||
if is_numeric_id:
|
||||
# It's an iTunes ID, use directly
|
||||
albums = itunes_client.get_artist_albums(artist_id, album_type='album,single', limit=50)
|
||||
if albums:
|
||||
active_source = 'itunes'
|
||||
print(f"📊 Got {len(albums)} albums from iTunes (direct ID)")
|
||||
elif artist_name:
|
||||
# Search iTunes by name
|
||||
print(f"🔄 Trying iTunes search by name: '{artist_name}'")
|
||||
itunes_artists = itunes_client.search_artists(artist_name, limit=5)
|
||||
if itunes_artists:
|
||||
# Find best match
|
||||
best_match = None
|
||||
for artist in itunes_artists:
|
||||
if artist.name.lower() == artist_name.lower():
|
||||
best_match = artist
|
||||
break
|
||||
if not best_match:
|
||||
best_match = itunes_artists[0]
|
||||
|
||||
print(f"✅ Found iTunes artist: {best_match.name} (ID: {best_match.id})")
|
||||
albums = itunes_client.get_artist_albums(best_match.id, album_type='album,single', limit=50)
|
||||
if albums:
|
||||
active_source = 'itunes'
|
||||
print(f"📊 Got {len(albums)} albums from iTunes (name search)")
|
||||
except Exception as e:
|
||||
print(f"iTunes lookup failed: {e}")
|
||||
|
||||
print(f"📊 Total albums returned: {len(albums)} (source: {active_source})")
|
||||
|
||||
if not albums:
|
||||
return jsonify({
|
||||
"albums": [],
|
||||
"singles": []
|
||||
"singles": [],
|
||||
"source": active_source or "unknown"
|
||||
})
|
||||
|
||||
# Separate albums from singles/EPs
|
||||
|
|
@ -5073,23 +5127,18 @@ def get_artist_discography(artist_id):
|
|||
if album.id in seen_albums:
|
||||
continue
|
||||
seen_albums.add(album.id)
|
||||
|
||||
# Debug: Check artist information
|
||||
print(f"🔍 Checking album: {album.name}")
|
||||
|
||||
# Handle artist matching for both Spotify (objects) and iTunes (strings) formats
|
||||
if hasattr(album, 'artists') and album.artists:
|
||||
primary_artist_id = album.artists[0].id if hasattr(album.artists[0], 'id') else None
|
||||
primary_artist_name = album.artists[0].name if hasattr(album.artists[0], 'name') else None
|
||||
print(f" Primary artist: {primary_artist_name} (ID: {primary_artist_id})")
|
||||
print(f" Requested artist ID: {artist_id}")
|
||||
|
||||
# Skip if the primary artist doesn't match our requested artist
|
||||
if primary_artist_id and primary_artist_id != artist_id:
|
||||
print(f"🚫 Skipping '{album.name}' - primary artist mismatch")
|
||||
continue
|
||||
elif not primary_artist_id:
|
||||
print(f"⚠️ No primary artist ID found for '{album.name}' - including anyway")
|
||||
else:
|
||||
print(f"⚠️ No artists found for '{album.name}' - including anyway")
|
||||
first_artist = album.artists[0]
|
||||
# Check if artists are objects (Spotify) or strings (iTunes)
|
||||
if hasattr(first_artist, 'id'):
|
||||
# Spotify format: artist is an object with .id and .name
|
||||
primary_artist_id = first_artist.id
|
||||
# Skip if the primary artist doesn't match our requested artist
|
||||
if primary_artist_id and primary_artist_id != artist_id:
|
||||
continue
|
||||
# iTunes format: artist is a string, can't verify ID match so include all
|
||||
|
||||
album_data = {
|
||||
"id": album.id,
|
||||
|
|
@ -5138,9 +5187,10 @@ def get_artist_discography(artist_id):
|
|||
|
||||
return jsonify({
|
||||
"albums": album_list,
|
||||
"singles": singles_list
|
||||
"singles": singles_list,
|
||||
"source": active_source or "spotify"
|
||||
})
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error fetching artist discography: {e}")
|
||||
import traceback
|
||||
|
|
@ -18178,84 +18228,141 @@ metadata_update_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix=
|
|||
# == DISCOVER PAGE ENDPOINTS ==
|
||||
# ===============================
|
||||
|
||||
def _get_active_discovery_source():
|
||||
"""
|
||||
Determine which music source is active for discovery.
|
||||
Returns 'spotify' if Spotify is authenticated, 'itunes' otherwise.
|
||||
"""
|
||||
if spotify_client and spotify_client.is_spotify_authenticated():
|
||||
return 'spotify'
|
||||
return 'itunes'
|
||||
|
||||
|
||||
@app.route('/api/discover/hero', methods=['GET'])
|
||||
def get_discover_hero():
|
||||
"""Get featured similar artists for hero slideshow"""
|
||||
try:
|
||||
database = get_database()
|
||||
|
||||
# Determine active source
|
||||
active_source = _get_active_discovery_source()
|
||||
print(f"🎵 Discover hero using source: {active_source}")
|
||||
|
||||
# Get top similar artists (by occurrence count) - get 20 for variety
|
||||
similar_artists = database.get_top_similar_artists(limit=20)
|
||||
|
||||
if not similar_artists:
|
||||
return jsonify({"success": True, "artists": []})
|
||||
return jsonify({"success": True, "artists": [], "source": active_source})
|
||||
|
||||
# Filter to artists that have the appropriate ID for the active source
|
||||
valid_artists = []
|
||||
for artist in similar_artists:
|
||||
if active_source == 'spotify' and artist.similar_artist_spotify_id:
|
||||
valid_artists.append(artist)
|
||||
elif active_source == 'itunes' and artist.similar_artist_itunes_id:
|
||||
valid_artists.append(artist)
|
||||
# If we have both IDs, include regardless of source
|
||||
elif artist.similar_artist_spotify_id and artist.similar_artist_itunes_id:
|
||||
valid_artists.append(artist)
|
||||
|
||||
# Shuffle for variety and take top 10
|
||||
import random
|
||||
shuffled = list(similar_artists)
|
||||
shuffled = list(valid_artists)
|
||||
random.shuffle(shuffled)
|
||||
similar_artists = shuffled[:10]
|
||||
|
||||
# Convert to JSON format with Spotify data enrichment
|
||||
# Import iTunes client for fallback
|
||||
from core.itunes_client import iTunesClient
|
||||
itunes_client = iTunesClient()
|
||||
|
||||
# Convert to JSON format with data enrichment from appropriate source
|
||||
hero_artists = []
|
||||
for artist in similar_artists:
|
||||
# Use the ID for the active source, falling back to the other if needed
|
||||
if active_source == 'spotify':
|
||||
artist_id = artist.similar_artist_spotify_id or artist.similar_artist_itunes_id
|
||||
else:
|
||||
artist_id = artist.similar_artist_itunes_id or artist.similar_artist_spotify_id
|
||||
|
||||
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_name": artist.similar_artist_name,
|
||||
"occurrence_count": artist.occurrence_count,
|
||||
"similarity_rank": artist.similarity_rank
|
||||
"similarity_rank": artist.similarity_rank,
|
||||
"source": active_source
|
||||
}
|
||||
|
||||
# Try to get artist image from Spotify
|
||||
# Try to get artist image from the active source
|
||||
try:
|
||||
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['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)
|
||||
except:
|
||||
pass
|
||||
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['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:
|
||||
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}")
|
||||
|
||||
hero_artists.append(artist_data)
|
||||
|
||||
return jsonify({"success": True, "artists": hero_artists})
|
||||
return jsonify({"success": True, "artists": hero_artists, "source": active_source})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting discover hero: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/discover/recent-releases', methods=['GET'])
|
||||
def get_discover_recent_releases():
|
||||
"""Get cached recent albums from watchlist and similar artists"""
|
||||
try:
|
||||
database = get_database()
|
||||
|
||||
# Get cached recent albums (max 20)
|
||||
albums = database.get_discovery_recent_albums(limit=20)
|
||||
# Determine active source
|
||||
active_source = _get_active_discovery_source()
|
||||
|
||||
return jsonify({"success": True, "albums": albums})
|
||||
# Get cached recent albums filtered by source (max 20)
|
||||
albums = database.get_discovery_recent_albums(limit=20, source=active_source)
|
||||
|
||||
return jsonify({"success": True, "albums": albums, "source": active_source})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting recent releases: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/discover/release-radar', methods=['GET'])
|
||||
def get_discover_release_radar():
|
||||
"""Get release radar playlist - curated selection that stays consistent until next update"""
|
||||
try:
|
||||
database = get_database()
|
||||
|
||||
if not spotify_client or not spotify_client.is_authenticated():
|
||||
return jsonify({"success": True, "tracks": []})
|
||||
# Determine active source - release radar works with any source now
|
||||
active_source = _get_active_discovery_source()
|
||||
|
||||
# Try to get curated playlist first
|
||||
curated_track_ids = database.get_curated_playlist('release_radar')
|
||||
|
||||
if curated_track_ids:
|
||||
# Use curated selection - fetch track data from discovery pool (same as Discovery Weekly)
|
||||
discovery_tracks = database.get_discovery_pool_tracks(limit=5000, new_releases_only=False)
|
||||
tracks_by_id = {track.spotify_track_id: track for track in discovery_tracks}
|
||||
# Use curated selection - fetch track data from discovery pool filtered by source
|
||||
discovery_tracks = database.get_discovery_pool_tracks(limit=5000, new_releases_only=False, source=active_source)
|
||||
|
||||
# Build lookup dict with source-appropriate IDs
|
||||
tracks_by_id = {}
|
||||
for track in discovery_tracks:
|
||||
if active_source == 'spotify' and track.spotify_track_id:
|
||||
tracks_by_id[track.spotify_track_id] = track
|
||||
elif active_source == 'itunes' and track.itunes_track_id:
|
||||
tracks_by_id[track.itunes_track_id] = track
|
||||
|
||||
selected_tracks = []
|
||||
for track_id in curated_track_ids:
|
||||
|
|
@ -18271,19 +18378,22 @@ def get_discover_release_radar():
|
|||
track_data = None
|
||||
|
||||
selected_tracks.append({
|
||||
"track_id": track.spotify_track_id or track.itunes_track_id,
|
||||
"spotify_track_id": track.spotify_track_id,
|
||||
"itunes_track_id": track.itunes_track_id,
|
||||
"track_name": track.track_name,
|
||||
"artist_name": track.artist_name,
|
||||
"album_name": track.album_name,
|
||||
"album_cover_url": track.album_cover_url,
|
||||
"duration_ms": track.duration_ms,
|
||||
"track_data_json": track_data # Now properly parsed
|
||||
"track_data_json": track_data,
|
||||
"source": track.source
|
||||
})
|
||||
|
||||
return jsonify({"success": True, "tracks": selected_tracks})
|
||||
return jsonify({"success": True, "tracks": selected_tracks, "source": active_source})
|
||||
|
||||
# Fallback: no curated playlist exists (shouldn't happen after first scan)
|
||||
return jsonify({"success": True, "tracks": []})
|
||||
return jsonify({"success": True, "tracks": [], "source": active_source})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting release radar: {e}")
|
||||
|
|
@ -18297,13 +18407,23 @@ def get_discover_weekly():
|
|||
try:
|
||||
database = get_database()
|
||||
|
||||
# Determine active source
|
||||
active_source = _get_active_discovery_source()
|
||||
|
||||
# Try to get curated playlist first
|
||||
curated_track_ids = database.get_curated_playlist('discovery_weekly')
|
||||
|
||||
if curated_track_ids:
|
||||
# Use curated selection - fetch track data from discovery pool
|
||||
discovery_tracks = database.get_discovery_pool_tracks(limit=5000, new_releases_only=False)
|
||||
tracks_by_id = {track.spotify_track_id: track for track in discovery_tracks}
|
||||
# Use curated selection - fetch track data from discovery pool filtered by source
|
||||
discovery_tracks = database.get_discovery_pool_tracks(limit=5000, new_releases_only=False, source=active_source)
|
||||
|
||||
# Build lookup dict with source-appropriate IDs
|
||||
tracks_by_id = {}
|
||||
for track in discovery_tracks:
|
||||
if active_source == 'spotify' and track.spotify_track_id:
|
||||
tracks_by_id[track.spotify_track_id] = track
|
||||
elif active_source == 'itunes' and track.itunes_track_id:
|
||||
tracks_by_id[track.itunes_track_id] = track
|
||||
|
||||
selected_tracks = []
|
||||
for track_id in curated_track_ids:
|
||||
|
|
@ -18319,19 +18439,22 @@ def get_discover_weekly():
|
|||
track_data = None
|
||||
|
||||
selected_tracks.append({
|
||||
"track_id": track.spotify_track_id or track.itunes_track_id,
|
||||
"spotify_track_id": track.spotify_track_id,
|
||||
"itunes_track_id": track.itunes_track_id,
|
||||
"track_name": track.track_name,
|
||||
"artist_name": track.artist_name,
|
||||
"album_name": track.album_name,
|
||||
"album_cover_url": track.album_cover_url,
|
||||
"duration_ms": track.duration_ms,
|
||||
"track_data_json": track_data # Now properly parsed
|
||||
"track_data_json": track_data,
|
||||
"source": track.source
|
||||
})
|
||||
|
||||
return jsonify({"success": True, "tracks": selected_tracks})
|
||||
return jsonify({"success": True, "tracks": selected_tracks, "source": active_source})
|
||||
|
||||
# Fallback: no curated playlist exists (shouldn't happen after first scan)
|
||||
return jsonify({"success": True, "tracks": []})
|
||||
return jsonify({"success": True, "tracks": [], "source": active_source})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting discovery weekly: {e}")
|
||||
|
|
|
|||
|
|
@ -20000,15 +20000,17 @@ async function selectArtistForDetail(artist) {
|
|||
// Update artist info in header
|
||||
updateArtistDetailHeader(artist);
|
||||
|
||||
// Load discography
|
||||
await loadArtistDiscography(artist.id);
|
||||
// Load discography (pass artist name for cross-source fallback)
|
||||
await loadArtistDiscography(artist.id, artist.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load artist's discography from Spotify
|
||||
* Load artist's discography from Spotify or iTunes
|
||||
* @param {string} artistId - Artist ID (Spotify or iTunes format)
|
||||
* @param {string} [artistName] - Optional artist name for fallback searches
|
||||
*/
|
||||
async function loadArtistDiscography(artistId) {
|
||||
console.log(`💿 Loading discography for artist: ${artistId}`);
|
||||
async function loadArtistDiscography(artistId, artistName = null) {
|
||||
console.log(`💿 Loading discography for artist: ${artistId} (name: ${artistName})`);
|
||||
|
||||
// Check cache first
|
||||
if (artistsPageState.cache.discography[artistId]) {
|
||||
|
|
@ -20030,8 +20032,14 @@ async function loadArtistDiscography(artistId) {
|
|||
// Show loading states
|
||||
showDiscographyLoading();
|
||||
|
||||
// Build URL with optional artist name for fallback
|
||||
let url = `/api/artist/${artistId}/discography`;
|
||||
if (artistName) {
|
||||
url += `?artist_name=${encodeURIComponent(artistName)}`;
|
||||
}
|
||||
|
||||
// Call the real API endpoint
|
||||
const response = await fetch(`/api/artist/${artistId}/discography`);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue