diff --git a/web_server.py b/web_server.py index 3ffed756..72145ceb 100644 --- a/web_server.py +++ b/web_server.py @@ -13321,6 +13321,102 @@ def get_beatport_dj_charts_improved(): "count": 0 }), 500 + +@app.route('/api/beatport/hype-picks') +def get_beatport_hype_picks(): + """Get Beatport Hype Picks for the rebuild slider grid (EXACT same pattern as new-releases)""" + try: + logger.info("🔥 Fetching Beatport hype picks...") + + # Initialize scraper + scraper = BeatportUnifiedScraper() + + # Get page and extract releases + soup = scraper.get_page(scraper.base_url) + if not soup: + raise Exception("Could not fetch Beatport homepage") + + # Extract hype pick cards using data-testid selector (equivalent to new-releases CSS selector) + hype_pick_cards = soup.select('[data-testid="hype-picks"]') + releases = [] + + logger.info(f"🔍 Found {len(hype_pick_cards)} hype pick cards") + + for i, card in enumerate(hype_pick_cards[:100]): # Limit to 100 for 10 slides (same as new-releases) + release_data = {} + + # Extract title (exact same logic as new-releases) + title_elem = card.select_one('[class*="title"], [class*="Title"], h1, h2, h3, h4, h5, h6') + if title_elem: + title_text = title_elem.get_text(strip=True) + if title_text and len(title_text) > 2 and title_text not in ['Hype Picks', 'Buy', 'Play']: + release_data['title'] = title_text + + # Extract artist (exact same logic as new-releases) + artist_elem = card.select_one('[class*="artist"], [class*="Artist"], a[href*="/artist/"]') + if artist_elem: + artist_text = artist_elem.get_text(strip=True) + if artist_text and len(artist_text) > 1: + release_data['artist'] = artist_text + + # Extract label (exact same logic as new-releases) + label_elem = card.select_one('[class*="label"], [class*="Label"], a[href*="/label/"]') + if label_elem: + label_text = label_elem.get_text(strip=True) + if label_text and len(label_text) > 1: + release_data['label'] = label_text + + # Extract URL (exact same logic as new-releases) + url_link = card.select_one('a[href*="/release/"]') + if url_link: + href = url_link.get('href') + if href: + release_data['url'] = urljoin(scraper.base_url, href) + + # Extract image (exact same logic as new-releases) + img = card.select_one('img') + if img: + src = img.get('src') or img.get('data-src') or img.get('data-lazy-src') + if src: + release_data['image_url'] = src + + # URL fallback for title (exact same logic as new-releases) + if not release_data.get('title') and release_data.get('url'): + url_parts = release_data['url'].split('/release/') + if len(url_parts) > 1: + slug = url_parts[1].split('/')[0] + release_data['title'] = slug.replace('-', ' ').title() + + # Only add if we have essential data (exact same logic as new-releases) + if release_data.get('title') and release_data.get('url'): + # Add fallbacks for missing data (exact same logic as new-releases) + if not release_data.get('artist'): + release_data['artist'] = 'Various Artists' + if not release_data.get('label'): + release_data['label'] = 'Unknown Label' + + releases.append(release_data) + + logger.info(f"✅ Successfully extracted {len(releases)} hype picks") + + return jsonify({ + 'success': True, + 'releases': releases, + 'count': len(releases), + 'slides': (len(releases) + 9) // 10, # Calculate number of slides needed (same as new-releases) + 'timestamp': datetime.now().isoformat() + }) + + except Exception as e: + logger.error(f"❌ Error getting Beatport hype picks: {e}") + return jsonify({ + 'success': False, + 'error': str(e), + 'releases': [], + 'count': 0 + }), 500 + + @app.route('/api/beatport/discovery/start/', methods=['POST']) def start_beatport_discovery(url_hash): """Start Spotify discovery for Beatport chart tracks""" diff --git a/webui/index.html b/webui/index.html index 55beb3b0..60b47c8f 100644 --- a/webui/index.html +++ b/webui/index.html @@ -839,6 +839,39 @@ + +
+
+

🔥 Hype Picks

+

Editor selected trending tracks from Beatport

+
+ +
+
+
+ +
+
+

🔥 Loading Hype Picks...

+

Fetching the hottest trending tracks

+
+
+
+ + +
+ + +
+ + +
+ +
+
+
+
+
diff --git a/webui/static/script.js b/webui/static/script.js index 7cee8ca8..c892b87e 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -9735,6 +9735,7 @@ function initializeSyncPage() { if (tabId === 'rebuild') { initializeBeatportRebuildSlider(); initializeBeatportReleasesSlider(); + initializeBeatportHypePicksSlider(); initializeBeatportChartsSlider(); initializeBeatportDJSlider(); } @@ -18830,6 +18831,335 @@ function cleanupBeatportReleasesSlider() { } } +// =================================== +// BEATPORT HYPE PICKS SLIDER +// =================================== + +// Hype Picks Slider State +let beatportHypePicksSliderState = { + currentSlide: 0, + totalSlides: 0, + autoPlayInterval: null, + autoPlayDelay: 4000, + isInitialized: false +}; + +/** + * Initialize the beatport hype picks slider functionality (based on releases slider) + */ +function initializeBeatportHypePicksSlider() { + console.log('🔥 Initializing beatport hype picks slider...'); + + const slider = document.getElementById('beatport-hype-picks-slider'); + if (!slider) { + console.warn('Beatport hype picks slider not found'); + return; + } + + // Check if already initialized + if (beatportHypePicksSliderState.isInitialized) { + console.log('Beatport hype picks slider already initialized, skipping...'); + startBeatportHypePicksSliderAutoPlay(); // Just restart autoplay + return; + } + + // Mark as initialized + beatportHypePicksSliderState.isInitialized = true; + + // Reset state + beatportHypePicksSliderState.currentSlide = 0; + beatportHypePicksSliderState.totalSlides = 0; + + // Load data and initialize + loadBeatportHypePicks().then(success => { + if (success) { + setupBeatportHypePicksSliderNavigation(); + setupBeatportHypePicksSliderIndicators(); + setupBeatportHypePicksSliderHoverPause(); + startBeatportHypePicksSliderAutoPlay(); + } + }); + + console.log('✅ Beatport hype picks slider initialized successfully'); +} + +/** + * Load hype picks data from API + */ +async function loadBeatportHypePicks() { + try { + console.log('🔥 Fetching hype picks data...'); + + const response = await fetch('/api/beatport/hype-picks'); + const data = await response.json(); + + if (data.success && data.releases && data.releases.length > 0) { + console.log(`🔥 Loaded ${data.releases.length} hype picks releases`); + populateBeatportHypePicksSlider(data.releases); + return true; + } else { + console.error('Failed to load hype picks:', data.error || 'No hype picks found'); + showBeatportHypePicksError(data.error || 'No hype picks available'); + return false; + } + } catch (error) { + console.error('Error loading hype picks:', error); + showBeatportHypePicksError('Failed to load hype picks'); + return false; + } +} + +/** + * Populate the hype picks slider with data (based on releases slider) + */ +function populateBeatportHypePicksSlider(releases) { + const sliderTrack = document.getElementById('beatport-hype-picks-slider-track'); + const indicatorsContainer = document.getElementById('beatport-hype-picks-slider-indicators'); + + if (!sliderTrack || !indicatorsContainer) return; + + // Clear existing content + sliderTrack.innerHTML = ''; + indicatorsContainer.innerHTML = ''; + + // Group releases into slides (10 releases per slide in 5x2 grid) + const releasesPerSlide = 10; + const slides = []; + for (let i = 0; i < releases.length; i += releasesPerSlide) { + slides.push(releases.slice(i, i + releasesPerSlide)); + } + + console.log(`🔥 Hype Picks: Got ${releases.length} releases, creating ${slides.length} slides`); + beatportHypePicksSliderState.totalSlides = slides.length; + beatportHypePicksSliderState.currentSlide = 0; + + // Create slides + slides.forEach((slideReleases, slideIndex) => { + const slideHtml = ` +
+
+ ${slideReleases.map(release => createBeatportHypePickCard(release)).join('')} + ${slideReleases.length < releasesPerSlide ? + Array(releasesPerSlide - slideReleases.length).fill(0).map(() => + `
+
🔥
+
` + ).join('') : '' + } +
+
+ `; + sliderTrack.insertAdjacentHTML('beforeend', slideHtml); + console.log(`🔥 Created slide ${slideIndex + 1}/${slides.length} with ${slideReleases.length} releases`); + + // Create indicator + const indicatorHtml = ``; + indicatorsContainer.insertAdjacentHTML('beforeend', indicatorHtml); + }); + + // Add click handlers to track cards + setupBeatportHypePickCardHandlers(); +} + +/** + * Create a hype pick card HTML (for release cards, same as new releases) + */ +function createBeatportHypePickCard(release) { + const artworkUrl = release.image_url || ''; + const bgStyle = artworkUrl ? `style="--card-bg-image: url('${artworkUrl}')"` : ''; + + return ` +
+
+
+ ${artworkUrl ? `${release.title || 'Release'}` : ''} +
+
+
${release.title || 'Unknown Title'}
+
${release.artist || 'Unknown Artist'}
+
${release.label || 'Hype Pick'}
+
+
+
+ `; +} + +/** + * Setup navigation for hype picks slider (same pattern as releases) + */ +function setupBeatportHypePicksSliderNavigation() { + const prevBtn = document.getElementById('beatport-hype-picks-prev-btn'); + const nextBtn = document.getElementById('beatport-hype-picks-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 hype picks button clicked, current slide:', beatportHypePicksSliderState.currentSlide); + goToBeatportHypePicksSlide(beatportHypePicksSliderState.currentSlide - 1); + resetBeatportHypePicksSliderAutoPlay(); + }); + } + + 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 hype picks button clicked, current slide:', beatportHypePicksSliderState.currentSlide); + goToBeatportHypePicksSlide(beatportHypePicksSliderState.currentSlide + 1); + resetBeatportHypePicksSliderAutoPlay(); + }); + } +} + +/** + * Setup indicators for hype picks slider + */ +function setupBeatportHypePicksSliderIndicators() { + const indicators = document.querySelectorAll('.beatport-hype-picks-indicator'); + + indicators.forEach((indicator, index) => { + indicator.addEventListener('click', () => { + goToBeatportHypePicksSlide(index); + resetBeatportHypePicksSliderAutoPlay(); + }); + }); +} + +/** + * Navigate to specific slide + */ +function goToBeatportHypePicksSlide(slideIndex) { + console.log('goToBeatportHypePicksSlide called with:', slideIndex, 'current:', beatportHypePicksSliderState.currentSlide); + + // Handle wrap around + if (slideIndex < 0) { + slideIndex = beatportHypePicksSliderState.totalSlides - 1; + } else if (slideIndex >= beatportHypePicksSliderState.totalSlides) { + slideIndex = 0; + } + + // Update current slide + beatportHypePicksSliderState.currentSlide = slideIndex; + + // Update slides + const slides = document.querySelectorAll('.beatport-hype-picks-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-hype-picks-indicator'); + indicators.forEach((indicator, index) => { + indicator.classList.toggle('active', index === slideIndex); + }); + + console.log('Slide updated to:', beatportHypePicksSliderState.currentSlide); +} + +/** + * Start auto-play for hype picks slider + */ +function startBeatportHypePicksSliderAutoPlay() { + if (beatportHypePicksSliderState.autoPlayInterval) { + clearInterval(beatportHypePicksSliderState.autoPlayInterval); + } + + beatportHypePicksSliderState.autoPlayInterval = setInterval(() => { + goToBeatportHypePicksSlide(beatportHypePicksSliderState.currentSlide + 1); + }, beatportHypePicksSliderState.autoPlayDelay); + + console.log('🔥 Hype picks slider autoplay started'); +} + +/** + * Reset auto-play for hype picks slider + */ +function resetBeatportHypePicksSliderAutoPlay() { + startBeatportHypePicksSliderAutoPlay(); +} + +/** + * Setup hover pause for hype picks slider + */ +function setupBeatportHypePicksSliderHoverPause() { + const sliderContainer = document.querySelector('.beatport-hype-picks-slider-container'); + if (sliderContainer) { + sliderContainer.addEventListener('mouseenter', () => { + if (beatportHypePicksSliderState.autoPlayInterval) { + clearInterval(beatportHypePicksSliderState.autoPlayInterval); + } + }); + + sliderContainer.addEventListener('mouseleave', () => { + startBeatportHypePicksSliderAutoPlay(); + }); + } +} + +/** + * Setup click handlers for hype pick cards + */ +function setupBeatportHypePickCardHandlers() { + const cards = document.querySelectorAll('.beatport-hype-pick-card:not(.beatport-hype-pick-placeholder)'); + + cards.forEach(card => { + card.addEventListener('click', () => { + const releaseUrl = card.dataset.releaseUrl; + + if (releaseUrl) { + console.log('🔥 Clicked hype pick release:', releaseUrl); + // You can add release handling logic here + // For now, just log the click + showToast('🔥 Hype pick release clicked!', 'info'); + } + }); + }); +} + +/** + * Show error state for hype picks slider + */ +function showBeatportHypePicksError(errorMessage) { + const sliderTrack = document.getElementById('beatport-hype-picks-slider-track'); + if (sliderTrack) { + sliderTrack.innerHTML = ` +
+
+

❌ Error Loading Hype Picks

+

${errorMessage}

+
+
+ `; + } +} + +/** + * Clean up hype picks slider when switching away + */ +function cleanupBeatportHypePicksSlider() { + if (beatportHypePicksSliderState.autoPlayInterval) { + clearInterval(beatportHypePicksSliderState.autoPlayInterval); + beatportHypePicksSliderState.autoPlayInterval = null; + } +} + // =================================== // BEATPORT FEATURED CHARTS SLIDER // =================================== diff --git a/webui/static/style.css b/webui/static/style.css index a7c01422..19781a50 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -12421,6 +12421,397 @@ body { } } +/* ==================== HYPE PICKS SLIDER STYLES ==================== */ + +.beatport-hype-picks-section { + margin-top: 40px; + padding: 0 20px; +} + +.beatport-hype-picks-header { + text-align: center; + margin-bottom: 30px; +} + +.beatport-hype-picks-title { + font-size: 32px; + font-weight: bold; + background: linear-gradient(135deg, #ff6b6b, #ff8787, #ffa8a8); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + margin-bottom: 8px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.beatport-hype-picks-subtitle { + font-size: 16px; + color: rgba(255, 255, 255, 0.7); + margin: 0; +} + +.beatport-hype-picks-slider-container { + position: relative; + background: linear-gradient(135deg, + rgba(18, 18, 18, 0.95), + rgba(30, 30, 30, 0.9)); + border-radius: 20px; + padding: 30px; + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.5), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.beatport-hype-picks-slider { + position: relative; + border-radius: 16px; +} + +.beatport-hype-picks-slider-track { + position: relative; + height: 425px; +} + +.beatport-hype-picks-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; + background: linear-gradient(135deg, + rgba(255, 107, 107, 0.1), + rgba(255, 135, 135, 0.05)); + border-radius: 16px; + border: 2px dashed rgba(255, 107, 107, 0.3); +} + +.beatport-hype-picks-loading-content { + text-align: center; +} + +.beatport-hype-picks-loading-content h3 { + font-size: 24px; + color: #ff6b6b; + margin-bottom: 8px; + font-weight: 600; +} + +.beatport-hype-picks-loading-content p { + color: rgba(255, 255, 255, 0.6); + font-size: 14px; + margin: 0; +} + +/* Individual Slide (contains 10 cards in 5x2 grid) */ +.beatport-hype-picks-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-hype-picks-slide.active { + opacity: 1; + z-index: 10; + transform: translateX(0); +} + +.beatport-hype-picks-slide.prev { + transform: translateX(-100px); +} + +.beatport-hype-picks-slide.next { + transform: translateX(100px); +} + +.beatport-hype-picks-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 20px; + 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(255, 255, 255, 0.08); + justify-items: center; + align-items: center; + margin: 0 auto; +} + +/* Hype Pick Card Styling */ +.beatport-hype-pick-card { + background: linear-gradient(145deg, + rgba(40, 40, 40, 0.9), + rgba(28, 28, 28, 0.95)); + border-radius: 12px; + padding: 16px; + border: 1px solid rgba(255, 255, 255, 0.1); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + position: relative; + overflow: hidden; + backdrop-filter: blur(10px); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.1); + width: 100%; + height: 160px; + min-height: 160px; + max-height: 160px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.beatport-hype-pick-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: var(--card-bg-image, none); + background-size: cover; + background-position: center; + opacity: 0.15; + border-radius: 12px; + z-index: 0; +} + +.beatport-hype-pick-card:hover { + transform: translateY(-2px) scale(1.05); + background: linear-gradient(145deg, + rgba(255, 107, 107, 0.15), + rgba(40, 40, 40, 0.95)); + border-color: rgba(255, 107, 107, 0.4); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.4), + 0 0 16px rgba(255, 107, 107, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + z-index: 10; +} + +.beatport-hype-pick-card-content { + position: relative; + z-index: 1; + text-align: center; +} + +.beatport-hype-pick-artwork { + width: 80px; + height: 80px; + border-radius: 8px; + margin: 0 auto 12px auto; + background: linear-gradient(135deg, #333, #555); + border: 2px solid rgba(255, 255, 255, 0.1); + overflow: hidden; + position: relative; +} + +.beatport-hype-pick-artwork img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.beatport-hype-pick-card:hover .beatport-hype-pick-artwork img { + transform: scale(1.1); +} + +.beatport-hype-pick-info { + text-align: center; +} + +.beatport-hype-pick-title { + font-size: 13px; + font-weight: 600; + color: #ffffff; + margin-bottom: 4px; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.beatport-hype-pick-artist { + font-size: 11px; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.beatport-hype-pick-label { + font-size: 10px; + color: rgba(255, 107, 107, 0.8); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Placeholder Card Styling */ +.beatport-hype-pick-placeholder { + opacity: 0.3; + cursor: default; + pointer-events: none; +} + +.beatport-hype-pick-placeholder .placeholder-icon { + font-size: 32px; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: rgba(255, 255, 255, 0.5); +} + +.beatport-hype-pick-placeholder:hover { + transform: none; + background: linear-gradient(145deg, + rgba(40, 40, 40, 0.9), + rgba(28, 28, 28, 0.95)); +} + +/* Navigation Buttons */ +.beatport-hype-picks-slider-nav { + position: absolute; + top: 50%; + width: 100%; + display: flex; + justify-content: space-between; + transform: translateY(-50%); + pointer-events: none; + z-index: 10; +} + +.beatport-hype-picks-nav-btn { + pointer-events: all; + background: linear-gradient(135deg, + rgba(255, 107, 107, 0.9), + rgba(255, 135, 135, 0.8)); + border: none; + border-radius: 50%; + width: 50px; + height: 50px; + color: white; + font-size: 24px; + font-weight: bold; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(10px); + box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4); + transition: all 0.3s ease; + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.beatport-hype-picks-nav-btn:hover { + transform: scale(1.1); + background: linear-gradient(135deg, + rgba(255, 107, 107, 1), + rgba(255, 135, 135, 0.9)); + box-shadow: 0 8px 25px rgba(255, 107, 107, 0.6); +} + +.beatport-hype-picks-prev-btn { + margin-left: -25px; +} + +.beatport-hype-picks-next-btn { + margin-right: -25px; +} + +/* Slider Indicators */ +.beatport-hype-picks-slider-indicators { + display: flex; + justify-content: center; + gap: 12px; + margin-top: 25px; +} + +.beatport-hype-picks-indicator { + width: 12px; + height: 12px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + border: 2px solid rgba(255, 255, 255, 0.1); + cursor: pointer; + transition: all 0.3s ease; +} + +.beatport-hype-picks-indicator.active { + background: linear-gradient(135deg, #ff6b6b, #ff8787); + border-color: rgba(255, 107, 107, 0.6); + box-shadow: 0 0 15px rgba(255, 107, 107, 0.5); + transform: scale(1.2); +} + +.beatport-hype-picks-indicator:hover { + background: rgba(255, 255, 255, 0.5); + transform: scale(1.1); +} + +/* Responsive Design */ +@media (max-width: 1400px) { + .beatport-hype-picks-grid { + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(2, 1fr); + } +} + +@media (max-width: 1200px) { + .beatport-hype-picks-grid { + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 16px; + } + + .beatport-hype-pick-artwork { + width: 70px; + height: 70px; + } +} + +@media (max-width: 900px) { + .beatport-hype-picks-grid { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: 14px; + } + + .beatport-hype-picks-slider-container { + padding: 20px; + } +} + +@media (max-width: 600px) { + .beatport-hype-picks-grid { + grid-template-columns: 1fr; + grid-template-rows: repeat(5, 1fr); + gap: 12px; + } + + .beatport-hype-picks-section { + padding: 0 15px; + } + + .beatport-hype-picks-title { + font-size: 24px; + } +} + /* ==================== FEATURED CHARTS SLIDER STYLES ==================== */ /* Charts Section */