diff --git a/web_server.py b/web_server.py index 1e85c203..3ffed756 100644 --- a/web_server.py +++ b/web_server.py @@ -2342,6 +2342,129 @@ def get_beatport_featured_charts(): 'charts': [] }), 500 + +@app.route('/api/beatport/dj-charts') +def get_beatport_dj_charts(): + """Get DJ charts from Beatport for the DJ charts slider using Carousel approach""" + try: + logger.info("🎧 Fetching Beatport DJ charts...") + + # Initialize scraper + scraper = BeatportUnifiedScraper() + + # Get page and extract charts + soup = scraper.get_page(scraper.base_url) + if not soup: + raise Exception("Could not fetch Beatport homepage") + + # Find all Carousel containers + carousels = soup.select('[class*="Carousel-style__Wrapper"]') + dj_container = None + + logger.info(f"🔍 Checking {len(carousels)} Carousel containers for DJ charts...") + + # Based on test results, DJ charts are in the second carousel (index 1) with ~9 chart links + for i, container in enumerate(carousels): + chart_links = container.select('a[href*="/chart/"]') + logger.info(f"📋 Carousel {i+1}: {len(chart_links)} chart links") + + # DJ charts container typically has 8-12 chart links (not 99+ like featured charts) + if 5 <= len(chart_links) <= 15: + dj_container = container + logger.info(f"🔥 FOUND DJ CHARTS: Carousel {i+1} with {len(chart_links)} charts") + break + + if not dj_container: + logger.warning("❌ No DJ Charts Carousel container found") + return jsonify({ + 'success': False, + 'error': 'DJ Charts section not found', + 'charts': [] + }) + + # Extract charts from the container using chart links + charts = [] + chart_links = dj_container.select('a[href*="/chart/"]') + + logger.info(f"📊 Found {len(chart_links)} DJ chart links") + + for i, link in enumerate(chart_links): + chart_data = {} + + # Extract chart name from link text or nearby elements + name_elem = link.select_one('h3, h4, h5, h6, [class*="title"], [class*="Title"], [class*="name"], [class*="Name"]') + if name_elem: + name_text = name_elem.get_text(strip=True) + else: + name_text = link.get_text(strip=True) + + if name_text and len(name_text) > 2: + chart_data['name'] = name_text + + # Extract creator - for DJ charts, the chart name might be the artist name + creator = name_text # Use chart name as creator for DJ charts + + # Look for additional creator info in parent elements + parent = link.parent + if parent: + creator_selectors = [ + '[class*="artist"]', '[class*="Artist"]', + '[class*="creator"]', '[class*="Creator"]', + '[class*="author"]', '[class*="Author"]' + ] + + for selector in creator_selectors: + creator_elem = parent.select_one(selector) + if creator_elem: + creator_text = creator_elem.get_text(strip=True) + if creator_text and len(creator_text) > 1 and creator_text != name_text: + creator = creator_text + break + + chart_data['creator'] = creator + + # Extract URL + href = link.get('href', '') + if href: + if href.startswith('/'): + chart_data['url'] = f"https://www.beatport.com{href}" + else: + chart_data['url'] = href + + # Extract image + img_elem = link.select_one('img') or (link.parent.select_one('img') if link.parent else None) + if img_elem: + src = img_elem.get('src', '') or img_elem.get('data-src', '') + if src: + if src.startswith('//'): + src = f"https:{src}" + elif src.startswith('/'): + src = f"https://www.beatport.com{src}" + chart_data['image'] = src + + # Only add if we have meaningful data + if 'name' in chart_data and 'url' in chart_data: + charts.append(chart_data) + logger.info(f"✅ DJ Chart {len(charts)}: {chart_data['name']} by {chart_data['creator']}") + + logger.info(f"📊 Successfully extracted {len(charts)} DJ charts") + + return jsonify({ + 'success': True, + 'charts': charts, + 'count': len(charts), + 'slides': max(1, (len(charts) + 2) // 3), # 3 cards per slide + 'timestamp': datetime.now().isoformat() + }) + + except Exception as e: + logger.error(f"❌ Error fetching DJ charts: {str(e)}") + return jsonify({ + 'success': False, + 'error': str(e), + 'charts': [] + }), 500 + # --- Placeholder API Endpoints for Other Pages --- @app.route('/api/activity') diff --git a/webui/index.html b/webui/index.html index 8079084c..55beb3b0 100644 --- a/webui/index.html +++ b/webui/index.html @@ -871,6 +871,39 @@ + + +
+
+

🎧 DJ Charts

+

Curated charts from top DJs and artists

+
+ +
+
+
+ +
+
+

🎧 Loading DJ Charts...

+

Fetching curated DJ selections

+
+
+
+ + +
+ + +
+ + +
+ +
+
+
+
diff --git a/webui/static/script.js b/webui/static/script.js index c39d53e5..7cee8ca8 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -9736,6 +9736,7 @@ function initializeSyncPage() { initializeBeatportRebuildSlider(); initializeBeatportReleasesSlider(); initializeBeatportChartsSlider(); + initializeBeatportDJSlider(); } }); }); @@ -19108,3 +19109,283 @@ function cleanupBeatportChartsSlider() { beatportChartsSliderState.autoPlayInterval = null; } } + +// =================================== +// BEATPORT DJ CHARTS SLIDER +// =================================== + +// State management for DJ charts slider (3 cards per slide) +let beatportDJSliderState = { + currentSlide: 0, + totalSlides: 0, + autoPlayInterval: null, + autoPlayDelay: 12000, // Longer auto-play for DJ charts + isInitialized: false +}; + +/** + * Initialize the beatport DJ charts slider functionality (based on charts slider) + */ +function initializeBeatportDJSlider() { + console.log('🎧 Initializing beatport DJ charts slider...'); + + const slider = document.getElementById('beatport-dj-slider'); + if (!slider) { + console.warn('Beatport DJ slider not found'); + return; + } + + // Prevent double initialization + if (slider.dataset.initialized === 'true') { + console.log('DJ slider already initialized'); + return; + } + + const sliderTrack = document.getElementById('beatport-dj-slider-track'); + const indicatorsContainer = document.getElementById('beatport-dj-slider-indicators'); + + if (!sliderTrack || !indicatorsContainer) { + console.warn('DJ slider elements not found'); + return; + } + + // Load data and initialize + loadBeatportDJCharts().then(success => { + if (success) { + setupBeatportDJSliderNavigation(); + setupBeatportDJSliderIndicators(); + setupBeatportDJSliderHoverPause(); + startBeatportDJSliderAutoPlay(); + slider.dataset.initialized = 'true'; + beatportDJSliderState.isInitialized = true; + console.log('✅ DJ charts slider initialized successfully'); + } + }); +} + +/** + * Load DJ charts data from API + */ +async function loadBeatportDJCharts() { + try { + console.log('🎧 Loading DJ charts data...'); + const response = await fetch('/api/beatport/dj-charts'); + const data = await response.json(); + + if (data.success && data.charts && data.charts.length > 0) { + console.log(`📈 Loaded ${data.charts.length} DJ charts`); + createBeatportDJSlides(data.charts); + return true; + } else { + console.warn('No DJ charts data available'); + return false; + } + } catch (error) { + console.error('❌ Error loading DJ charts:', error); + return false; + } +} + +/** + * Create DJ chart slides with 3 cards per slide layout + */ +function createBeatportDJSlides(charts) { + const sliderTrack = document.getElementById('beatport-dj-slider-track'); + const indicatorsContainer = document.getElementById('beatport-dj-slider-indicators'); + + if (!sliderTrack || !indicatorsContainer) { + console.error('DJ slider elements not found'); + return; + } + + const cardsPerSlide = 3; // 3 cards per slide for DJ charts + const totalSlides = Math.ceil(charts.length / cardsPerSlide); + + // Clear existing content + sliderTrack.innerHTML = ''; + indicatorsContainer.innerHTML = ''; + + // Update state + beatportDJSliderState.totalSlides = totalSlides; + beatportDJSliderState.currentSlide = 0; + + console.log(`🎯 Creating ${totalSlides} DJ chart slides with ${cardsPerSlide} cards each`); + + // Generate slides HTML + for (let slideIndex = 0; slideIndex < totalSlides; slideIndex++) { + const startIndex = slideIndex * cardsPerSlide; + const endIndex = Math.min(startIndex + cardsPerSlide, charts.length); + const slideCharts = charts.slice(startIndex, endIndex); + + // Create grid HTML for this slide + const gridHtml = slideCharts.map(chart => { + const bgImageStyle = chart.image ? `--dj-bg-image: url('${chart.image}')` : ''; + return ` +
+
+
${chart.name || 'Unknown Chart'}
+
${chart.creator || 'Unknown Creator'}
+
+
+ `; + }).join(''); + + // Create slide HTML + const slideHtml = ` +
+
+ ${gridHtml} +
+
+ `; + + sliderTrack.innerHTML += slideHtml; + + // Create indicator + const indicatorHtml = ``; + indicatorsContainer.innerHTML += indicatorHtml; + } + + console.log(`✅ Created ${totalSlides} DJ chart slides`); +} + +/** + * Set up navigation functionality (copied from charts slider with button cloning) + */ +function setupBeatportDJSliderNavigation() { + const prevBtn = document.getElementById('beatport-dj-prev-btn'); + const nextBtn = document.getElementById('beatport-dj-next-btn'); + + if (prevBtn) { + // Clone button to remove all existing event listeners + const newPrevBtn = prevBtn.cloneNode(true); + prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn); + + newPrevBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('Previous DJ button clicked, current slide:', beatportDJSliderState.currentSlide); + goToBeatportDJSlide(beatportDJSliderState.currentSlide - 1); + resetBeatportDJSliderAutoPlay(); + }); + } + + if (nextBtn) { + // Clone button to remove all existing event listeners + const newNextBtn = nextBtn.cloneNode(true); + nextBtn.parentNode.replaceChild(newNextBtn, nextBtn); + + newNextBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('Next DJ button clicked, current slide:', beatportDJSliderState.currentSlide); + goToBeatportDJSlide(beatportDJSliderState.currentSlide + 1); + resetBeatportDJSliderAutoPlay(); + }); + } +} + +/** + * Set up indicator functionality (copied from charts slider) + */ +function setupBeatportDJSliderIndicators() { + const indicators = document.querySelectorAll('.beatport-dj-indicator'); + + indicators.forEach((indicator, index) => { + indicator.addEventListener('click', () => { + goToBeatportDJSlide(index); + resetBeatportDJSliderAutoPlay(); + }); + }); +} + +/** + * Navigate to a specific slide (copied from charts slider) + */ +function goToBeatportDJSlide(slideIndex) { + console.log('goToBeatportDJSlide called with:', slideIndex, 'current:', beatportDJSliderState.currentSlide); + + // Wrap around if out of bounds + if (slideIndex < 0) { + slideIndex = beatportDJSliderState.totalSlides - 1; + } else if (slideIndex >= beatportDJSliderState.totalSlides) { + slideIndex = 0; + } + + console.log('After wrapping, slideIndex:', slideIndex); + + // Update current slide + beatportDJSliderState.currentSlide = slideIndex; + + // Update slide visibility + const slides = document.querySelectorAll('.beatport-dj-slide'); + slides.forEach((slide, index) => { + slide.classList.remove('active', 'prev', 'next'); + + if (index === slideIndex) { + slide.classList.add('active'); + } else if (index < slideIndex) { + slide.classList.add('prev'); + } else { + slide.classList.add('next'); + } + }); + + // Update indicators + const indicators = document.querySelectorAll('.beatport-dj-indicator'); + indicators.forEach((indicator, index) => { + indicator.classList.toggle('active', index === slideIndex); + }); + + console.log('DJ slide updated to:', beatportDJSliderState.currentSlide); +} + +/** + * Start auto-play functionality (copied from charts slider) + */ +function startBeatportDJSliderAutoPlay() { + if (beatportDJSliderState.autoPlayInterval) { + clearInterval(beatportDJSliderState.autoPlayInterval); + } + + beatportDJSliderState.autoPlayInterval = setInterval(() => { + goToBeatportDJSlide(beatportDJSliderState.currentSlide + 1); + }, beatportDJSliderState.autoPlayDelay); +} + +/** + * Reset auto-play timer (copied from charts slider) + */ +function resetBeatportDJSliderAutoPlay() { + startBeatportDJSliderAutoPlay(); +} + +/** + * Set up hover pause functionality (copied from charts slider) + */ +function setupBeatportDJSliderHoverPause() { + const sliderContainer = document.querySelector('.beatport-dj-slider-container'); + + if (sliderContainer) { + sliderContainer.addEventListener('mouseenter', () => { + if (beatportDJSliderState.autoPlayInterval) { + clearInterval(beatportDJSliderState.autoPlayInterval); + beatportDJSliderState.autoPlayInterval = null; + } + }); + + sliderContainer.addEventListener('mouseleave', () => { + startBeatportDJSliderAutoPlay(); + }); + } +} + +/** + * Clean up DJ slider when switching away (copied from charts slider) + */ +function cleanupBeatportDJSlider() { + if (beatportDJSliderState.autoPlayInterval) { + clearInterval(beatportDJSliderState.autoPlayInterval); + beatportDJSliderState.autoPlayInterval = null; + } +} diff --git a/webui/static/style.css b/webui/static/style.css index 7584b851..a7c01422 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -12793,3 +12793,369 @@ body { font-size: 24px; } } + +/* ==================== DJ CHARTS SLIDER STYLES ==================== */ + +/* DJ Section */ +.beatport-dj-section { + margin-top: 40px; + padding: 0 30px; +} + +.beatport-dj-header { + text-align: center; + margin-bottom: 30px; +} + +.beatport-dj-title { + font-size: 32px; + font-weight: 700; + color: #ffffff; + margin-bottom: 8px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); +} + +.beatport-dj-subtitle { + font-size: 16px; + color: rgba(255, 255, 255, 0.8); + margin: 0; +} + +/* DJ Slider Container */ +.beatport-dj-slider-container { + position: relative; + background: linear-gradient(135deg, + rgba(147, 51, 234, 0.1), + rgba(79, 70, 229, 0.05)); + border-radius: 20px; + border: 1px solid rgba(147, 51, 234, 0.2); + backdrop-filter: blur(15px); + min-height: 300px; + padding: 30px; +} + +.beatport-dj-slider { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + border-radius: 16px; +} + +.beatport-dj-slider-track { + position: relative; + height: 300px; +} + +.beatport-dj-slide { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + transition: all 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94); + transform: translateX(100px); +} + +.beatport-dj-slide.active { + opacity: 1; + z-index: 10; + transform: translateX(0); +} + +.beatport-dj-slide.prev { + opacity: 0; + transform: translateX(-100px); +} + +.beatport-dj-slide.next { + opacity: 0; + transform: translateX(100px); +} + +/* DJ Cards Layout - 3 cards per slide */ +.beatport-dj-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 25px; + padding: 20px; + background: linear-gradient(135deg, + rgba(16, 16, 16, 0.6), + rgba(24, 24, 24, 0.4)); + border-radius: 16px; + border: 1px solid rgba(147, 51, 234, 0.15); + justify-items: center; + align-items: center; + margin: 0 auto; +} + +/* DJ Chart Card Styling - Unique vertical design */ +.beatport-dj-card { + position: relative; + width: 100%; + height: 220px; + min-height: 220px; + max-height: 220px; + border-radius: 20px; + cursor: pointer; + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); + overflow: hidden; + background: linear-gradient(135deg, + rgba(147, 51, 234, 0.8), + rgba(79, 70, 229, 0.6)); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.beatport-dj-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--dj-bg-image, linear-gradient(135deg, #9333ea, #4f46e5)); + background-size: cover; + background-position: center; + border-radius: 20px; + z-index: 1; +} + +.beatport-dj-card::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient( + 180deg, + rgba(147, 51, 234, 0.3) 0%, + rgba(0, 0, 0, 0.4) 50%, + rgba(0, 0, 0, 0.8) 100% + ); + border-radius: 20px; + z-index: 2; +} + +.beatport-dj-card:hover { + transform: translateY(-15px) scale(1.1); + box-shadow: + 0 30px 60px rgba(0, 0, 0, 0.8), + 0 0 50px rgba(147, 51, 234, 0.6), + inset 0 1px 0 rgba(255, 255, 255, 0.3); + z-index: 25; +} + +.beatport-dj-card:hover::after { + background: linear-gradient( + 180deg, + rgba(147, 51, 234, 0.4) 0%, + rgba(0, 0, 0, 0.2) 50%, + rgba(0, 0, 0, 0.6) 100% + ); +} + +.beatport-dj-card:hover .beatport-dj-name { + color: rgba(147, 51, 234, 0.95); + text-shadow: 0 2px 15px rgba(0, 0, 0, 0.9); + transform: translateY(-3px); +} + +.beatport-dj-card:hover .beatport-dj-creator { + color: #ffffff; + text-shadow: 0 1px 10px rgba(0, 0, 0, 0.9); + transform: translateY(-2px); +} + +.beatport-dj-card-content { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 25px; + z-index: 3; + text-align: center; +} + +.beatport-dj-name { + font-size: 20px; + font-weight: 700; + color: #ffffff; + margin-bottom: 10px; + line-height: 1.2; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +.beatport-dj-creator { + font-size: 16px; + color: rgba(255, 255, 255, 0.9); + font-weight: 500; + text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +/* DJ Loading State */ +.beatport-dj-loading { + display: flex; + align-items: center; + justify-content: center; + height: 300px; + background: linear-gradient(135deg, + rgba(147, 51, 234, 0.1), + rgba(16, 16, 16, 0.8)); + border-radius: 16px; + border: 1px solid rgba(147, 51, 234, 0.2); +} + +.beatport-dj-loading-content { + text-align: center; + color: #ffffff; +} + +.beatport-dj-loading-content h3 { + font-size: 24px; + margin-bottom: 8px; + color: rgba(147, 51, 234, 0.9); +} + +.beatport-dj-loading-content p { + font-size: 16px; + color: rgba(255, 255, 255, 0.7); + margin: 0; +} + +/* DJ Navigation */ +.beatport-dj-slider-nav { + position: absolute; + top: 50%; + left: 0; + right: 0; + transform: translateY(-50%); + display: flex; + justify-content: space-between; + padding: 0 20px; + pointer-events: none; + z-index: 100; +} + +.beatport-dj-nav-btn { + background: linear-gradient(135deg, + rgba(147, 51, 234, 0.9), + rgba(79, 70, 229, 0.8)); + border: none; + color: #ffffff; + font-size: 24px; + font-weight: 600; + width: 50px; + height: 50px; + border-radius: 50%; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + backdrop-filter: blur(10px); + border: 2px solid rgba(255, 255, 255, 0.2); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; +} + +.beatport-dj-nav-btn:hover { + background: linear-gradient(135deg, + rgba(147, 51, 234, 1), + rgba(79, 70, 229, 0.9)); + transform: scale(1.1); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.4), + 0 0 20px rgba(147, 51, 234, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.3); +} + +.beatport-dj-nav-btn:active { + transform: scale(0.95); +} + +/* DJ Indicators */ +.beatport-dj-slider-indicators { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 8px; + z-index: 10; +} + +.beatport-dj-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.4); + border: none; + cursor: pointer; + transition: all 0.3s ease; +} + +.beatport-dj-indicator.active { + background: rgba(147, 51, 234, 0.9); + transform: scale(1.2); + box-shadow: 0 0 10px rgba(147, 51, 234, 0.5); +} + +.beatport-dj-indicator:hover { + background: rgba(255, 255, 255, 0.6); + transform: scale(1.1); +} + +/* DJ Responsive Design */ +@media (max-width: 1200px) { + .beatport-dj-grid { + grid-template-columns: repeat(2, 1fr); + gap: 20px; + } + + .beatport-dj-card { + height: 200px; + min-height: 200px; + max-height: 200px; + } +} + +@media (max-width: 900px) { + .beatport-dj-grid { + grid-template-columns: 1fr; + gap: 16px; + } + + .beatport-dj-slider-container { + padding: 20px; + } + + .beatport-dj-card { + height: 180px; + min-height: 180px; + max-height: 180px; + } +} + +@media (max-width: 600px) { + .beatport-dj-section { + padding: 0 15px; + } + + .beatport-dj-title { + font-size: 24px; + } +}