From ed5328e6c711a460a13703aa6b54e1765062b66b Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Tue, 30 Sep 2025 20:50:16 -0700 Subject: [PATCH] Add Beatport hero slider releases scraping support Introduces a new API endpoint to scrape tracks from multiple Beatport release URLs and integrates hero slider click handling on the rebuild page. The frontend now extracts release URLs from the hero slider and fetches track data using the new backend endpoint, improving rebuild page functionality. --- web_server.py | 65 +++++++++++++++++++++++++++ webui/static/script.js | 99 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 163 insertions(+), 1 deletion(-) diff --git a/web_server.py b/web_server.py index 5fc7b33e..33b94a32 100644 --- a/web_server.py +++ b/web_server.py @@ -13516,6 +13516,71 @@ def get_beatport_homepage_top10_releases_cards(): "releases_count": 0 }), 500 +@app.route('/api/beatport/scrape-releases', methods=['POST']) +def scrape_beatport_releases(): + """General scraper endpoint - takes release URLs and returns tracks""" + try: + data = request.get_json() + if not data: + return jsonify({ + "success": False, + "error": "No JSON data provided", + "tracks": [], + "track_count": 0 + }), 400 + + release_urls = data.get('release_urls', []) + source_name = data.get('source_name', 'General Release Scraper') + + if not release_urls: + return jsonify({ + "success": False, + "error": "No release URLs provided", + "tracks": [], + "track_count": 0 + }), 400 + + logger.info(f"🎯 API request to scrape {len(release_urls)} release URLs with source: {source_name}") + + # Initialize the Beatport scraper + scraper = BeatportUnifiedScraper() + + # Use our new general scraper function + tracks = scraper.scrape_multiple_releases(release_urls, source_name) + + logger.info(f"✅ Successfully extracted {len(tracks)} tracks from {len(release_urls)} releases") + + # Apply text cleaning to track data + cleaned_tracks = [] + for track in tracks: + cleaned_track = track.copy() + if 'title' in cleaned_track: + cleaned_track['title'] = clean_beatport_text(cleaned_track['title']) + if 'artist' in cleaned_track: + cleaned_track['artist'] = clean_beatport_text(cleaned_track['artist']) + if 'label' in cleaned_track: + cleaned_track['label'] = clean_beatport_text(cleaned_track['label']) + cleaned_tracks.append(cleaned_track) + + return jsonify({ + "success": True, + "tracks": cleaned_tracks, + "track_count": len(cleaned_tracks), + "source": source_name, + "release_urls_processed": len(release_urls) + }) + + except Exception as e: + logger.error(f"❌ Error scraping releases: {e}") + import traceback + logger.error(f"Full traceback: {traceback.format_exc()}") + return jsonify({ + "success": False, + "error": str(e), + "tracks": [], + "track_count": 0 + }), 500 + @app.route('/api/beatport/homepage/featured-charts', methods=['GET']) def get_beatport_homepage_featured_charts(): """Get Beatport Featured Charts from homepage section""" diff --git a/webui/static/script.js b/webui/static/script.js index 2869be7d..4c75ebb1 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -9819,6 +9819,16 @@ function initializeSyncPage() { }); } + // Logic for Rebuild page Hero Slider - follows same pattern as Top 10 lists + const beatportHeroSliderContainer = document.querySelector('.beatport-rebuild-slider-container'); + if (beatportHeroSliderContainer) { + beatportHeroSliderContainer.addEventListener('click', (event) => { + console.log('🎯 Hero slider container clicked on rebuild page'); + event.stopPropagation(); // Prevent event bubbling + handleRebuildHeroSliderClick(); + }); + } + // Logic for the Start Sync button const startSyncBtn = document.getElementById('start-sync-btn'); if (startSyncBtn) { @@ -10742,7 +10752,8 @@ function showBeatportMainView() { // Global variable to store rebuild page track data for reuse let rebuildPageTrackData = { beatport_top10: null, - hype_top10: null + hype_top10: null, + hero_slider: null }; async function handleRebuildBeatportTop10Click() { @@ -10759,6 +10770,31 @@ async function handleRebuildHypeTop10Click() { await handleRebuildChartClick('hype_top10', 'Hype Top 10', 'rebuild_hype_top10'); } +// Debounce flag to prevent double execution +let heroSliderClickInProgress = false; + +async function handleRebuildHeroSliderClick() { + console.log('🎯 Handling Hero Slider click on rebuild page'); + + // Prevent double execution + if (heroSliderClickInProgress) { + console.log('⏳ Hero slider click already in progress, ignoring...'); + return; + } + + heroSliderClickInProgress = true; + + try { + // Use the existing chart creation pattern from Browse Charts EXACTLY + await handleRebuildChartClick('hero_slider', 'Hero Slider Releases', 'rebuild_hero_slider'); + } finally { + // Reset flag after completion (whether success or failure) + setTimeout(() => { + heroSliderClickInProgress = false; + }, 1000); // 1 second cooldown + } +} + async function handleRebuildChartClick(trackDataKey, chartName, chartType) { try { // Create chart hash (following Browse Charts pattern) @@ -10831,6 +10867,9 @@ async function getRebuildPageTrackData(trackDataKey) { } else if (trackDataKey === 'hype_top10') { containerSelector = '#beatport-hype10-list'; cardSelector = '.beatport-hype10-card[data-url]'; + } else if (trackDataKey === 'hero_slider') { + // Hero slider needs special handling - extract release URLs and scrape tracks + return await getHeroSliderTrackData(); } else { throw new Error(`Unknown track data key: ${trackDataKey}`); } @@ -10870,6 +10909,64 @@ async function getRebuildPageTrackData(trackDataKey) { return tracks; } +async function getHeroSliderTrackData() { + console.log('🎯 Getting hero slider track data'); + + try { + // Show loading overlay + showLoadingOverlay('Extracting tracks from releases...'); + + // Extract all release URLs from hero slider slides + const heroSlides = document.querySelectorAll('.beatport-rebuild-slide[data-url]'); + const releaseUrls = Array.from(heroSlides).map(slide => slide.getAttribute('data-url')).filter(url => url); + + console.log(`🎯 Found ${releaseUrls.length} release URLs from hero slider:`, releaseUrls); + + if (releaseUrls.length === 0) { + hideLoadingOverlay(); + throw new Error('No release URLs found in hero slider'); + } + + // Call our general scraper function to get tracks from all releases + const response = await fetch('/api/beatport/scrape-releases', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + release_urls: releaseUrls, + source_name: 'Hero Slider Releases' + }) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (!data.success || !data.tracks || data.tracks.length === 0) { + hideLoadingOverlay(); + throw new Error('No tracks found in hero slider releases'); + } + + console.log(`🎯 Got ${data.tracks.length} tracks from hero slider releases`); + + // Hide loading overlay + hideLoadingOverlay(); + + // Cache for future use + rebuildPageTrackData.hero_slider = data.tracks; + + return data.tracks; + + } catch (error) { + console.error('❌ Error getting hero slider track data:', error); + hideLoadingOverlay(); + throw error; + } +} + // Hook into the loadBeatportTop10Lists function to cache track data const originalLoadBeatportTop10Lists = window.loadBeatportTop10Lists; if (originalLoadBeatportTop10Lists) {