pull genres with imges.
This commit is contained in:
parent
ccc686a892
commit
0ba4b6a079
6 changed files with 931 additions and 23 deletions
BIN
database/music_library.db-shm
Normal file
BIN
database/music_library.db-shm
Normal file
Binary file not shown.
0
database/music_library.db-wal
Normal file
0
database/music_library.db-wal
Normal file
156
web_server.py
156
web_server.py
|
|
@ -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"""
|
||||
|
||||
|
|
|
|||
193
webui/index.html
193
webui/index.html
|
|
@ -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>
|
||||
</div>
|
||||
<!-- 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="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 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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…
Reference in a new issue