discover page progress
This commit is contained in:
parent
ee266920a9
commit
cc62b3b48e
7 changed files with 516 additions and 8 deletions
|
|
@ -9,6 +9,7 @@ from typing import List, Dict, Any, Optional, Tuple
|
|||
from datetime import datetime, timedelta
|
||||
from collections import Counter
|
||||
import random
|
||||
import json
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("personalized_playlists")
|
||||
|
|
@ -162,6 +163,170 @@ class PersonalizedPlaylistsService:
|
|||
logger.error(f"Error getting decade playlist for {decade}s: {e}")
|
||||
return []
|
||||
|
||||
def get_available_genres(self) -> List[Dict]:
|
||||
"""
|
||||
Get list of genres with track counts from discovery pool.
|
||||
Uses cached artist genres from database (populated during discovery scan).
|
||||
"""
|
||||
try:
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all tracks with genres from discovery pool
|
||||
cursor.execute("""
|
||||
SELECT artist_genres
|
||||
FROM discovery_pool
|
||||
WHERE artist_genres IS NOT NULL
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if not rows:
|
||||
logger.warning("No genres found in discovery pool - genres may not be populated yet")
|
||||
return []
|
||||
|
||||
# Count tracks per genre
|
||||
genre_track_count = {} # {genre: count}
|
||||
|
||||
for row in rows:
|
||||
try:
|
||||
artist_genres_json = row[0]
|
||||
if artist_genres_json:
|
||||
genres = json.loads(artist_genres_json)
|
||||
for genre in genres:
|
||||
genre_track_count[genre] = genre_track_count.get(genre, 0) + 1
|
||||
except Exception as e:
|
||||
logger.debug(f"Error parsing genres JSON: {e}")
|
||||
continue
|
||||
|
||||
# Filter genres with at least 10 tracks and sort by count
|
||||
available_genres = [
|
||||
{'name': genre, 'track_count': count}
|
||||
for genre, count in genre_track_count.items()
|
||||
if count >= 10
|
||||
]
|
||||
available_genres.sort(key=lambda x: x['track_count'], reverse=True)
|
||||
|
||||
logger.info(f"Found {len(available_genres)} genres with 10+ tracks")
|
||||
return available_genres[:20] # Top 20 genres
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting available genres: {e}")
|
||||
return []
|
||||
|
||||
def get_genre_playlist(self, genre: str, limit: int = 50) -> List[Dict]:
|
||||
"""
|
||||
Get tracks from a specific genre with diversity filtering.
|
||||
Uses cached artist genres from database (populated during discovery scan).
|
||||
"""
|
||||
try:
|
||||
with self.database._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all tracks with this genre (query cached genres)
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
spotify_track_id,
|
||||
track_name,
|
||||
artist_name,
|
||||
album_name,
|
||||
album_cover_url,
|
||||
duration_ms,
|
||||
popularity,
|
||||
artist_genres
|
||||
FROM discovery_pool
|
||||
WHERE artist_genres IS NOT NULL
|
||||
""")
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Filter tracks that match the genre (using partial matching)
|
||||
matching_tracks = []
|
||||
genre_lower = genre.lower()
|
||||
|
||||
for row in rows:
|
||||
try:
|
||||
artist_genres_json = row[7] # artist_genres column
|
||||
if artist_genres_json:
|
||||
genres = json.loads(artist_genres_json)
|
||||
# Partial match: if search genre is in any artist genre
|
||||
# e.g., "house" matches "electro house", "progressive house", etc.
|
||||
genre_match = any(genre_lower in g.lower() for g in genres)
|
||||
|
||||
if genre_match:
|
||||
# Convert row to dict (exclude artist_genres from output)
|
||||
track_dict = {
|
||||
'spotify_track_id': row[0],
|
||||
'track_name': row[1],
|
||||
'artist_name': row[2],
|
||||
'album_name': row[3],
|
||||
'album_cover_url': row[4],
|
||||
'duration_ms': row[5],
|
||||
'popularity': row[6]
|
||||
}
|
||||
matching_tracks.append(track_dict)
|
||||
except Exception as e:
|
||||
logger.debug(f"Error parsing genres for track: {e}")
|
||||
continue
|
||||
|
||||
if not matching_tracks:
|
||||
logger.warning(f"No tracks found for genre: {genre}")
|
||||
return []
|
||||
|
||||
# Shuffle before limiting for better variety
|
||||
random.shuffle(matching_tracks)
|
||||
|
||||
# Limit to 10x for diversity filtering
|
||||
all_tracks = matching_tracks[:limit * 10] if len(matching_tracks) > limit * 10 else matching_tracks
|
||||
|
||||
if not all_tracks:
|
||||
return []
|
||||
|
||||
# Apply adaptive diversity filtering (relaxed for genres)
|
||||
unique_artists = len(set(track['artist_name'] for track in all_tracks))
|
||||
|
||||
if unique_artists >= 20:
|
||||
max_per_album = 3
|
||||
max_per_artist = 5
|
||||
elif unique_artists >= 10:
|
||||
max_per_album = 4
|
||||
max_per_artist = 10
|
||||
elif unique_artists >= 5:
|
||||
max_per_album = 6
|
||||
max_per_artist = 15
|
||||
else:
|
||||
# Very limited artist pool - be more lenient
|
||||
max_per_album = 8
|
||||
max_per_artist = 25
|
||||
|
||||
logger.info(f"Genre '{genre}' has {unique_artists} artists, {len(all_tracks)} total tracks - limits: {max_per_album}/album, {max_per_artist}/artist")
|
||||
|
||||
# Shuffle and apply diversity
|
||||
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']
|
||||
|
||||
album_count = tracks_by_album.get(album, 0)
|
||||
artist_count = tracks_by_artist.get(artist, 0)
|
||||
|
||||
if album_count < max_per_album and artist_count < max_per_artist:
|
||||
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 for genre '{genre}'")
|
||||
return diverse_tracks[:limit]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting genre playlist for {genre}: {e}")
|
||||
return []
|
||||
|
||||
# ========================================
|
||||
# DISCOVERY POOL PLAYLISTS
|
||||
# ========================================
|
||||
|
|
|
|||
|
|
@ -763,6 +763,15 @@ class WatchlistScanner:
|
|||
logger.debug(f"No albums found for {similar_artist.similar_artist_name}")
|
||||
continue
|
||||
|
||||
# Fetch artist genres once for all tracks of this artist
|
||||
artist_genres = []
|
||||
try:
|
||||
artist_data = self.spotify_client.get_artist(similar_artist.similar_artist_spotify_id)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch genres for {similar_artist.similar_artist_name}: {e}")
|
||||
|
||||
# IMPROVED: Smart selection mixing albums, singles, and EPs
|
||||
# Prioritize recent releases and popular content
|
||||
|
||||
|
|
@ -833,7 +842,8 @@ class WatchlistScanner:
|
|||
'popularity': album_data.get('popularity', 0),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'is_new_release': is_new,
|
||||
'track_data_json': track # Store full Spotify track object
|
||||
'track_data_json': track, # Store full Spotify track object
|
||||
'artist_genres': artist_genres # Add cached genres
|
||||
}
|
||||
|
||||
# Add to discovery pool
|
||||
|
|
@ -893,6 +903,17 @@ class WatchlistScanner:
|
|||
if album_data and 'tracks' in album_data:
|
||||
tracks = album_data['tracks'].get('items', [])
|
||||
|
||||
# Fetch artist genres
|
||||
artist_genres = []
|
||||
try:
|
||||
if album_data.get('artists') and len(album_data['artists']) > 0:
|
||||
artist_id = album_data['artists'][0]['id']
|
||||
artist_data = self.spotify_client.get_artist(artist_id)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch genres for album artist: {e}")
|
||||
|
||||
# Check if new release
|
||||
is_new = False
|
||||
try:
|
||||
|
|
@ -918,7 +939,8 @@ class WatchlistScanner:
|
|||
'popularity': album_data.get('popularity', 0),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'is_new_release': is_new,
|
||||
'track_data_json': track
|
||||
'track_data_json': track,
|
||||
'artist_genres': artist_genres
|
||||
}
|
||||
|
||||
if self.database.add_to_discovery_pool(track_data):
|
||||
|
|
@ -1010,6 +1032,15 @@ class WatchlistScanner:
|
|||
if not recent_releases:
|
||||
continue
|
||||
|
||||
# Fetch artist genres once for all tracks of this artist
|
||||
artist_genres = []
|
||||
try:
|
||||
artist_data = self.spotify_client.get_artist(artist.spotify_artist_id)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch genres for {artist.artist_name}: {e}")
|
||||
|
||||
for release in recent_releases:
|
||||
try:
|
||||
# Check if release is within cutoff
|
||||
|
|
@ -1050,7 +1081,8 @@ class WatchlistScanner:
|
|||
'popularity': album_data.get('popularity', 0),
|
||||
'release_date': album_data.get('release_date', ''),
|
||||
'is_new_release': is_new,
|
||||
'track_data_json': track
|
||||
'track_data_json': track,
|
||||
'artist_genres': artist_genres
|
||||
}
|
||||
|
||||
if self.database.add_to_discovery_pool(track_data):
|
||||
|
|
@ -1314,8 +1346,28 @@ class WatchlistScanner:
|
|||
|
||||
# Add Release Radar tracks to discovery pool so they're available for fast lookup
|
||||
logger.info(f"Adding {len(release_radar_track_data)} Release Radar tracks to discovery pool...")
|
||||
|
||||
# Cache genres by artist_id to avoid duplicate API calls
|
||||
artist_genres_cache = {}
|
||||
|
||||
for track_data in release_radar_track_data:
|
||||
try:
|
||||
# Fetch artist genres (with caching)
|
||||
artist_genres = []
|
||||
if track_data['artists'] and len(track_data['artists']) > 0:
|
||||
artist_id = track_data['artists'][0]['id']
|
||||
|
||||
if artist_id in artist_genres_cache:
|
||||
artist_genres = artist_genres_cache[artist_id]
|
||||
else:
|
||||
try:
|
||||
artist_data = self.spotify_client.get_artist(artist_id)
|
||||
if artist_data and 'genres' in artist_data:
|
||||
artist_genres = artist_data['genres']
|
||||
artist_genres_cache[artist_id] = artist_genres
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not fetch genres for artist {artist_id}: {e}")
|
||||
|
||||
# Format track data for discovery pool (expects specific structure)
|
||||
formatted_track = {
|
||||
'spotify_track_id': track_data['id'],
|
||||
|
|
@ -1329,7 +1381,8 @@ class WatchlistScanner:
|
|||
'popularity': track_data.get('popularity', 0),
|
||||
'release_date': track_data['album'].get('release_date', ''),
|
||||
'is_new_release': True,
|
||||
'track_data_json': track_data
|
||||
'track_data_json': track_data,
|
||||
'artist_genres': artist_genres
|
||||
}
|
||||
self.database.add_to_discovery_pool(formatted_track)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -521,6 +521,14 @@ class MusicDatabase:
|
|||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_date ON discovery_recent_albums (release_date)")
|
||||
cursor.execute("CREATE INDEX IF NOT EXISTS idx_discovery_recent_albums_artist ON discovery_recent_albums (artist_spotify_id)")
|
||||
|
||||
# Add genres column to discovery_pool if it doesn't exist (migration)
|
||||
cursor.execute("PRAGMA table_info(discovery_pool)")
|
||||
discovery_pool_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
if 'artist_genres' not in discovery_pool_columns:
|
||||
cursor.execute("ALTER TABLE discovery_pool ADD COLUMN artist_genres TEXT")
|
||||
logger.info("Added artist_genres column to discovery_pool table")
|
||||
|
||||
logger.info("Discovery tables created successfully")
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -2589,12 +2597,16 @@ class MusicDatabase:
|
|||
if cursor.fetchone()['count'] > 0:
|
||||
return True # Already in pool
|
||||
|
||||
# Get artist genres if available
|
||||
artist_genres = track_data.get('artist_genres')
|
||||
artist_genres_json = json.dumps(artist_genres) if artist_genres else None
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO discovery_pool
|
||||
(spotify_track_id, spotify_album_id, spotify_artist_id, track_name, artist_name,
|
||||
album_name, album_cover_url, duration_ms, popularity, release_date,
|
||||
is_new_release, track_data_json, added_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
is_new_release, track_data_json, artist_genres, added_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
""", (
|
||||
track_data['spotify_track_id'],
|
||||
track_data['spotify_album_id'],
|
||||
|
|
@ -2607,7 +2619,8 @@ class MusicDatabase:
|
|||
track_data.get('popularity', 0),
|
||||
track_data['release_date'],
|
||||
track_data.get('is_new_release', False),
|
||||
json.dumps(track_data['track_data_json'])
|
||||
json.dumps(track_data['track_data_json']),
|
||||
artist_genres_json
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
|
|
|||
|
|
@ -15201,6 +15201,73 @@ def get_discover_decade_playlist(decade):
|
|||
traceback.print_exc()
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/discover/genres/available', methods=['GET'])
|
||||
def get_available_genres():
|
||||
"""Get list of genres that have content in discovery pool"""
|
||||
try:
|
||||
from core.personalized_playlists import get_personalized_playlists_service
|
||||
|
||||
database = get_database()
|
||||
service = get_personalized_playlists_service(database, spotify_client)
|
||||
|
||||
genres = service.get_available_genres()
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"genres": genres
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting available genres: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/discover/genre/<path:genre_name>', methods=['GET'])
|
||||
def get_discover_genre_playlist(genre_name):
|
||||
"""Get tracks from a specific genre 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_genre_playlist(genre_name, limit=50)
|
||||
|
||||
if not tracks:
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"tracks": [],
|
||||
"genre": genre_name,
|
||||
"message": f"No tracks found for {genre_name}"
|
||||
}), 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,
|
||||
"genre": genre_name
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting genre 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"""
|
||||
|
|
|
|||
|
|
@ -2071,6 +2071,21 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Genre Browser Section -->
|
||||
<div class="discover-section">
|
||||
<div class="discover-section-header">
|
||||
<h2 class="discover-section-title">🎵 Browse by Genre</h2>
|
||||
<p class="discover-section-subtitle">Explore music by style</p>
|
||||
</div>
|
||||
<div class="discover-carousel" id="genre-browser-carousel">
|
||||
<!-- Content will be populated dynamically -->
|
||||
<div class="discover-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Loading genres...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -24742,7 +24742,8 @@ async function loadDiscoverPage() {
|
|||
loadPersonalizedForgottenFavorites(), // NEW: Forgotten favorites
|
||||
loadDiscoveryShuffle(), // NEW: Discovery Shuffle
|
||||
loadFamiliarFavorites(), // NEW: Familiar Favorites
|
||||
loadDecadeBrowser() // Decade browser
|
||||
loadDecadeBrowser(), // Decade browser
|
||||
loadGenreBrowser() // Genre browser
|
||||
]);
|
||||
|
||||
// Check for active syncs after page load
|
||||
|
|
@ -25352,6 +25353,156 @@ async function openDecadePlaylist(decade) {
|
|||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// GENRE BROWSER
|
||||
// ===============================
|
||||
|
||||
let selectedGenre = null;
|
||||
let genreTracks = [];
|
||||
|
||||
async function loadGenreBrowser() {
|
||||
try {
|
||||
const carousel = document.getElementById('genre-browser-carousel');
|
||||
if (!carousel) return;
|
||||
|
||||
// Fetch available genres from backend
|
||||
const response = await fetch('/api/discover/genres/available');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch available genres');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.genres || data.genres.length === 0) {
|
||||
carousel.innerHTML = '<div class="discover-empty"><p>No genre content available yet. Run a watchlist scan to populate your discovery pool!</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build genre cards matching Recent Releases style
|
||||
let html = '';
|
||||
data.genres.forEach(genre => {
|
||||
const icon = getGenreIcon(genre.name);
|
||||
const displayName = capitalizeGenre(genre.name);
|
||||
html += `
|
||||
<div class="discover-card genre-card-modern" onclick="openGenrePlaylist('${escapeHtml(genre.name)}')">
|
||||
<div class="discover-card-image genre-card-image">
|
||||
<div class="genre-icon-large">${icon}</div>
|
||||
</div>
|
||||
<div class="discover-card-info">
|
||||
<h4 class="discover-card-title">${displayName}</h4>
|
||||
<p class="discover-card-subtitle">${genre.track_count} tracks</p>
|
||||
<p class="discover-card-meta">Curated</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
carousel.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading genre browser:', error);
|
||||
const carousel = document.getElementById('genre-browser-carousel');
|
||||
if (carousel) {
|
||||
carousel.innerHTML = '<div class="discover-empty"><p>Failed to load genres</p></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getGenreIcon(genreName) {
|
||||
const genre = genreName.toLowerCase();
|
||||
|
||||
// Electronic/Dance
|
||||
if (genre.includes('house') || genre.includes('techno') || genre.includes('edm') ||
|
||||
genre.includes('electro') || genre.includes('trance')) {
|
||||
return '🎹';
|
||||
}
|
||||
// Hip Hop/Rap
|
||||
if (genre.includes('hip hop') || genre.includes('rap') || genre.includes('trap')) {
|
||||
return '🎤';
|
||||
}
|
||||
// Rock
|
||||
if (genre.includes('rock') || genre.includes('metal') || genre.includes('punk')) {
|
||||
return '🎸';
|
||||
}
|
||||
// Jazz/Blues
|
||||
if (genre.includes('jazz') || genre.includes('blues') || genre.includes('soul')) {
|
||||
return '🎺';
|
||||
}
|
||||
// Pop
|
||||
if (genre.includes('pop')) {
|
||||
return '🎵';
|
||||
}
|
||||
// R&B
|
||||
if (genre.includes('r&b') || genre.includes('rnb')) {
|
||||
return '🎙️';
|
||||
}
|
||||
// Country/Folk
|
||||
if (genre.includes('country') || genre.includes('folk')) {
|
||||
return '🪕';
|
||||
}
|
||||
// Classical
|
||||
if (genre.includes('classical') || genre.includes('orchestra')) {
|
||||
return '🎻';
|
||||
}
|
||||
// Indie/Alternative
|
||||
if (genre.includes('indie') || genre.includes('alternative')) {
|
||||
return '🎧';
|
||||
}
|
||||
// Lo-fi/Chill
|
||||
if (genre.includes('lo-fi') || genre.includes('chill') || genre.includes('ambient')) {
|
||||
return '☁️';
|
||||
}
|
||||
|
||||
// Default
|
||||
return '🎶';
|
||||
}
|
||||
|
||||
function capitalizeGenre(genre) {
|
||||
// Capitalize each word in genre
|
||||
return genre.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function openGenrePlaylist(genre) {
|
||||
try {
|
||||
showLoadingOverlay(`Loading ${capitalizeGenre(genre)} playlist...`);
|
||||
|
||||
const response = await fetch(`/api/discover/genre/${encodeURIComponent(genre)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch genre playlist');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||
const message = data.message || `No tracks found for ${genre}`;
|
||||
showToast(message, 'info');
|
||||
hideLoadingOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
selectedGenre = genre;
|
||||
genreTracks = data.tracks;
|
||||
|
||||
// Open download modal
|
||||
const playlistName = `${capitalizeGenre(genre)} Mix`;
|
||||
const virtualPlaylistId = `genre_${genre.replace(/\s+/g, '_')}`;
|
||||
|
||||
await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, data.tracks);
|
||||
hideLoadingOverlay();
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error opening ${genre} playlist:`, error);
|
||||
showToast(`Failed to load ${genre} playlist`, 'error');
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// SEASONAL DISCOVERY
|
||||
// ===============================
|
||||
|
|
|
|||
|
|
@ -17339,3 +17339,47 @@ body {
|
|||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* =======================
|
||||
Genre Browser Cards
|
||||
======================= */
|
||||
|
||||
.genre-card-modern {
|
||||
/* Inherits from .discover-card */
|
||||
}
|
||||
|
||||
.genre-card-image {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.genre-card-image::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(102, 126, 234, 0.1) 0%,
|
||||
rgba(118, 75, 162, 0.1) 100%);
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.genre-card-modern:hover .genre-card-image::before {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.genre-icon-large {
|
||||
font-size: 96px;
|
||||
line-height: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.genre-card-modern:hover .genre-icon-large {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue