discover progress

This commit is contained in:
Broque Thomas 2025-11-16 20:56:08 -08:00
parent 86fe7dde1f
commit 08a4decda8
4 changed files with 70 additions and 211 deletions

View file

@ -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

View file

@ -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:

View file

@ -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({

View file

@ -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',