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.
|
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:
|
try:
|
||||||
with self.database._get_connection() as conn:
|
logger.warning("Top Tracks requires Spotify-linked library tracks - returning empty")
|
||||||
cursor = conn.cursor()
|
return []
|
||||||
|
|
||||||
# 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]
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting top tracks: {e}")
|
logger.error(f"Error getting top tracks: {e}")
|
||||||
|
|
@ -107,68 +60,12 @@ class PersonalizedPlaylistsService:
|
||||||
"""
|
"""
|
||||||
Get tracks you loved but haven't played recently.
|
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:
|
try:
|
||||||
with self.database._get_connection() as conn:
|
logger.warning("Forgotten Favorites requires Spotify-linked library tracks - returning empty")
|
||||||
cursor = conn.cursor()
|
return []
|
||||||
|
|
||||||
# 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]
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting forgotten favorites: {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.
|
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:
|
try:
|
||||||
with self.database._get_connection() as conn:
|
logger.warning("Familiar Favorites requires Spotify-linked library tracks - returning empty")
|
||||||
cursor = conn.cursor()
|
return []
|
||||||
|
|
||||||
# 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]
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting familiar favorites: {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]:
|
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:
|
try:
|
||||||
with self.database._get_connection() as conn:
|
logger.warning("Library tracks by category requires Spotify-linked library - returning empty")
|
||||||
cursor = conn.cursor()
|
return []
|
||||||
|
|
||||||
# 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]
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting library tracks by category: {e}")
|
logger.error(f"Error getting library tracks by category: {e}")
|
||||||
|
|
@ -758,6 +586,7 @@ class PersonalizedPlaylistsService:
|
||||||
|
|
||||||
for track in tracks:
|
for track in tracks:
|
||||||
if track['id']:
|
if track['id']:
|
||||||
|
# Format in discovery pool format (for rendering + modal compatibility)
|
||||||
all_tracks.append({
|
all_tracks.append({
|
||||||
'spotify_track_id': track['id'],
|
'spotify_track_id': track['id'],
|
||||||
'track_name': track['name'],
|
'track_name': track['name'],
|
||||||
|
|
@ -765,7 +594,15 @@ class PersonalizedPlaylistsService:
|
||||||
'album_name': album_data.get('name', 'Unknown'),
|
'album_name': album_data.get('name', 'Unknown'),
|
||||||
'album_cover_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None,
|
'album_cover_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None,
|
||||||
'duration_ms': track.get('duration_ms', 0),
|
'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
|
import time
|
||||||
|
|
|
||||||
|
|
@ -278,8 +278,9 @@ class SeasonalDiscoveryService:
|
||||||
logger.info(f"Found {len(watchlist_albums)} albums from watchlist artists")
|
logger.info(f"Found {len(watchlist_albums)} albums from watchlist artists")
|
||||||
|
|
||||||
# Source 3: General Spotify search for seasonal content
|
# 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...")
|
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:
|
for album in spotify_albums:
|
||||||
if self._add_seasonal_album(season_key, album):
|
if self._add_seasonal_album(season_key, album):
|
||||||
albums_found += 1
|
albums_found += 1
|
||||||
|
|
@ -363,15 +364,15 @@ class SeasonalDiscoveryService:
|
||||||
|
|
||||||
seasonal_albums = []
|
seasonal_albums = []
|
||||||
|
|
||||||
# Sample 10 random watchlist artists to avoid API abuse
|
# IMPROVED: Sample 20 random watchlist artists (up from 10) for more variety
|
||||||
sampled_artists = random.sample(watchlist_artists, min(10, len(watchlist_artists)))
|
sampled_artists = random.sample(watchlist_artists, min(20, len(watchlist_artists)))
|
||||||
|
|
||||||
for artist in sampled_artists:
|
for artist in sampled_artists:
|
||||||
try:
|
try:
|
||||||
# Get artist's albums
|
# Get artist's albums (including full albums, singles, and EPs)
|
||||||
albums = self.spotify_client.get_artist_albums(
|
albums = self.spotify_client.get_artist_albums(
|
||||||
artist.spotify_artist_id,
|
artist.spotify_artist_id,
|
||||||
album_type='album,single',
|
album_type='album,single,ep',
|
||||||
limit=50
|
limit=50
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -402,8 +403,12 @@ class SeasonalDiscoveryService:
|
||||||
logger.error(f"Error searching watchlist seasonal albums: {e}")
|
logger.error(f"Error searching watchlist seasonal albums: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _search_spotify_seasonal_albums(self, season_key: str, limit: int = 20) -> List[Dict]:
|
def _search_spotify_seasonal_albums(self, season_key: str, limit: int = 50) -> List[Dict]:
|
||||||
"""Search Spotify for seasonal albums using keyword search"""
|
"""
|
||||||
|
Search Spotify for seasonal albums using keyword search.
|
||||||
|
|
||||||
|
IMPROVED: Searches more broadly for full albums to get larger track pools.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
if not self.spotify_client or not self.spotify_client.is_authenticated():
|
if not self.spotify_client or not self.spotify_client.is_authenticated():
|
||||||
return []
|
return []
|
||||||
|
|
@ -414,11 +419,21 @@ class SeasonalDiscoveryService:
|
||||||
seasonal_albums = []
|
seasonal_albums = []
|
||||||
seen_album_ids = set()
|
seen_album_ids = set()
|
||||||
|
|
||||||
# Search with top 3 keywords
|
# IMPROVED: Search with top 5 keywords (up from 3) for more variety
|
||||||
for keyword in keywords[:3]:
|
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:
|
try:
|
||||||
# Search for albums
|
# IMPROVED: Get 20 albums per keyword (up from 10)
|
||||||
search_results = self.spotify_client.search_albums(keyword, limit=10)
|
search_results = self.spotify_client.search_albums(keyword, limit=20)
|
||||||
|
|
||||||
for album in search_results:
|
for album in search_results:
|
||||||
if album.id in seen_album_ids:
|
if album.id in seen_album_ids:
|
||||||
|
|
@ -442,7 +457,10 @@ class SeasonalDiscoveryService:
|
||||||
logger.debug(f"Error searching Spotify for '{keyword}': {e}")
|
logger.debug(f"Error searching Spotify for '{keyword}': {e}")
|
||||||
continue
|
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]
|
return seasonal_albums[:limit]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -610,8 +628,8 @@ class SeasonalDiscoveryService:
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
all_tracks.extend([dict(row) for row in rows])
|
all_tracks.extend([dict(row) for row in rows])
|
||||||
|
|
||||||
# Get tracks from seasonal albums
|
# Get tracks from seasonal albums (increased for larger track pool)
|
||||||
seasonal_albums = self.get_seasonal_albums(season_key, limit=30)
|
seasonal_albums = self.get_seasonal_albums(season_key, limit=50)
|
||||||
|
|
||||||
for album in seasonal_albums:
|
for album in seasonal_albums:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -14708,8 +14708,8 @@ def get_current_seasonal_content():
|
||||||
from core.seasonal_discovery import SEASONAL_CONFIG
|
from core.seasonal_discovery import SEASONAL_CONFIG
|
||||||
config = SEASONAL_CONFIG[current_season]
|
config = SEASONAL_CONFIG[current_season]
|
||||||
|
|
||||||
# Get albums
|
# Get albums (increased limit for more variety)
|
||||||
albums = seasonal_service.get_seasonal_albums(current_season, limit=20)
|
albums = seasonal_service.get_seasonal_albums(current_season, limit=40)
|
||||||
|
|
||||||
# Check if playlist is curated
|
# Check if playlist is curated
|
||||||
playlist_track_ids = seasonal_service.get_curated_seasonal_playlist(current_season)
|
playlist_track_ids = seasonal_service.get_curated_seasonal_playlist(current_season)
|
||||||
|
|
@ -14740,7 +14740,7 @@ def get_seasonal_albums(season_key):
|
||||||
database = get_database()
|
database = get_database()
|
||||||
seasonal_service = get_seasonal_discovery_service(spotify_client, 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]
|
config = SEASONAL_CONFIG[season_key]
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|
|
||||||
|
|
@ -4201,7 +4201,11 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
|
||||||
// Generate hero section with dynamic source detection
|
// Generate hero section with dynamic source detection
|
||||||
const source = playlistName.includes('[Beatport]') ? 'Beatport' :
|
const source = playlistName.includes('[Beatport]') ? 'Beatport' :
|
||||||
playlistName.includes('[Tidal]') ? 'Tidal' :
|
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 = {
|
const heroContext = {
|
||||||
type: 'playlist',
|
type: 'playlist',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue