discover page

pull/80/head
Broque Thomas 6 months ago
parent bd4e0d131e
commit 68a15071f1

@ -0,0 +1,543 @@
#!/usr/bin/env python3
"""
Personalized Playlists Service - Creates Spotify-quality personalized playlists
from user's library and discovery pool
"""
from typing import List, Dict, Any, Optional, Tuple
from datetime import datetime, timedelta
from collections import Counter
import random
from utils.logging_config import get_logger
logger = get_logger("personalized_playlists")
class PersonalizedPlaylistsService:
"""Service for generating personalized playlists from library and discovery pool"""
def __init__(self, database, spotify_client=None):
self.database = database
self.spotify_client = spotify_client
# ========================================
# LIBRARY-BASED PLAYLISTS
# ========================================
def get_recently_added(self, limit: int = 50) -> List[Dict]:
"""
Get recently added tracks from library.
Returns tracks ordered by date_added DESC
"""
try:
with self.database._get_connection() as conn:
cursor = conn.cursor()
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
ORDER BY t.date_added DESC
LIMIT ?
""", (limit,))
rows = cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f"Error getting recently added tracks: {e}")
return []
def get_top_tracks(self, limit: int = 50) -> List[Dict]:
"""
Get user's all-time top tracks based on play count.
Note: Requires play_count column in tracks table
"""
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]
except Exception as e:
logger.error(f"Error getting top tracks: {e}")
return []
def get_forgotten_favorites(self, limit: int = 50) -> List[Dict]:
"""
Get tracks you loved but haven't played recently.
Criteria: High play count but not played in 60+ days
"""
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]
except Exception as e:
logger.error(f"Error getting forgotten favorites: {e}")
return []
def get_decade_playlist(self, decade: int, limit: int = 100) -> List[Dict]:
"""
Get tracks from a specific decade.
Args:
decade: Decade year (e.g., 2020 for 2020s, 2010 for 2010s)
limit: Maximum tracks to return
"""
try:
start_year = decade
end_year = decade + 9
with self.database._get_connection() as conn:
cursor = conn.cursor()
# Check if release_year column exists
cursor.execute("PRAGMA table_info(tracks)")
columns = [row['name'] for row in cursor.fetchall()]
if 'release_year' in columns:
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.release_year
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.release_year BETWEEN ? AND ?
ORDER BY t.popularity DESC
LIMIT ?
""", (start_year, end_year, limit))
else:
# Try to extract year from album release_date
logger.warning("release_year column not found - using album release_date")
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,
al.release_date
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 al.release_date IS NOT NULL
AND CAST(SUBSTR(al.release_date, 1, 4) AS INTEGER) BETWEEN ? AND ?
ORDER BY t.popularity DESC
LIMIT ?
""", (start_year, end_year, limit))
rows = cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f"Error getting decade playlist for {decade}s: {e}")
return []
# ========================================
# DISCOVERY POOL PLAYLISTS
# ========================================
def get_popular_picks(self, limit: int = 50) -> List[Dict]:
"""Get high popularity tracks from discovery pool"""
try:
with self.database._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT
spotify_track_id,
track_name,
artist_name,
album_name,
album_cover_url,
duration_ms,
popularity
FROM discovery_pool
WHERE popularity >= 60
ORDER BY popularity DESC, RANDOM()
LIMIT ?
""", (limit,))
rows = cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f"Error getting popular picks: {e}")
return []
def get_hidden_gems(self, limit: int = 50) -> List[Dict]:
"""Get low popularity (underground/indie) tracks from discovery pool"""
try:
with self.database._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT
spotify_track_id,
track_name,
artist_name,
album_name,
album_cover_url,
duration_ms,
popularity
FROM discovery_pool
WHERE popularity < 40
ORDER BY RANDOM()
LIMIT ?
""", (limit,))
rows = cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f"Error getting hidden gems: {e}")
return []
# ========================================
# DAILY MIX (HYBRID PLAYLISTS)
# ========================================
def get_top_genres_from_library(self, limit: int = 5) -> List[Tuple[str, int]]:
"""
Get top genres from user's library by track count.
Returns: List of (genre_name, track_count) tuples
"""
try:
# Get all genres from library tracks
with self.database._get_connection() as conn:
cursor = conn.cursor()
# Try to get genres from tracks or albums
cursor.execute("PRAGMA table_info(tracks)")
columns = [row['name'] for row in cursor.fetchall()]
if 'genres' in columns:
# Get genres directly from tracks
cursor.execute("""
SELECT genres FROM tracks WHERE genres IS NOT NULL
""")
rows = cursor.fetchall()
# Parse genres (assuming JSON array or comma-separated)
all_genres = []
for row in rows:
genres_str = row['genres']
if genres_str:
# Try JSON parse first
try:
import json
genres = json.loads(genres_str)
all_genres.extend(genres)
except:
# Fallback to comma-separated
genres = [g.strip() for g in genres_str.split(',')]
all_genres.extend(genres)
# Count genres
genre_counts = Counter(all_genres)
return genre_counts.most_common(limit)
else:
# Fallback: use artist names as "genres"
logger.warning("No genres column - using top artists as categories")
cursor.execute("""
SELECT ar.name, COUNT(*) as count
FROM tracks t
LEFT JOIN artists ar ON t.artist_id = ar.id
WHERE ar.name IS NOT NULL
GROUP BY ar.name
ORDER BY count DESC
LIMIT ?
""", (limit,))
rows = cursor.fetchall()
return [(row['name'], row['count']) for row in rows]
except Exception as e:
logger.error(f"Error getting top genres: {e}")
return []
def create_daily_mix(self, genre_or_artist: str, mix_number: int = 1) -> Dict[str, Any]:
"""
Create a Daily Mix playlist - hybrid of library + discovery pool.
Strategy:
- 50% tracks from user's library matching genre/artist
- 50% tracks from discovery pool matching genre/artist
Args:
genre_or_artist: Genre name or artist name to base mix on
mix_number: Mix number (1, 2, 3, etc.)
Returns:
Dict with playlist metadata and tracks
"""
try:
logger.info(f"Creating Daily Mix #{mix_number} for: {genre_or_artist}")
mix_size = 50
library_portion = mix_size // 2 # 25 tracks
discovery_portion = mix_size - library_portion # 25 tracks
# Get tracks from library
library_tracks = self._get_library_tracks_by_category(genre_or_artist, library_portion)
# Get tracks from discovery pool
discovery_tracks = self._get_discovery_tracks_by_category(genre_or_artist, discovery_portion)
# Combine and shuffle
all_tracks = library_tracks + discovery_tracks
random.shuffle(all_tracks)
return {
'mix_number': mix_number,
'name': f"Daily Mix {mix_number}",
'description': f"{genre_or_artist} mix",
'category': genre_or_artist,
'track_count': len(all_tracks),
'tracks': all_tracks
}
except Exception as e:
logger.error(f"Error creating daily mix: {e}")
return {
'mix_number': mix_number,
'name': f"Daily Mix {mix_number}",
'description': 'Mix',
'category': genre_or_artist,
'track_count': 0,
'tracks': []
}
def _get_library_tracks_by_category(self, category: str, limit: int) -> List[Dict]:
"""Get tracks from library matching genre or artist"""
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]
except Exception as e:
logger.error(f"Error getting library tracks by category: {e}")
return []
def _get_discovery_tracks_by_category(self, category: str, limit: int) -> List[Dict]:
"""Get tracks from discovery pool matching genre or artist"""
try:
with self.database._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT
spotify_track_id,
track_name,
artist_name,
album_name,
album_cover_url,
duration_ms,
popularity
FROM discovery_pool
WHERE artist_name LIKE ? OR track_name LIKE ?
ORDER BY RANDOM()
LIMIT ?
""", (f'%{category}%', f'%{category}%', limit))
rows = cursor.fetchall()
return [dict(row) for row in rows]
except Exception as e:
logger.error(f"Error getting discovery tracks by category: {e}")
return []
def get_all_daily_mixes(self, max_mixes: int = 4) -> List[Dict]:
"""
Generate multiple Daily Mix playlists based on top genres/artists.
Args:
max_mixes: Maximum number of mixes to generate (default: 4)
Returns:
List of daily mix dictionaries
"""
try:
# Get top categories (genres or artists)
top_categories = self.get_top_genres_from_library(limit=max_mixes)
if not top_categories:
logger.warning("No categories found for Daily Mixes")
return []
daily_mixes = []
for i, (category, _count) in enumerate(top_categories, 1):
mix = self.create_daily_mix(category, mix_number=i)
if mix['track_count'] > 0:
daily_mixes.append(mix)
logger.info(f"Created {len(daily_mixes)} Daily Mixes")
return daily_mixes
except Exception as e:
logger.error(f"Error getting all daily mixes: {e}")
return []
# Singleton instance
_personalized_playlists_instance = None
def get_personalized_playlists_service(database, spotify_client=None):
"""Get the global personalized playlists service instance"""
global _personalized_playlists_instance
if _personalized_playlists_instance is None:
_personalized_playlists_instance = PersonalizedPlaylistsService(database, spotify_client)
return _personalized_playlists_instance

@ -14860,6 +14860,153 @@ def refresh_seasonal_content():
print(f"Error refreshing seasonal content: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ========================================
# PERSONALIZED PLAYLISTS ENDPOINTS
# ========================================
@app.route('/api/discover/personalized/recently-added', methods=['GET'])
def get_recently_added_playlist():
"""Get recently added tracks from library"""
try:
from core.personalized_playlists import get_personalized_playlists_service
database = get_database()
service = get_personalized_playlists_service(database, spotify_client)
tracks = service.get_recently_added(limit=50)
return jsonify({
"success": True,
"tracks": tracks
})
except Exception as e:
print(f"Error getting recently added playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/top-tracks', methods=['GET'])
def get_top_tracks_playlist():
"""Get user's all-time top tracks"""
try:
from core.personalized_playlists import get_personalized_playlists_service
database = get_database()
service = get_personalized_playlists_service(database, spotify_client)
tracks = service.get_top_tracks(limit=50)
return jsonify({
"success": True,
"tracks": tracks
})
except Exception as e:
print(f"Error getting top tracks playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/forgotten-favorites', methods=['GET'])
def get_forgotten_favorites_playlist():
"""Get forgotten favorites - tracks you loved but haven't played recently"""
try:
from core.personalized_playlists import get_personalized_playlists_service
database = get_database()
service = get_personalized_playlists_service(database, spotify_client)
tracks = service.get_forgotten_favorites(limit=50)
return jsonify({
"success": True,
"tracks": tracks
})
except Exception as e:
print(f"Error getting forgotten favorites playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/decade/<int:decade>', methods=['GET'])
def get_decade_playlist(decade):
"""Get tracks from a specific decade"""
try:
from core.personalized_playlists import get_personalized_playlists_service
database = get_database()
service = get_personalized_playlists_service(database, spotify_client)
tracks = service.get_decade_playlist(decade, limit=100)
return jsonify({
"success": True,
"decade": decade,
"tracks": tracks
})
except Exception as e:
print(f"Error getting decade playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/popular-picks', methods=['GET'])
def get_popular_picks_playlist():
"""Get high popularity tracks from discovery pool"""
try:
from core.personalized_playlists import get_personalized_playlists_service
database = get_database()
service = get_personalized_playlists_service(database, spotify_client)
tracks = service.get_popular_picks(limit=50)
return jsonify({
"success": True,
"tracks": tracks
})
except Exception as e:
print(f"Error getting popular picks playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/hidden-gems', methods=['GET'])
def get_hidden_gems_playlist():
"""Get hidden gems (low popularity) from discovery pool"""
try:
from core.personalized_playlists import get_personalized_playlists_service
database = get_database()
service = get_personalized_playlists_service(database, spotify_client)
tracks = service.get_hidden_gems(limit=50)
return jsonify({
"success": True,
"tracks": tracks
})
except Exception as e:
print(f"Error getting hidden gems playlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/discover/personalized/daily-mixes', methods=['GET'])
def get_daily_mixes():
"""Get all Daily Mix playlists (hybrid library + discovery)"""
try:
from core.personalized_playlists import get_personalized_playlists_service
database = get_database()
service = get_personalized_playlists_service(database, spotify_client)
mixes = service.get_all_daily_mixes(max_mixes=4)
return jsonify({
"success": True,
"mixes": mixes
})
except Exception as e:
print(f"Error getting daily mixes: {e}")
import traceback
traceback.print_exc()
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/metadata/start', methods=['POST'])
def start_metadata_update():
"""Start the metadata update process - EXACT copy of dashboard.py logic"""

@ -1740,6 +1740,28 @@
</div>
</div>
<!-- Recently Added Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<h2 class="discover-section-title">🆕 Recently Added</h2>
<p class="discover-section-subtitle">Latest additions to your library</p>
</div>
<div class="discover-playlist-container compact" id="personalized-recently-added">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Daily Mixes Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<h2 class="discover-section-title">🎵 Daily Mixes</h2>
<p class="discover-section-subtitle">Personalized mixes based on your taste</p>
</div>
<div class="discover-more-playlists-grid" id="daily-mixes-grid">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Fresh Tape Section -->
<div class="discover-section">
<div class="discover-section-header">
@ -1828,6 +1850,50 @@
</div>
</div>
<!-- Popular Picks Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<h2 class="discover-section-title">🔥 Popular Picks</h2>
<p class="discover-section-subtitle">Trending tracks from new discoveries</p>
</div>
<div class="discover-playlist-container compact" id="personalized-popular-picks">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Hidden Gems Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<h2 class="discover-section-title">🌟 Hidden Gems</h2>
<p class="discover-section-subtitle">Underground discoveries waiting for you</p>
</div>
<div class="discover-playlist-container compact" id="personalized-hidden-gems">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Your Top 50 Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<h2 class="discover-section-title">🏆 Your Top 50</h2>
<p class="discover-section-subtitle">All-time favorites from your library</p>
</div>
<div class="discover-playlist-container compact" id="personalized-top-tracks">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- Forgotten Favorites Section -->
<div class="discover-section" style="display: none;">
<div class="discover-section-header">
<h2 class="discover-section-title">💎 Forgotten Favorites</h2>
<p class="discover-section-subtitle">Rediscover tracks you used to love</p>
</div>
<div class="discover-playlist-container compact" id="personalized-forgotten-favorites">
<!-- Content will be populated dynamically -->
</div>
</div>
<!-- More Playlists Section -->
<div class="discover-section">
<div class="discover-section-header">

@ -24708,6 +24708,14 @@ let discoverSeasonalAlbums = [];
let discoverSeasonalTracks = [];
let currentSeasonKey = null;
// Personalized playlists storage
let personalizedRecentlyAdded = [];
let personalizedTopTracks = [];
let personalizedForgottenFavorites = [];
let personalizedPopularPicks = [];
let personalizedHiddenGems = [];
let personalizedDailyMixes = [];
async function loadDiscoverPage() {
console.log('Loading discover page...');
@ -24715,9 +24723,15 @@ async function loadDiscoverPage() {
await Promise.all([
loadDiscoverHero(),
loadDiscoverRecentReleases(),
loadSeasonalContent(), // NEW: Seasonal discovery
loadSeasonalContent(), // Seasonal discovery
loadPersonalizedRecentlyAdded(), // NEW: Recently added from library
loadPersonalizedDailyMixes(), // NEW: Daily Mix playlists
loadDiscoverReleaseRadar(),
loadDiscoverWeekly(),
loadPersonalizedPopularPicks(), // NEW: Popular picks from discovery pool
loadPersonalizedHiddenGems(), // NEW: Hidden gems from discovery pool
loadPersonalizedTopTracks(), // NEW: Your top tracks
loadPersonalizedForgottenFavorites(), // NEW: Forgotten favorites
loadMoreForYou()
]);
@ -25516,6 +25530,208 @@ async function syncSeasonalPlaylist() {
await syncPlaylistToLibrary(playlistData);
}
// ===============================
// PERSONALIZED PLAYLISTS
// ===============================
async function loadPersonalizedRecentlyAdded() {
try {
const container = document.getElementById('personalized-recently-added');
if (!container) return;
const response = await fetch('/api/discover/personalized/recently-added');
if (!response.ok) return;
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
container.closest('.discover-section').style.display = 'none';
return;
}
personalizedRecentlyAdded = data.tracks;
renderCompactPlaylist(container, data.tracks);
container.closest('.discover-section').style.display = 'block';
} catch (error) {
console.error('Error loading recently added:', error);
}
}
async function loadPersonalizedTopTracks() {
try {
const container = document.getElementById('personalized-top-tracks');
if (!container) return;
const response = await fetch('/api/discover/personalized/top-tracks');
if (!response.ok) return;
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
container.closest('.discover-section').style.display = 'none';
return;
}
personalizedTopTracks = data.tracks;
renderCompactPlaylist(container, data.tracks);
container.closest('.discover-section').style.display = 'block';
} catch (error) {
console.error('Error loading top tracks:', error);
}
}
async function loadPersonalizedForgottenFavorites() {
try {
const container = document.getElementById('personalized-forgotten-favorites');
if (!container) return;
const response = await fetch('/api/discover/personalized/forgotten-favorites');
if (!response.ok) return;
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
container.closest('.discover-section').style.display = 'none';
return;
}
personalizedForgottenFavorites = data.tracks;
renderCompactPlaylist(container, data.tracks);
container.closest('.discover-section').style.display = 'block';
} catch (error) {
console.error('Error loading forgotten favorites:', error);
}
}
async function loadPersonalizedPopularPicks() {
try {
const container = document.getElementById('personalized-popular-picks');
if (!container) return;
const response = await fetch('/api/discover/personalized/popular-picks');
if (!response.ok) return;
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
container.closest('.discover-section').style.display = 'none';
return;
}
personalizedPopularPicks = data.tracks;
renderCompactPlaylist(container, data.tracks);
container.closest('.discover-section').style.display = 'block';
} catch (error) {
console.error('Error loading popular picks:', error);
}
}
async function loadPersonalizedHiddenGems() {
try {
const container = document.getElementById('personalized-hidden-gems');
if (!container) return;
const response = await fetch('/api/discover/personalized/hidden-gems');
if (!response.ok) return;
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
container.closest('.discover-section').style.display = 'none';
return;
}
personalizedHiddenGems = data.tracks;
renderCompactPlaylist(container, data.tracks);
container.closest('.discover-section').style.display = 'block';
} catch (error) {
console.error('Error loading hidden gems:', error);
}
}
async function loadPersonalizedDailyMixes() {
try {
const container = document.getElementById('daily-mixes-grid');
if (!container) return;
const response = await fetch('/api/discover/personalized/daily-mixes');
if (!response.ok) return;
const data = await response.json();
if (!data.success || !data.mixes || data.mixes.length === 0) {
container.closest('.discover-section').style.display = 'none';
return;
}
personalizedDailyMixes = data.mixes;
// Render Daily Mix cards
let html = '';
data.mixes.forEach((mix, index) => {
const coverUrl = mix.tracks && mix.tracks.length > 0 ?
(mix.tracks[0].album_cover_url || '/static/placeholder-album.png') :
'/static/placeholder-album.png';
html += `
<div class="discover-playlist-card" onclick="openDailyMix(${index})">
<div class="discover-playlist-cover">
<img src="${coverUrl}" alt="${mix.name}">
<div class="playlist-play-overlay"></div>
</div>
<div class="discover-playlist-info">
<h4 class="discover-playlist-name">${mix.name}</h4>
<p class="discover-playlist-description">${mix.description}</p>
<p class="discover-playlist-count">${mix.track_count} tracks</p>
</div>
</div>
`;
});
container.innerHTML = html;
container.closest('.discover-section').style.display = 'block';
} catch (error) {
console.error('Error loading daily mixes:', error);
}
}
function renderCompactPlaylist(container, tracks) {
let html = '<div class="discover-playlist-tracks-compact">';
tracks.forEach((track, index) => {
const coverUrl = track.album_cover_url || '/static/placeholder-album.png';
const durationMin = Math.floor(track.duration_ms / 60000);
const durationSec = Math.floor((track.duration_ms % 60000) / 1000);
const duration = `${durationMin}:${durationSec.toString().padStart(2, '0')}`;
html += `
<div class="discover-playlist-track-compact" data-track-index="${index}">
<div class="track-compact-number">${index + 1}</div>
<div class="track-compact-image">
<img src="${coverUrl}" alt="${track.album_name}">
</div>
<div class="track-compact-info">
<div class="track-compact-name">${track.track_name}</div>
<div class="track-compact-artist">${track.artist_name}</div>
</div>
<div class="track-compact-album">${track.album_name}</div>
<div class="track-compact-duration">${duration}</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
function openDailyMix(mixIndex) {
const mix = personalizedDailyMixes[mixIndex];
if (!mix || !mix.tracks) return;
// TODO: Open modal or dedicated view for Daily Mix
console.log('Opening Daily Mix:', mix.name);
}
// ===============================
// DISCOVER PLAYLIST ACTIONS
// ===============================

Loading…
Cancel
Save