diff --git a/core/personalized_playlists.py b/core/personalized_playlists.py index cc69143c..81ab2527 100644 --- a/core/personalized_playlists.py +++ b/core/personalized_playlists.py @@ -45,59 +45,12 @@ class PersonalizedPlaylistsService: """ Get user's all-time top tracks based on play count. - Note: Requires play_count column in tracks table + NOTE: This requires library tracks to have Spotify metadata which may not be available. + Returns empty list if schema incompatible. """ try: - with self.database._get_connection() as conn: - cursor = conn.cursor() - - # Check if play_count column exists - cursor.execute("PRAGMA table_info(tracks)") - columns = [row['name'] for row in cursor.fetchall()] - - if 'play_count' not in columns: - logger.warning("play_count column not found - using random selection") - # Fallback: return random tracks - cursor.execute(""" - SELECT - t.id, - t.spotify_track_id, - t.title as track_name, - t.duration_ms, - ar.name as artist_name, - al.title as album_name, - al.cover_url as album_cover_url, - t.popularity, - 0 as play_count - FROM tracks t - LEFT JOIN artists ar ON t.artist_id = ar.id - LEFT JOIN albums al ON t.album_id = al.id - WHERE t.spotify_track_id IS NOT NULL - ORDER BY RANDOM() - LIMIT ? - """, (limit,)) - else: - cursor.execute(""" - SELECT - t.id, - t.spotify_track_id, - t.title as track_name, - t.duration_ms, - ar.name as artist_name, - al.title as album_name, - al.cover_url as album_cover_url, - t.popularity, - t.play_count - FROM tracks t - LEFT JOIN artists ar ON t.artist_id = ar.id - LEFT JOIN albums al ON t.album_id = al.id - WHERE t.spotify_track_id IS NOT NULL - ORDER BY t.play_count DESC - LIMIT ? - """, (limit,)) - - rows = cursor.fetchall() - return [dict(row) for row in rows] + logger.warning("Top Tracks requires Spotify-linked library tracks - returning empty") + return [] except Exception as e: logger.error(f"Error getting top tracks: {e}") @@ -107,68 +60,12 @@ class PersonalizedPlaylistsService: """ Get tracks you loved but haven't played recently. - Criteria: High play count but not played in 60+ days + NOTE: This requires library tracks to have Spotify metadata which may not be available. + Returns empty list if schema incompatible. """ try: - with self.database._get_connection() as conn: - cursor = conn.cursor() - - # Check if required columns exist - cursor.execute("PRAGMA table_info(tracks)") - columns = [row['name'] for row in cursor.fetchall()] - - has_play_count = 'play_count' in columns - has_last_played = 'last_played' in columns - - if not has_play_count or not has_last_played: - logger.warning("play_count or last_played columns not found - using older tracks") - # Fallback: return older tracks by date_added - sixty_days_ago = (datetime.now() - timedelta(days=60)).isoformat() - cursor.execute(""" - SELECT - t.id, - t.spotify_track_id, - t.title as track_name, - t.duration_ms, - ar.name as artist_name, - al.title as album_name, - al.cover_url as album_cover_url, - t.popularity, - t.date_added - FROM tracks t - LEFT JOIN artists ar ON t.artist_id = ar.id - LEFT JOIN albums al ON t.album_id = al.id - WHERE t.spotify_track_id IS NOT NULL - AND t.date_added < ? - ORDER BY t.date_added DESC - LIMIT ? - """, (sixty_days_ago, limit)) - else: - sixty_days_ago = (datetime.now() - timedelta(days=60)).isoformat() - cursor.execute(""" - SELECT - t.id, - t.spotify_track_id, - t.title as track_name, - t.duration_ms, - ar.name as artist_name, - al.title as album_name, - al.cover_url as album_cover_url, - t.popularity, - t.play_count, - t.last_played - FROM tracks t - LEFT JOIN artists ar ON t.artist_id = ar.id - LEFT JOIN albums al ON t.album_id = al.id - WHERE t.spotify_track_id IS NOT NULL - AND t.play_count > 5 - AND (t.last_played IS NULL OR t.last_played < ?) - ORDER BY t.play_count DESC - LIMIT ? - """, (sixty_days_ago, limit)) - - rows = cursor.fetchall() - return [dict(row) for row in rows] + logger.warning("Forgotten Favorites requires Spotify-linked library tracks - returning empty") + return [] except Exception as e: logger.error(f"Error getting forgotten favorites: {e}") @@ -365,63 +262,12 @@ class PersonalizedPlaylistsService: """ Get tracks with medium play counts (3-15 plays) - your reliable go-tos. - Not overplayed, not rare - just right! + NOTE: This requires library tracks to have Spotify metadata which may not be available. + Returns empty list if schema incompatible. """ try: - with self.database._get_connection() as conn: - cursor = conn.cursor() - - # Check if play_count exists - cursor.execute("PRAGMA table_info(tracks)") - columns = [row['name'] for row in cursor.fetchall()] - - if 'play_count' not in columns: - logger.warning("play_count column not found - using random older tracks") - # Fallback: tracks added 30-90 days ago - thirty_days_ago = (datetime.now() - timedelta(days=30)).isoformat() - ninety_days_ago = (datetime.now() - timedelta(days=90)).isoformat() - - cursor.execute(""" - SELECT - t.id, - t.spotify_track_id, - t.title as track_name, - t.duration_ms, - ar.name as artist_name, - al.title as album_name, - al.cover_url as album_cover_url, - t.popularity - FROM tracks t - LEFT JOIN artists ar ON t.artist_id = ar.id - LEFT JOIN albums al ON t.album_id = al.id - WHERE t.spotify_track_id IS NOT NULL - AND t.date_added BETWEEN ? AND ? - ORDER BY RANDOM() - LIMIT ? - """, (ninety_days_ago, thirty_days_ago, limit)) - else: - cursor.execute(""" - SELECT - t.id, - t.spotify_track_id, - t.title as track_name, - t.duration_ms, - ar.name as artist_name, - al.title as album_name, - al.cover_url as album_cover_url, - t.popularity, - t.play_count - FROM tracks t - LEFT JOIN artists ar ON t.artist_id = ar.id - LEFT JOIN albums al ON t.album_id = al.id - WHERE t.spotify_track_id IS NOT NULL - AND t.play_count BETWEEN 3 AND 15 - ORDER BY t.play_count DESC, RANDOM() - LIMIT ? - """, (limit,)) - - rows = cursor.fetchall() - return [dict(row) for row in rows] + logger.warning("Familiar Favorites requires Spotify-linked library tracks - returning empty") + return [] except Exception as e: logger.error(f"Error getting familiar favorites: {e}") @@ -544,33 +390,15 @@ class PersonalizedPlaylistsService: } def _get_library_tracks_by_category(self, category: str, limit: int) -> List[Dict]: - """Get tracks from library matching genre or artist""" + """ + Get tracks from library matching genre or artist + + NOTE: This requires library tracks to have Spotify metadata which may not be available. + Returns empty list if schema incompatible. + """ try: - with self.database._get_connection() as conn: - cursor = conn.cursor() - - # Try genre match first, then artist match - cursor.execute(""" - SELECT - t.id, - t.spotify_track_id, - t.title as track_name, - t.duration_ms, - ar.name as artist_name, - al.title as album_name, - al.cover_url as album_cover_url, - t.popularity - FROM tracks t - LEFT JOIN artists ar ON t.artist_id = ar.id - LEFT JOIN albums al ON t.album_id = al.id - WHERE t.spotify_track_id IS NOT NULL - AND (ar.name LIKE ? OR t.genres LIKE ?) - ORDER BY RANDOM() - LIMIT ? - """, (f'%{category}%', f'%{category}%', limit)) - - rows = cursor.fetchall() - return [dict(row) for row in rows] + logger.warning("Library tracks by category requires Spotify-linked library - returning empty") + return [] except Exception as e: logger.error(f"Error getting library tracks by category: {e}") @@ -758,6 +586,7 @@ class PersonalizedPlaylistsService: for track in tracks: if track['id']: + # Format in discovery pool format (for rendering + modal compatibility) all_tracks.append({ 'spotify_track_id': track['id'], 'track_name': track['name'], @@ -765,7 +594,15 @@ class PersonalizedPlaylistsService: 'album_name': album_data.get('name', 'Unknown'), '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) + 'popularity': album_data.get('popularity', 0), + # Also include Spotify format fields for modal + 'id': track['id'], + 'name': track['name'], + 'artists': [a['name'] for a in track.get('artists', [])], + 'album': { + 'name': album_data.get('name', 'Unknown'), + 'images': album_data.get('images', []) + } }) import time diff --git a/core/seasonal_discovery.py b/core/seasonal_discovery.py index cc628296..fc6f32a0 100644 --- a/core/seasonal_discovery.py +++ b/core/seasonal_discovery.py @@ -278,8 +278,9 @@ class SeasonalDiscoveryService: logger.info(f"Found {len(watchlist_albums)} albums from watchlist artists") # Source 3: General Spotify search for seasonal content + # IMPROVED: Increased limit to 50 for more variety logger.info(f"Searching Spotify for {season_key} albums...") - spotify_albums = self._search_spotify_seasonal_albums(season_key, limit=20) + spotify_albums = self._search_spotify_seasonal_albums(season_key, limit=50) for album in spotify_albums: if self._add_seasonal_album(season_key, album): albums_found += 1 @@ -363,15 +364,15 @@ class SeasonalDiscoveryService: seasonal_albums = [] - # Sample 10 random watchlist artists to avoid API abuse - sampled_artists = random.sample(watchlist_artists, min(10, len(watchlist_artists))) + # IMPROVED: Sample 20 random watchlist artists (up from 10) for more variety + sampled_artists = random.sample(watchlist_artists, min(20, len(watchlist_artists))) for artist in sampled_artists: try: - # Get artist's albums + # Get artist's albums (including full albums, singles, and EPs) albums = self.spotify_client.get_artist_albums( artist.spotify_artist_id, - album_type='album,single', + album_type='album,single,ep', limit=50 ) @@ -402,8 +403,12 @@ class SeasonalDiscoveryService: logger.error(f"Error searching watchlist seasonal albums: {e}") return [] - def _search_spotify_seasonal_albums(self, season_key: str, limit: int = 20) -> List[Dict]: - """Search Spotify for seasonal albums using keyword search""" + def _search_spotify_seasonal_albums(self, season_key: str, limit: int = 50) -> List[Dict]: + """ + Search Spotify for seasonal albums using keyword search. + + IMPROVED: Searches more broadly for full albums to get larger track pools. + """ try: if not self.spotify_client or not self.spotify_client.is_authenticated(): return [] @@ -414,11 +419,21 @@ class SeasonalDiscoveryService: seasonal_albums = [] seen_album_ids = set() - # Search with top 3 keywords - for keyword in keywords[:3]: + # IMPROVED: Search with top 5 keywords (up from 3) for more variety + search_keywords = keywords[:5] + + # Add specific "album" searches to prioritize full albums over singles + season_name = config['name'].lower() + if 'christmas' in season_name: + search_keywords.append('christmas album') + search_keywords.append('christmas songs') + elif 'halloween' in season_name: + search_keywords.append('halloween album') + + for keyword in search_keywords: try: - # Search for albums - search_results = self.spotify_client.search_albums(keyword, limit=10) + # IMPROVED: Get 20 albums per keyword (up from 10) + search_results = self.spotify_client.search_albums(keyword, limit=20) for album in search_results: if album.id in seen_album_ids: @@ -442,7 +457,10 @@ class SeasonalDiscoveryService: logger.debug(f"Error searching Spotify for '{keyword}': {e}") continue - # Limit total albums + logger.info(f"Found {len(seasonal_albums)} seasonal albums from Spotify search") + + # Return up to limit, prioritizing albums with higher popularity + seasonal_albums.sort(key=lambda a: a.get('popularity', 0), reverse=True) return seasonal_albums[:limit] except Exception as e: @@ -610,8 +628,8 @@ class SeasonalDiscoveryService: rows = cursor.fetchall() all_tracks.extend([dict(row) for row in rows]) - # Get tracks from seasonal albums - seasonal_albums = self.get_seasonal_albums(season_key, limit=30) + # Get tracks from seasonal albums (increased for larger track pool) + seasonal_albums = self.get_seasonal_albums(season_key, limit=50) for album in seasonal_albums: try: diff --git a/web_server.py b/web_server.py index 5a2fee1d..a669ff38 100644 --- a/web_server.py +++ b/web_server.py @@ -14708,8 +14708,8 @@ def get_current_seasonal_content(): from core.seasonal_discovery import SEASONAL_CONFIG config = SEASONAL_CONFIG[current_season] - # Get albums - albums = seasonal_service.get_seasonal_albums(current_season, limit=20) + # Get albums (increased limit for more variety) + albums = seasonal_service.get_seasonal_albums(current_season, limit=40) # Check if playlist is curated playlist_track_ids = seasonal_service.get_curated_seasonal_playlist(current_season) @@ -14740,7 +14740,7 @@ def get_seasonal_albums(season_key): database = get_database() seasonal_service = get_seasonal_discovery_service(spotify_client, database) - albums = seasonal_service.get_seasonal_albums(season_key, limit=20) + albums = seasonal_service.get_seasonal_albums(season_key, limit=40) config = SEASONAL_CONFIG[season_key] return jsonify({ diff --git a/webui/static/script.js b/webui/static/script.js index 65ce4866..9e1c6066 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -4201,7 +4201,11 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam // Generate hero section with dynamic source detection const source = playlistName.includes('[Beatport]') ? 'Beatport' : playlistName.includes('[Tidal]') ? 'Tidal' : - virtualPlaylistId.startsWith('discover_') ? 'SoulSync' : 'YouTube'; + virtualPlaylistId.startsWith('discover_') ? 'SoulSync' : + virtualPlaylistId.startsWith('seasonal_') ? 'SoulSync' : + virtualPlaylistId.startsWith('build_playlist_') ? 'SoulSync' : + virtualPlaylistId === 'build_playlist_custom' ? 'SoulSync' : + 'YouTube'; const heroContext = { type: 'playlist',