pull genres with imges.

pull/49/head
Broque Thomas 8 months ago
parent ccc686a892
commit 0ba4b6a079

Binary file not shown.

@ -39,6 +39,7 @@ from services.sync_service import PlaylistSyncService
from datetime import datetime
import yt_dlp
from core.matching_engine import MusicMatchingEngine
from beatport_unified_scraper import BeatportUnifiedScraper
# --- Flask App Setup ---
base_dir = os.path.abspath(os.path.dirname(__file__))
@ -11855,6 +11856,161 @@ def get_active_media_server():
print(f"Error getting active media server: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# ================================= #
# BEATPORT API ENDPOINTS #
# ================================= #
@app.route('/api/beatport/genres', methods=['GET'])
def get_beatport_genres():
"""Get current Beatport genres with images dynamically scraped from homepage"""
try:
logger.info("🔍 API request for Beatport genres")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
include_images = request.args.get('include_images', 'false').lower() == 'true'
# Discover genres dynamically
if include_images:
logger.info("🖼️ Including genre images in response (slower)")
genres = scraper.discover_genres_with_images(include_images=True)
else:
logger.info("📝 Returning genres without images (faster)")
genres = scraper.discover_genres_from_homepage()
logger.info(f"✅ Successfully discovered {len(genres)} Beatport genres")
return jsonify({
"success": True,
"genres": genres,
"count": len(genres),
"includes_images": include_images
})
except Exception as e:
logger.error(f"❌ Error fetching Beatport genres: {e}")
return jsonify({
"success": False,
"error": str(e),
"genres": [],
"count": 0
}), 500
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/tracks', methods=['GET'])
def get_beatport_genre_tracks(genre_slug, genre_id):
"""Get tracks for a specific Beatport genre"""
try:
logger.info(f"🎵 API request for {genre_slug} genre tracks (ID: {genre_id})")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '100'))
# Create genre dict for scraper
genre = {
'name': genre_slug.replace('-', ' ').title(),
'slug': genre_slug,
'id': genre_id
}
# Scrape tracks for this genre
tracks = scraper.scrape_genre_charts(genre, limit=limit)
logger.info(f"✅ Successfully scraped {len(tracks)} tracks for {genre_slug}")
return jsonify({
"success": True,
"tracks": tracks,
"genre": genre,
"count": len(tracks)
})
except Exception as e:
logger.error(f"❌ Error fetching tracks for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/top-100', methods=['GET'])
def get_beatport_top_100():
"""Get Beatport Top 100 tracks"""
try:
logger.info("🔥 API request for Beatport Top 100")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '100'))
# Scrape Top 100
tracks = scraper.scrape_top_100(limit=limit)
logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Top 100")
return jsonify({
"success": True,
"tracks": tracks,
"chart_name": "Beatport Top 100",
"count": len(tracks)
})
except Exception as e:
logger.error(f"❌ Error fetching Beatport Top 100: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/genre-image/<genre_slug>/<genre_id>', methods=['GET'])
def get_beatport_genre_image(genre_slug, genre_id):
"""Get image for a specific Beatport genre"""
try:
logger.info(f"🖼️ API request for {genre_slug} genre image")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Construct genre URL
genre_url = f"{scraper.base_url}/genre/{genre_slug}/{genre_id}"
# Get genre image
image_url = scraper.get_genre_image(genre_url)
if image_url:
logger.info(f"✅ Found image for {genre_slug}")
return jsonify({
"success": True,
"image_url": image_url,
"genre_slug": genre_slug,
"genre_id": genre_id
})
else:
logger.info(f"⚠️ No image found for {genre_slug}")
return jsonify({
"success": False,
"image_url": None,
"genre_slug": genre_slug,
"genre_id": genre_id
})
except Exception as e:
logger.error(f"❌ Error fetching image for {genre_slug}: {e}")
return jsonify({
"success": False,
"error": str(e),
"image_url": None
}), 500
class WebMetadataUpdateWorker:
"""Web-based metadata update worker - EXACT port of dashboard.py MetadataUpdateWorker"""

@ -409,26 +409,183 @@
</div>
<div class="beatport-navigation">
<div class="beatport-category-cards">
<div class="beatport-category-card" data-category="top-charts">
<div class="category-icon top-charts-icon"></div>
<h3>🔥 Top Charts</h3>
<p>Beatport Top 100, Hype Top 10, New Releases</p>
<span class="category-count">3 Charts</span>
<!-- Main Category Cards View -->
<div class="beatport-main-view active" id="beatport-main-view">
<div class="beatport-category-cards">
<div class="beatport-category-card" data-category="top-charts">
<div class="category-icon top-charts-icon"></div>
<h3>🔥 Top Charts</h3>
<p>Beatport Top 100, Hype Top 10, New Releases</p>
<span class="category-count">3 Charts</span>
</div>
<div class="beatport-category-card" data-category="genres">
<div class="category-icon genres-icon"></div>
<h3>🎵 Genre Explorer</h3>
<p>House, Techno, Trance, and 36 more genres</p>
<span class="category-count">39 Genres</span>
</div>
<div class="beatport-category-card" data-category="staff-picks">
<div class="category-icon staff-picks-icon"></div>
<h3>📊 Staff Picks</h3>
<p>Curated selections and secret weapons</p>
<span class="category-count">5 Collections</span>
</div>
</div>
</div>
<!-- Top Charts Sub-View -->
<div class="beatport-sub-view" id="beatport-top-charts-view">
<div class="beatport-breadcrumb">
<button class="breadcrumb-back">← Back to Categories</button>
<span class="breadcrumb-path">Browse Charts > Top Charts</span>
</div>
<div class="beatport-chart-list">
<div class="beatport-chart-item" data-chart-type="top-100" data-chart-id="top-100">
<div class="chart-icon">🏆</div>
<div class="chart-info">
<h3>Beatport Top 100</h3>
<p>The hottest electronic tracks right now</p>
<span class="track-count">100 tracks</span>
</div>
</div>
<div class="beatport-chart-item" data-chart-type="hype" data-chart-id="hype-10">
<div class="chart-icon">🚀</div>
<div class="chart-info">
<h3>Hype Top 10</h3>
<p>Rising stars and breakthrough tracks</p>
<span class="track-count">10 tracks</span>
</div>
</div>
<div class="beatport-chart-item" data-chart-type="new-releases" data-chart-id="new-releases">
<div class="chart-icon"></div>
<div class="chart-info">
<h3>New Releases</h3>
<p>Fresh tracks added to Beatport</p>
<span class="track-count">50 tracks</span>
</div>
</div>
</div>
</div>
<div class="beatport-category-card" data-category="genres">
<div class="category-icon genres-icon"></div>
<h3>🎵 Genre Explorer</h3>
<p>House, Techno, Trance, and 36 more genres</p>
<span class="category-count">39 Genres</span>
<!-- Genre Explorer Sub-View -->
<div class="beatport-sub-view" id="beatport-genres-view">
<div class="beatport-breadcrumb">
<button class="breadcrumb-back">← Back to Categories</button>
<span class="breadcrumb-path">Browse Charts > Genre Explorer</span>
</div>
<div class="beatport-genre-grid">
<div class="beatport-genre-item" data-genre-slug="house" data-genre-id="5">
<div class="genre-icon">🏠</div>
<h3>House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="tech-house" data-genre-id="11">
<div class="genre-icon">🔧</div>
<h3>Tech House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="techno" data-genre-id="6">
<div class="genre-icon"></div>
<h3>Techno</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="deep-house" data-genre-id="12">
<div class="genre-icon">🌊</div>
<h3>Deep House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="trance" data-genre-id="7">
<div class="genre-icon">🌀</div>
<h3>Trance</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="drum-and-bass" data-genre-id="1">
<div class="genre-icon">🥁</div>
<h3>Drum & Bass</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="dubstep" data-genre-id="18">
<div class="genre-icon">🎵</div>
<h3>Dubstep</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="progressive-house" data-genre-id="15">
<div class="genre-icon">📈</div>
<h3>Progressive House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="melodic-house-and-techno" data-genre-id="90">
<div class="genre-icon">🎼</div>
<h3>Melodic House & Techno</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="afro-house" data-genre-id="89">
<div class="genre-icon">🌍</div>
<h3>Afro House</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="minimal" data-genre-id="14">
<div class="genre-icon"></div>
<h3>Minimal</h3>
<span class="genre-track-count">Top 100</span>
</div>
<div class="beatport-genre-item" data-genre-slug="nu-disco" data-genre-id="50">
<div class="genre-icon"></div>
<h3>Nu Disco</h3>
<span class="genre-track-count">Top 100</span>
</div>
</div>
</div>
<div class="beatport-category-card" data-category="staff-picks">
<div class="category-icon staff-picks-icon"></div>
<h3>📊 Staff Picks</h3>
<p>Curated selections and secret weapons</p>
<span class="category-count">5 Collections</span>
<!-- Staff Picks Sub-View -->
<div class="beatport-sub-view" id="beatport-staff-picks-view">
<div class="beatport-breadcrumb">
<button class="breadcrumb-back">← Back to Categories</button>
<span class="breadcrumb-path">Browse Charts > Staff Picks</span>
</div>
<div class="beatport-chart-list">
<div class="beatport-chart-item" data-chart-type="staff-pick" data-chart-id="best-new-tracks">
<div class="chart-icon"></div>
<div class="chart-info">
<h3>Best New Tracks</h3>
<p>Editor's picks of the week</p>
<span class="track-count">20 tracks</span>
</div>
</div>
<div class="beatport-chart-item" data-chart-type="staff-pick" data-chart-id="secret-weapons">
<div class="chart-icon">🔫</div>
<div class="chart-info">
<h3>Secret Weapons</h3>
<p>Underground gems for DJs</p>
<span class="track-count">15 tracks</span>
</div>
</div>
<div class="beatport-chart-item" data-chart-type="staff-pick" data-chart-id="closing-essentials">
<div class="chart-icon">🌅</div>
<div class="chart-info">
<h3>Closing Essentials</h3>
<p>Perfect tracks to end the night</p>
<span class="track-count">12 tracks</span>
</div>
</div>
<div class="beatport-chart-item" data-chart-type="staff-pick" data-chart-id="peak-time-driving">
<div class="chart-icon">🔥</div>
<div class="chart-info">
<h3>Peak Time Driving</h3>
<p>High-energy dancefloor destroyers</p>
<span class="track-count">25 tracks</span>
</div>
</div>
<div class="beatport-chart-item" data-chart-type="staff-pick" data-chart-id="underground-gems">
<div class="chart-icon">💎</div>
<div class="chart-info">
<h3>Underground Gems</h3>
<p>Hidden treasures from emerging artists</p>
<span class="track-count">18 tracks</span>
</div>
</div>
</div>
</div>
</div>

@ -9223,6 +9223,34 @@ function initializeSyncPage() {
});
});
// Logic for Beatport breadcrumb back buttons
const beatportBackButtons = document.querySelectorAll('.breadcrumb-back');
beatportBackButtons.forEach(button => {
button.addEventListener('click', () => {
showBeatportMainView();
});
});
// Logic for Beatport chart items
const beatportChartItems = document.querySelectorAll('.beatport-chart-item');
beatportChartItems.forEach(item => {
item.addEventListener('click', () => {
const chartType = item.dataset.chartType;
const chartId = item.dataset.chartId;
handleBeatportChartClick(chartType, chartId);
});
});
// Logic for Beatport genre items
const beatportGenreItems = document.querySelectorAll('.beatport-genre-item');
beatportGenreItems.forEach(item => {
item.addEventListener('click', () => {
const genreSlug = item.dataset.genreSlug;
const genreId = item.dataset.genreId;
handleBeatportGenreClick(genreSlug, genreId);
});
});
// Logic for the Start Sync button
const startSyncBtn = document.getElementById('start-sync-btn');
if (startSyncBtn) {
@ -9541,22 +9569,236 @@ async function loadBeatportCharts() {
function handleBeatportCategoryClick(category) {
console.log(`🎵 Beatport category clicked: ${category}`);
// Placeholder functionality for category navigation
// Show the appropriate sub-view based on category
switch(category) {
case 'top-charts':
showToast('🔥 Top Charts navigation coming soon!', 'info');
showBeatportSubView('top-charts');
break;
case 'genres':
showToast('🎵 Genre Explorer navigation coming soon!', 'info');
showBeatportSubView('genres');
loadBeatportGenres(); // Load genres dynamically
break;
case 'staff-picks':
showToast('📊 Staff Picks navigation coming soon!', 'info');
showBeatportSubView('staff-picks');
break;
default:
showToast(`Category "${category}" clicked`, 'info');
showToast(`Unknown category: ${category}`, 'error');
}
}
async function loadBeatportGenres() {
console.log('🔍 Loading Beatport genres dynamically...');
const genreGrid = document.querySelector('#beatport-genres-view .beatport-genre-grid');
if (!genreGrid) {
console.error('❌ Could not find genre grid element');
return;
}
// Show loading state
genreGrid.innerHTML = `
<div class="genre-loading-placeholder">
<div class="loading-spinner"></div>
<p>🔍 Discovering current Beatport genres...</p>
</div>
`;
try {
// First, fetch genres quickly without images
console.log('🚀 Fetching genres without images for fast loading...');
const fastResponse = await fetch('/api/beatport/genres');
if (!fastResponse.ok) {
throw new Error(`API returned ${fastResponse.status}: ${fastResponse.statusText}`);
}
const fastData = await fastResponse.json();
const genres = fastData.genres || [];
if (genres.length === 0) {
genreGrid.innerHTML = `
<div class="genre-error-placeholder">
<p> No genres available</p>
<button onclick="loadBeatportGenres()" class="refresh-genres-btn">🔄 Retry</button>
</div>
`;
return;
}
// Generate genre cards dynamically (without images first)
const genreCardsHTML = genres.map(genre => `
<div class="beatport-genre-item"
data-genre-slug="${genre.slug}"
data-genre-id="${genre.id}"
data-genre-name="${genre.name}">
<div class="genre-icon">🎵</div>
<h3>${genre.name}</h3>
<span class="track-count">Top 100</span>
</div>
`).join('');
genreGrid.innerHTML = genreCardsHTML;
// Add click handlers to dynamically created genre items
const genreItems = genreGrid.querySelectorAll('.beatport-genre-item');
genreItems.forEach(item => {
item.addEventListener('click', () => {
const genreSlug = item.dataset.genreSlug;
const genreId = item.dataset.genreId;
const genreName = item.dataset.genreName;
handleBeatportGenreClick(genreSlug, genreId, genreName);
});
});
console.log(`✅ Loaded ${genres.length} Beatport genres dynamically (fast mode)`);
showToast(`Loaded ${genres.length} current Beatport genres`, 'success');
// Now fetch images progressively in the background if there are many genres
if (genres.length > 10) {
console.log('🖼️ Loading genre images progressively...');
loadGenreImagesProgressively(genres);
}
} catch (error) {
console.error('❌ Error loading Beatport genres:', error);
genreGrid.innerHTML = `
<div class="genre-error-placeholder">
<p> Failed to load genres: ${error.message}</p>
<button onclick="loadBeatportGenres()" class="refresh-genres-btn">🔄 Retry</button>
</div>
`;
showToast(`Error loading Beatport genres: ${error.message}`, 'error');
}
}
async function loadGenreImagesProgressively(genres) {
// Load genre images with 2 concurrent workers for faster loading
const imageQueue = [...genres]; // Create a copy for processing
let imagesLoaded = 0;
const maxWorkers = 2;
console.log(`🖼️ Starting progressive image loading with ${maxWorkers} workers for ${imageQueue.length} genres`);
// Function to process a single image
async function processImage(genre) {
try {
// Fetch individual genre image from backend
const response = await fetch(`/api/beatport/genre-image/${genre.slug}/${genre.id}`);
if (response.ok) {
const data = await response.json();
if (data.success && data.image_url) {
// Find the genre item in the DOM
const genreItem = document.querySelector(
`[data-genre-slug="${genre.slug}"][data-genre-id="${genre.id}"]`
);
if (genreItem) {
const iconElement = genreItem.querySelector('.genre-icon');
if (iconElement) {
// Create new image element with smooth transition
const imageDiv = document.createElement('div');
imageDiv.className = 'genre-image';
imageDiv.style.backgroundImage = `url('${data.image_url}')`;
imageDiv.style.opacity = '0';
imageDiv.style.transition = 'opacity 0.3s ease';
// Replace icon with image
iconElement.replaceWith(imageDiv);
// Trigger fade-in animation
setTimeout(() => {
imageDiv.style.opacity = '1';
}, 50);
imagesLoaded++;
console.log(`🖼️ [${imagesLoaded}/${imageQueue.length}] Loaded image for ${genre.name}`);
}
}
}
}
} catch (error) {
console.warn(`⚠️ Failed to load image for ${genre.name}:`, error);
}
}
// Worker function that processes images from the queue
async function imageWorker(workerId) {
while (imageQueue.length > 0) {
const genre = imageQueue.shift(); // Take next image from queue
if (genre) {
await processImage(genre);
// Small delay between requests to be respectful (500ms per worker = ~2 images per second total)
await new Promise(resolve => setTimeout(resolve, 500));
}
}
console.log(`✅ Worker ${workerId} finished`);
}
// Start the workers
const workers = [];
for (let i = 0; i < maxWorkers; i++) {
workers.push(imageWorker(i + 1));
}
// Wait for all workers to complete
await Promise.all(workers);
console.log(`✅ Progressive image loading complete: ${imagesLoaded}/${genres.length} images loaded`);
}
function showBeatportSubView(viewType) {
// Hide main category view
const mainView = document.getElementById('beatport-main-view');
if (mainView) {
mainView.classList.remove('active');
}
// Hide all sub-views
document.querySelectorAll('.beatport-sub-view').forEach(view => {
view.classList.remove('active');
});
// Show the requested sub-view
const targetView = document.getElementById(`beatport-${viewType}-view`);
if (targetView) {
targetView.classList.add('active');
console.log(`🎵 Showing Beatport ${viewType} view`);
} else {
console.error(`🎵 Could not find view: beatport-${viewType}-view`);
}
}
function showBeatportMainView() {
// Hide all sub-views
document.querySelectorAll('.beatport-sub-view').forEach(view => {
view.classList.remove('active');
});
// Show main category view
const mainView = document.getElementById('beatport-main-view');
if (mainView) {
mainView.classList.add('active');
console.log('🎵 Showing Beatport main view');
}
}
function handleBeatportChartClick(chartType, chartId) {
console.log(`🎵 Beatport chart clicked: ${chartType} - ${chartId}`);
// Placeholder for Phase 2 - will open discovery modal
showToast(`🎵 Chart "${chartId}" selected - Discovery modal coming in Phase 2!`, 'info');
}
function handleBeatportGenreClick(genreSlug, genreId) {
console.log(`🎵 Beatport genre clicked: ${genreSlug} (${genreId})`);
// Placeholder for Phase 2 - will open discovery modal
showToast(`🎵 Genre "${genreSlug}" selected - Discovery modal coming in Phase 2!`, 'info');
}
// ===============================
// YOUTUBE PLAYLIST FUNCTIONALITY
// ===============================

@ -4542,6 +4542,359 @@ body {
z-index: 2;
}
/* ================================= */
/* BEATPORT NAVIGATION VIEWS */
/* ================================= */
.beatport-main-view,
.beatport-sub-view {
display: none;
height: 100%;
overflow-y: auto;
padding: 0 5px;
}
.beatport-main-view.active,
.beatport-sub-view.active {
display: block;
}
/* ================================= */
/* BEATPORT BREADCRUMB NAVIGATION */
/* ================================= */
.beatport-breadcrumb {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 25px;
padding: 15px 20px;
background: rgba(25, 25, 25, 0.6);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.breadcrumb-back {
background: linear-gradient(135deg, #01FF95 0%, #00E085 100%);
border: none;
border-radius: 8px;
color: #000;
font-size: 13px;
font-weight: 600;
padding: 8px 16px;
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.breadcrumb-back:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(1, 255, 149, 0.3);
}
.breadcrumb-path {
color: #888;
font-size: 14px;
font-weight: 500;
}
/* ================================= */
/* BEATPORT CHART LIST VIEW */
/* ================================= */
.beatport-chart-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.beatport-chart-item {
background: linear-gradient(135deg,
rgba(25, 25, 25, 0.95) 0%,
rgba(15, 15, 15, 0.98) 100%);
backdrop-filter: blur(20px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid rgba(255, 255, 255, 0.12);
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 20px;
position: relative;
overflow: hidden;
}
.beatport-chart-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(1, 255, 149, 0.1) 0%,
transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.beatport-chart-item:hover {
transform: translateY(-3px);
border-color: rgba(1, 255, 149, 0.3);
box-shadow:
0 15px 30px rgba(0, 0, 0, 0.3),
0 0 20px rgba(1, 255, 149, 0.2);
}
.beatport-chart-item:hover::before {
opacity: 1;
}
.chart-icon {
font-size: 32px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #01FF95 0%, #00E085 100%);
border-radius: 12px;
flex-shrink: 0;
position: relative;
z-index: 2;
}
.chart-info {
flex: 1;
position: relative;
z-index: 2;
}
.chart-info h3 {
font-size: 18px;
font-weight: 600;
color: #ffffff;
margin: 0 0 5px 0;
}
.chart-info p {
font-size: 14px;
color: #888;
margin: 0 0 8px 0;
line-height: 1.4;
}
.track-count {
display: inline-block;
background: rgba(1, 255, 149, 0.15);
color: #01FF95;
font-size: 12px;
font-weight: 600;
padding: 4px 10px;
border-radius: 15px;
border: 1px solid rgba(1, 255, 149, 0.3);
}
/* ================================= */
/* BEATPORT GENRE GRID VIEW */
/* ================================= */
.beatport-genre-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
padding: 10px 0;
}
.beatport-genre-item {
background: linear-gradient(135deg,
rgba(25, 25, 25, 0.95) 0%,
rgba(15, 15, 15, 0.98) 100%);
backdrop-filter: blur(20px);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-top: 1px solid rgba(255, 255, 255, 0.12);
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
position: relative;
overflow: hidden;
min-height: 120px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
}
.beatport-genre-item::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg,
rgba(1, 255, 149, 0.1) 0%,
transparent 50%);
opacity: 0;
transition: opacity 0.3s ease;
}
.beatport-genre-item:hover {
transform: translateY(-5px);
border-color: rgba(1, 255, 149, 0.3);
box-shadow:
0 15px 30px rgba(0, 0, 0, 0.3),
0 0 20px rgba(1, 255, 149, 0.2);
}
.beatport-genre-item:hover::before {
opacity: 1;
}
.genre-icon {
font-size: 28px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #01FF95 0%, #00E085 100%);
border-radius: 10px;
position: relative;
z-index: 2;
}
.beatport-genre-item h3 {
font-size: 16px;
font-weight: 600;
color: #ffffff;
margin: 0;
position: relative;
z-index: 2;
text-align: center;
line-height: 1.2;
}
.genre-track-count {
display: inline-block;
background: rgba(1, 255, 149, 0.15);
color: #01FF95;
font-size: 11px;
font-weight: 600;
padding: 4px 8px;
border-radius: 12px;
border: 1px solid rgba(1, 255, 149, 0.3);
position: relative;
z-index: 2;
}
/* Dynamic Genre Loading States */
.genre-loading-placeholder,
.genre-error-placeholder {
grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
color: #888;
}
.genre-loading-placeholder .loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(1, 255, 149, 0.2);
border-top: 3px solid #01FF95;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.refresh-genres-btn {
background: linear-gradient(135deg, #01FF95 0%, #00E085 100%);
color: #000;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
margin-top: 15px;
transition: all 0.3s ease;
}
.refresh-genres-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(1, 255, 149, 0.3);
}
/* Genre Images */
.beatport-genre-item .genre-image {
width: 50px;
height: 50px;
border-radius: 12px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
border: 2px solid rgba(1, 255, 149, 0.3);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
position: relative;
z-index: 2;
}
.beatport-genre-item:hover .genre-image {
transform: scale(1.1);
border-color: rgba(1, 255, 149, 0.6);
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.4),
0 0 20px rgba(1, 255, 149, 0.3);
}
/* ================================= */
/* RESPONSIVE DESIGN */
/* ================================= */
@media (max-width: 768px) {
.beatport-genre-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
}
.beatport-chart-item {
flex-direction: column;
text-align: center;
gap: 15px;
}
.chart-icon {
width: 50px;
height: 50px;
font-size: 24px;
}
.beatport-breadcrumb {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.breadcrumb-back {
text-align: center;
}
}
.playlist-scroll-container {
flex-grow: 1;
overflow-y: auto;

Loading…
Cancel
Save