discover progress
This commit is contained in:
parent
08a4decda8
commit
5a13b5fe94
5 changed files with 564 additions and 140 deletions
|
|
@ -73,7 +73,7 @@ class PersonalizedPlaylistsService:
|
|||
|
||||
def get_decade_playlist(self, decade: int, limit: int = 100) -> List[Dict]:
|
||||
"""
|
||||
Get tracks from a specific decade.
|
||||
Get tracks from a specific decade from discovery pool with diversity filtering.
|
||||
|
||||
Args:
|
||||
decade: Decade year (e.g., 2020 for 2020s, 2010 for 2010s)
|
||||
|
|
@ -86,56 +86,59 @@ class PersonalizedPlaylistsService:
|
|||
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))
|
||||
# Query discovery_pool - get 10x more for diversity filtering
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
spotify_track_id,
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
album_cover_url,
|
||||
duration_ms,
|
||||
popularity,
|
||||
release_date
|
||||
FROM discovery_pool
|
||||
WHERE release_date IS NOT NULL
|
||||
AND CAST(SUBSTR(release_date, 1, 4) AS INTEGER) BETWEEN ? AND ?
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
""", (start_year, end_year, limit * 10))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
all_tracks = [dict(row) for row in rows]
|
||||
|
||||
if not all_tracks:
|
||||
logger.warning(f"No tracks found for {decade}s")
|
||||
return []
|
||||
|
||||
# Apply diversity constraint: max 3 tracks per album, max 4 per artist
|
||||
# Shuffle first for randomness
|
||||
import random
|
||||
random.shuffle(all_tracks)
|
||||
|
||||
tracks_by_album = {}
|
||||
tracks_by_artist = {}
|
||||
diverse_tracks = []
|
||||
|
||||
for track in all_tracks:
|
||||
album = track['album_name']
|
||||
artist = track['artist_name']
|
||||
|
||||
# Count current tracks for this album/artist
|
||||
album_count = tracks_by_album.get(album, 0)
|
||||
artist_count = tracks_by_artist.get(artist, 0)
|
||||
|
||||
# Apply more lenient limits: max 3 per album, max 4 per artist
|
||||
if album_count < 3 and artist_count < 4:
|
||||
diverse_tracks.append(track)
|
||||
tracks_by_album[album] = album_count + 1
|
||||
tracks_by_artist[artist] = artist_count + 1
|
||||
|
||||
if len(diverse_tracks) >= limit:
|
||||
break
|
||||
|
||||
logger.info(f"Found {len(diverse_tracks)} tracks from {decade}s in discovery pool (with diversity filtering)")
|
||||
return diverse_tracks[:limit]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting decade playlist for {decade}s: {e}")
|
||||
|
|
|
|||
|
|
@ -14538,12 +14538,18 @@ def get_discover_hero():
|
|||
try:
|
||||
database = get_database()
|
||||
|
||||
# Get top similar artists (by occurrence count)
|
||||
similar_artists = database.get_top_similar_artists(limit=10)
|
||||
# Get top similar artists (by occurrence count) - get 20 for variety
|
||||
similar_artists = database.get_top_similar_artists(limit=20)
|
||||
|
||||
if not similar_artists:
|
||||
return jsonify({"success": True, "artists": []})
|
||||
|
||||
# Shuffle for variety and take top 10
|
||||
import random
|
||||
shuffled = list(similar_artists)
|
||||
random.shuffle(shuffled)
|
||||
similar_artists = shuffled[:10]
|
||||
|
||||
# Convert to JSON format with Spotify data enrichment
|
||||
hero_artists = []
|
||||
for artist in similar_artists:
|
||||
|
|
@ -15110,6 +15116,91 @@ def generate_custom_playlist():
|
|||
traceback.print_exc()
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/discover/decades/available', methods=['GET'])
|
||||
def get_available_decades():
|
||||
"""Get list of decades that have content in discovery pool"""
|
||||
try:
|
||||
database = get_database()
|
||||
|
||||
with database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get distinct decades from discovery pool
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT
|
||||
(CAST(SUBSTR(release_date, 1, 4) AS INTEGER) / 10) * 10 as decade,
|
||||
COUNT(*) as track_count
|
||||
FROM discovery_pool
|
||||
WHERE release_date IS NOT NULL
|
||||
AND CAST(SUBSTR(release_date, 1, 4) AS INTEGER) >= 1950
|
||||
AND CAST(SUBSTR(release_date, 1, 4) AS INTEGER) <= 2029
|
||||
GROUP BY decade
|
||||
HAVING track_count >= 10
|
||||
ORDER BY decade ASC
|
||||
""")
|
||||
|
||||
rows = cursor.fetchall()
|
||||
decades = []
|
||||
for row in rows:
|
||||
decades.append({
|
||||
'year': row[0],
|
||||
'track_count': row[1]
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"decades": decades
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting available decades: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/discover/decade/<int:decade>', methods=['GET'])
|
||||
def get_discover_decade_playlist(decade):
|
||||
"""Get tracks from a specific decade for discovery page"""
|
||||
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=50)
|
||||
|
||||
if not tracks:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"tracks": [],
|
||||
"decade": decade,
|
||||
"message": f"No tracks found for the {decade}s"
|
||||
}), 200
|
||||
|
||||
# Convert to Spotify format for modal compatibility
|
||||
spotify_tracks = []
|
||||
for track in tracks:
|
||||
spotify_tracks.append({
|
||||
'id': track.get('spotify_track_id', track.get('id')),
|
||||
'name': track.get('track_name', track.get('name')),
|
||||
'artists': [track.get('artist_name', 'Unknown')],
|
||||
'album': {
|
||||
'name': track.get('album_name', 'Unknown'),
|
||||
'images': [{'url': track.get('album_cover_url')}] if track.get('album_cover_url') else []
|
||||
},
|
||||
'duration_ms': track.get('duration_ms', 0)
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"tracks": spotify_tracks,
|
||||
"decade": decade
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting decade playlist: {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"""
|
||||
|
|
|
|||
|
|
@ -1673,6 +1673,9 @@
|
|||
<div class="discover-hero-label">FEATURED ARTIST</div>
|
||||
<h1 class="discover-hero-title" id="discover-hero-title">Loading...</h1>
|
||||
<p class="discover-hero-subtitle" id="discover-hero-subtitle">Discover new music tailored to your taste</p>
|
||||
<div class="discover-hero-meta" id="discover-hero-meta">
|
||||
<!-- Popularity and genres will be populated here -->
|
||||
</div>
|
||||
<div class="discover-hero-actions">
|
||||
<button class="discover-hero-button secondary" id="discover-hero-discography" onclick="viewDiscoverHeroDiscography()">
|
||||
<span class="button-icon">📀</span>
|
||||
|
|
@ -2054,20 +2057,31 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- More Playlists Section -->
|
||||
<!-- Decade Browser Section -->
|
||||
<div class="discover-section">
|
||||
<div class="discover-section-header">
|
||||
<h2 class="discover-section-title">More For You</h2>
|
||||
<p class="discover-section-subtitle">Curated playlists based on your taste</p>
|
||||
<h2 class="discover-section-title">⏰ Time Machine</h2>
|
||||
<p class="discover-section-subtitle">Travel through the decades</p>
|
||||
</div>
|
||||
<div class="discover-playlists-grid" id="more-playlists-grid">
|
||||
<div class="discover-carousel" id="decade-browser-carousel">
|
||||
<!-- Content will be populated dynamically -->
|
||||
<div class="discover-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading playlists...</p>
|
||||
<p>Loading decades...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Picks Section -->
|
||||
<div class="discover-section">
|
||||
<div class="discover-section-header">
|
||||
<h2 class="discover-section-title">⚡ Quick Picks</h2>
|
||||
<p class="discover-section-subtitle">Jump into curated experiences</p>
|
||||
</div>
|
||||
<div class="quick-picks-grid" id="quick-picks-grid">
|
||||
<!-- Content will be populated dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -4204,6 +4204,7 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
|
|||
virtualPlaylistId.startsWith('discover_') ? 'SoulSync' :
|
||||
virtualPlaylistId.startsWith('seasonal_') ? 'SoulSync' :
|
||||
virtualPlaylistId.startsWith('build_playlist_') ? 'SoulSync' :
|
||||
virtualPlaylistId.startsWith('decade_') ? 'SoulSync' :
|
||||
virtualPlaylistId === 'build_playlist_custom' ? 'SoulSync' :
|
||||
'YouTube';
|
||||
|
||||
|
|
@ -24741,7 +24742,8 @@ async function loadDiscoverPage() {
|
|||
loadPersonalizedForgottenFavorites(), // NEW: Forgotten favorites
|
||||
loadDiscoveryShuffle(), // NEW: Discovery Shuffle
|
||||
loadFamiliarFavorites(), // NEW: Familiar Favorites
|
||||
loadMoreForYou()
|
||||
loadDecadeBrowser(), // Decade browser
|
||||
loadQuickPicks() // Quick picks action cards
|
||||
]);
|
||||
|
||||
// Check for active syncs after page load
|
||||
|
|
@ -24878,6 +24880,7 @@ async function loadDiscoverHero() {
|
|||
function displayDiscoverHeroArtist(artist) {
|
||||
const titleEl = document.getElementById('discover-hero-title');
|
||||
const subtitleEl = document.getElementById('discover-hero-subtitle');
|
||||
const metaEl = document.getElementById('discover-hero-meta');
|
||||
const imageEl = document.getElementById('discover-hero-image');
|
||||
const bgEl = document.getElementById('discover-hero-bg');
|
||||
|
||||
|
|
@ -24893,14 +24896,37 @@ function displayDiscoverHeroArtist(artist) {
|
|||
} else {
|
||||
subtitle = 'Similar to an artist in your watchlist';
|
||||
}
|
||||
subtitleEl.textContent = subtitle;
|
||||
}
|
||||
|
||||
// Add genre context if available
|
||||
if (artist.genres && artist.genres.length > 0) {
|
||||
const topGenres = artist.genres.slice(0, 2).join(', ');
|
||||
subtitle += ` • ${topGenres}`;
|
||||
// Build metadata section with popularity and genres
|
||||
if (metaEl) {
|
||||
let metaHTML = '<div class="discover-hero-meta-content">';
|
||||
|
||||
// Add popularity indicator
|
||||
if (artist.popularity !== undefined && artist.popularity > 0) {
|
||||
const popularityClass = artist.popularity >= 80 ? 'high' :
|
||||
artist.popularity >= 50 ? 'medium' : 'low';
|
||||
metaHTML += `
|
||||
<div class="hero-meta-item hero-popularity ${popularityClass}">
|
||||
<span class="meta-icon">⭐</span>
|
||||
<span class="meta-value">${artist.popularity}/100</span>
|
||||
<span class="meta-label">Popularity</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
subtitleEl.textContent = subtitle;
|
||||
// Add genre tags
|
||||
if (artist.genres && artist.genres.length > 0) {
|
||||
metaHTML += '<div class="hero-meta-item hero-genres">';
|
||||
artist.genres.slice(0, 3).forEach(genre => {
|
||||
metaHTML += `<span class="genre-tag">${genre}</span>`;
|
||||
});
|
||||
metaHTML += '</div>';
|
||||
}
|
||||
|
||||
metaHTML += '</div>';
|
||||
metaEl.innerHTML = metaHTML;
|
||||
}
|
||||
|
||||
if (imageEl && artist.image_url) {
|
||||
|
|
@ -25225,98 +25251,152 @@ async function loadDiscoverWeekly() {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadMoreForYou() {
|
||||
// ===============================
|
||||
// DECADE BROWSER
|
||||
// ===============================
|
||||
|
||||
let selectedDecade = null;
|
||||
let decadeTracks = [];
|
||||
|
||||
async function loadDecadeBrowser() {
|
||||
try {
|
||||
const grid = document.getElementById('more-playlists-grid');
|
||||
if (!grid) return;
|
||||
const carousel = document.getElementById('decade-browser-carousel');
|
||||
if (!carousel) return;
|
||||
|
||||
grid.innerHTML = '<div class="discover-loading"><div class="loading-spinner"></div><p>Loading playlists...</p></div>';
|
||||
|
||||
// Fetch discovery pool tracks to create curated playlists
|
||||
const response = await fetch('/api/discover/weekly');
|
||||
// Fetch available decades from backend
|
||||
const response = await fetch('/api/discover/decades/available');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch tracks for playlists');
|
||||
throw new Error('Failed to fetch available decades');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.decades || data.decades.length === 0) {
|
||||
carousel.innerHTML = '<div class="discover-empty"><p>No decade content available yet. Run a watchlist scan to populate your discovery pool!</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build decade cards from available decades
|
||||
let html = '';
|
||||
data.decades.forEach(decade => {
|
||||
const icon = getDecadeIcon(decade.year);
|
||||
const label = `${decade.year}s`;
|
||||
html += `
|
||||
<div class="decade-card" onclick="openDecadePlaylist(${decade.year})">
|
||||
<div class="decade-card-content">
|
||||
<div class="decade-icon">${icon}</div>
|
||||
<div class="decade-year">${label}</div>
|
||||
<div class="decade-title">${decade.track_count} tracks</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
carousel.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading decade browser:', error);
|
||||
const carousel = document.getElementById('decade-browser-carousel');
|
||||
if (carousel) {
|
||||
carousel.innerHTML = '<div class="discover-empty"><p>Failed to load decades</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDecadeIcon(year) {
|
||||
const icons = {
|
||||
1950: '🎺',
|
||||
1960: '🎸',
|
||||
1970: '🕺',
|
||||
1980: '📻',
|
||||
1990: '💿',
|
||||
2000: '📱',
|
||||
2010: '🎧',
|
||||
2020: '🌐'
|
||||
};
|
||||
return icons[year] || '🎵';
|
||||
}
|
||||
|
||||
async function openDecadePlaylist(decade) {
|
||||
try {
|
||||
showLoadingOverlay(`Loading ${decade}s playlist...`);
|
||||
|
||||
const response = await fetch(`/api/discover/decade/${decade}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch decade playlist');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||
grid.innerHTML = '<div class="discover-empty"><p>No playlists available yet</p></div>';
|
||||
const message = data.message || `No tracks found for the ${decade}s`;
|
||||
showToast(message, 'info');
|
||||
hideLoadingOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
const tracks = data.tracks;
|
||||
selectedDecade = decade;
|
||||
decadeTracks = data.tracks;
|
||||
|
||||
// Create curated playlists
|
||||
const playlists = [];
|
||||
// Open download modal
|
||||
const playlistName = `${decade}s Classics`;
|
||||
const virtualPlaylistId = `decade_${decade}`;
|
||||
|
||||
// 1. Popular Picks (by popularity score)
|
||||
const popularTracks = [...tracks].sort((a, b) => b.popularity - a.popularity).slice(0, 20);
|
||||
if (popularTracks.length > 0) {
|
||||
playlists.push({
|
||||
name: 'Popular Picks',
|
||||
description: 'Trending tracks from similar artists',
|
||||
track_count: popularTracks.length,
|
||||
cover: popularTracks[0].album_cover_url
|
||||
});
|
||||
}
|
||||
await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, data.tracks);
|
||||
hideLoadingOverlay();
|
||||
|
||||
// 2. Deep Cuts (lower popularity, hidden gems)
|
||||
const deepCuts = [...tracks].filter(t => t.popularity < 50).slice(0, 20);
|
||||
if (deepCuts.length > 0) {
|
||||
playlists.push({
|
||||
name: 'Deep Cuts',
|
||||
description: 'Hidden gems you might have missed',
|
||||
track_count: deepCuts.length,
|
||||
cover: deepCuts[0].album_cover_url
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error opening ${decade}s playlist:`, error);
|
||||
showToast(`Failed to load ${decade}s playlist`, 'error');
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fresh Finds (newest additions to pool)
|
||||
const freshFinds = [...tracks].slice(0, 20);
|
||||
if (freshFinds.length > 0) {
|
||||
playlists.push({
|
||||
name: 'Fresh Finds',
|
||||
description: 'Recently added to your discovery pool',
|
||||
track_count: freshFinds.length,
|
||||
cover: freshFinds[0].album_cover_url
|
||||
});
|
||||
}
|
||||
// ===============================
|
||||
// QUICK PICKS
|
||||
// ===============================
|
||||
|
||||
// 4. Artist Mix (group by artist diversity)
|
||||
const artistMap = {};
|
||||
tracks.forEach(track => {
|
||||
if (!artistMap[track.artist_name]) {
|
||||
artistMap[track.artist_name] = [];
|
||||
function loadQuickPicks() {
|
||||
try {
|
||||
const grid = document.getElementById('quick-picks-grid');
|
||||
if (!grid) return;
|
||||
|
||||
const quickPicks = [
|
||||
{
|
||||
icon: '🎲',
|
||||
title: 'Surprise Me',
|
||||
description: 'Shuffle your discovery pool',
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
action: 'scrollToSection("personalized-discovery-shuffle")'
|
||||
},
|
||||
{
|
||||
icon: '🔥',
|
||||
title: 'Trending Now',
|
||||
description: 'High popularity discoveries',
|
||||
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
action: 'scrollToSection("personalized-popular-picks")'
|
||||
},
|
||||
{
|
||||
icon: '💎',
|
||||
title: 'Deep Cuts',
|
||||
description: 'Underground hidden gems',
|
||||
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||
action: 'scrollToSection("personalized-hidden-gems")'
|
||||
},
|
||||
{
|
||||
icon: '⏰',
|
||||
title: 'Time Machine',
|
||||
description: 'Browse by decade',
|
||||
gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
||||
action: 'scrollToSection("decade-browser-carousel")'
|
||||
}
|
||||
if (artistMap[track.artist_name].length < 3) {
|
||||
artistMap[track.artist_name].push(track);
|
||||
}
|
||||
});
|
||||
const mixTracks = Object.values(artistMap).flat().slice(0, 25);
|
||||
if (mixTracks.length > 0) {
|
||||
playlists.push({
|
||||
name: 'Artist Mix',
|
||||
description: 'Diverse selection from multiple artists',
|
||||
track_count: mixTracks.length,
|
||||
cover: mixTracks[0].album_cover_url
|
||||
});
|
||||
}
|
||||
];
|
||||
|
||||
// Build playlist grid HTML
|
||||
let html = '';
|
||||
playlists.forEach(playlist => {
|
||||
const coverUrl = playlist.cover || '/static/placeholder-album.png';
|
||||
quickPicks.forEach(pick => {
|
||||
html += `
|
||||
<div class="discover-playlist-card">
|
||||
<div class="discover-playlist-cover">
|
||||
<img src="${coverUrl}" alt="${playlist.name}">
|
||||
<div class="playlist-play-overlay">▶</div>
|
||||
</div>
|
||||
<div class="discover-playlist-info">
|
||||
<h4 class="discover-playlist-name">${playlist.name}</h4>
|
||||
<p class="discover-playlist-description">${playlist.description}</p>
|
||||
<p class="discover-playlist-count">${playlist.track_count} tracks</p>
|
||||
</div>
|
||||
<div class="quick-pick-card" onclick="${pick.action}" style="background: ${pick.gradient};">
|
||||
<div class="quick-pick-icon">${pick.icon}</div>
|
||||
<h3 class="quick-pick-title">${pick.title}</h3>
|
||||
<p class="quick-pick-description">${pick.description}</p>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
|
@ -25324,12 +25404,31 @@ async function loadMoreForYou() {
|
|||
grid.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading more for you:', error);
|
||||
const grid = document.getElementById('more-playlists-grid');
|
||||
if (grid) {
|
||||
grid.innerHTML = '<div class="discover-empty"><p>Failed to load playlists</p></div>';
|
||||
console.error('Error loading quick picks:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToSection(sectionId) {
|
||||
const section = document.getElementById(sectionId);
|
||||
if (!section) {
|
||||
console.warn(`Section not found: ${sectionId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show section if it's hidden
|
||||
const parentSection = section.closest('.discover-section');
|
||||
if (parentSection) {
|
||||
const currentDisplay = window.getComputedStyle(parentSection).display;
|
||||
if (currentDisplay === 'none') {
|
||||
parentSection.style.display = 'block';
|
||||
console.log(`Showing hidden section: ${sectionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a tick for layout, then scroll
|
||||
setTimeout(() => {
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// ===============================
|
||||
|
|
|
|||
|
|
@ -16239,11 +16239,82 @@ body {
|
|||
.discover-hero-subtitle {
|
||||
font-size: 19px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-bottom: 36px;
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.7;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Hero Meta Section (Popularity & Genres) */
|
||||
.discover-hero-meta {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.discover-hero-meta-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hero-popularity {
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero-popularity.high {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.hero-popularity.medium {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.hero-popularity.low {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.hero-popularity .meta-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.hero-popularity .meta-value {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero-popularity .meta-label {
|
||||
opacity: 0.8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.hero-genres {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.genre-tag {
|
||||
padding: 6px 14px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.genre-tag:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.discover-hero-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
|
@ -17223,3 +17294,149 @@ body {
|
|||
.build-playlist-metadata strong {
|
||||
color: #1db954;
|
||||
}
|
||||
|
||||
/* =======================
|
||||
QUICK PICKS SECTION
|
||||
======================= */
|
||||
|
||||
.quick-picks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.quick-pick-card {
|
||||
position: relative;
|
||||
padding: 32px 24px;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-pick-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: inherit;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-pick-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.quick-pick-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.quick-pick-icon {
|
||||
position: relative;
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.quick-pick-title {
|
||||
position: relative;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0 0 8px 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.quick-pick-description {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* =======================
|
||||
DECADE BROWSER SECTION
|
||||
======================= */
|
||||
|
||||
.decade-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
}
|
||||
|
||||
.decade-card:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.decade-card-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(102, 126, 234, 0.15) 0%,
|
||||
rgba(118, 75, 162, 0.15) 100%);
|
||||
}
|
||||
|
||||
.decade-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.decade-card:hover::before {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.decade-icon {
|
||||
position: relative;
|
||||
font-size: 80px;
|
||||
line-height: 1;
|
||||
z-index: 1;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.decade-card:hover .decade-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.decade-year {
|
||||
position: relative;
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
letter-spacing: -1px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.decade-title {
|
||||
position: relative;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue