discover progress
This commit is contained in:
parent
86fe7dde1f
commit
08a4decda8
4 changed files with 70 additions and 211 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue