From 5e73b53a0fb71e8dd6a21938a94004aef29ce03b Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Tue, 30 Sep 2025 12:36:25 -0700 Subject: [PATCH] beatport progress --- beatport_unified_scraper.py | 119 +++++++++ web_server.py | 34 +++ webui/index.html | 44 ++++ webui/static/script.js | 125 ++++++++++ webui/static/style.css | 466 ++++++++++++++++++++++++++++++++++++ 5 files changed, 788 insertions(+) diff --git a/beatport_unified_scraper.py b/beatport_unified_scraper.py index 0b3c8eb2..ed039009 100644 --- a/beatport_unified_scraper.py +++ b/beatport_unified_scraper.py @@ -1284,6 +1284,125 @@ class BeatportUnifiedScraper: print(f" āŒ Error converting track data: {e}") return None + def scrape_homepage_top10_lists(self) -> Dict[str, List[Dict]]: + """Scrape Top 10 Lists from homepage - Beatport Top 10 and Hype Top 10""" + print("\nšŸ† Scraping Top 10 Lists from homepage...") + + soup = self.get_page(self.base_url) + if not soup: + return {"beatport_top10": [], "hype_top10": []} + + # Extract Beatport Top 10 tracks + beatport_top10_items = soup.select('[data-testid="top-10-item"]') + print(f" Found {len(beatport_top10_items)} Beatport Top 10 items") + + beatport_tracks = [] + for i, item in enumerate(beatport_top10_items, 1): + try: + track_data = self.extract_track_from_top10_item(item, i, "Beatport Top 10") + if track_data: + beatport_tracks.append(track_data) + except Exception as e: + print(f" āŒ Error extracting Beatport track {i}: {e}") + + # Extract Hype Top 10 tracks + hype_top10_items = soup.select('[data-testid="hype-top-10-item"]') + print(f" Found {len(hype_top10_items)} Hype Top 10 items") + + hype_tracks = [] + for i, item in enumerate(hype_top10_items, 1): + try: + track_data = self.extract_track_from_top10_item(item, i, "Hype Top 10") + if track_data: + hype_tracks.append(track_data) + except Exception as e: + print(f" āŒ Error extracting Hype track {i}: {e}") + + print(f"āœ… Extracted {len(beatport_tracks)} Beatport Top 10 + {len(hype_tracks)} Hype Top 10 tracks") + + return { + "beatport_top10": beatport_tracks, + "hype_top10": hype_tracks + } + + def extract_track_from_top10_item(self, item, rank, list_name): + """Extract track data from a top 10 list item""" + try: + # Get the track URL + link_elem = item.select_one('a[href*="/track/"]') + track_url = "" + if link_elem and link_elem.get('href'): + track_url = f"https://www.beatport.com{link_elem.get('href')}" + + # Extract track title + title = "Unknown Title" + title_selectors = [ + '[class*="ItemName"]', + '[class*="TrackName"]', + '[class*="track-name"]', + 'a[href*="/track/"]' + ] + + for selector in title_selectors: + title_elem = item.select_one(selector) + if title_elem: + title = title_elem.get_text(strip=True) + if title and title != "Unknown Title": + break + + # Extract artist name + artist = "Unknown Artist" + artist_selectors = [ + '[class*="Artists"]', + '[class*="artist"]', + '[class*="Artist"]', + '[class*="ItemArtist"]', + 'a[href*="/artist/"]' + ] + + for selector in artist_selectors: + artist_elem = item.select_one(selector) + if artist_elem: + artist = artist_elem.get_text(strip=True) + if artist and artist != "Unknown Artist": + break + + # Extract label name + label = "Unknown Label" + label_selectors = [ + '[class*="Label"]', + '[class*="label"]', + '[class*="ItemLabel"]', + 'a[href*="/label/"]' + ] + + for selector in label_selectors: + label_elem = item.select_one(selector) + if label_elem: + label = label_elem.get_text(strip=True) + if label and label != "Unknown Label": + break + + # Extract artwork if available + artwork_url = "" + img_elem = item.select_one('img') + if img_elem and img_elem.get('src'): + artwork_url = img_elem.get('src') + + return { + "rank": rank, + "title": title, + "artist": artist, + "label": label, + "url": track_url, + "artwork_url": artwork_url, + "list_name": list_name + } + + except Exception as e: + print(f"Error extracting track data: {e}") + return None + def scrape_new_on_beatport_hero(self, limit: int = 10) -> List[Dict]: """Scrape the 'New on Beatport' hero slideshow from homepage""" print("\nšŸŽÆ Scraping 'New on Beatport' hero slideshow...") diff --git a/web_server.py b/web_server.py index 72145ceb..2a97b8aa 100644 --- a/web_server.py +++ b/web_server.py @@ -13228,6 +13228,40 @@ def get_beatport_homepage_top_10_releases(): "track_count": 0 }), 500 +@app.route('/api/beatport/homepage/top-10-lists', methods=['GET']) +def get_beatport_homepage_top10_lists(): + """Get Beatport Top 10 Lists from homepage - both Beatport Top 10 and Hype Top 10""" + try: + logger.info("šŸ† API request for Beatport homepage Top 10 Lists") + + # Initialize the Beatport scraper + scraper = BeatportUnifiedScraper() + + # Get top 10 lists from homepage + top10_lists = scraper.scrape_homepage_top10_lists() + + logger.info(f"āœ… Successfully extracted Beatport Top 10: {len(top10_lists['beatport_top10'])}, Hype Top 10: {len(top10_lists['hype_top10'])}") + + return jsonify({ + "success": True, + "beatport_top10": top10_lists["beatport_top10"], + "hype_top10": top10_lists["hype_top10"], + "beatport_count": len(top10_lists["beatport_top10"]), + "hype_count": len(top10_lists["hype_top10"]), + "source": "beatport_homepage_top10_lists" + }) + + except Exception as e: + logger.error(f"āŒ Error getting Beatport homepage top 10 lists: {e}") + return jsonify({ + "success": False, + "error": str(e), + "beatport_top10": [], + "hype_top10": [], + "beatport_count": 0, + "hype_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/index.html b/webui/index.html index 60b47c8f..3e66cf4e 100644 --- a/webui/index.html +++ b/webui/index.html @@ -806,6 +806,50 @@ + +
+
+

šŸ† Top 10 Lists

+

Current trending tracks from Beatport charts

+
+ +
+ +
+
+

šŸŽµ Beatport Top 10

+

Most popular tracks on Beatport

+
+
+ +
+
+

šŸŽµ Loading Beatport Top 10...

+

Fetching trending tracks

+
+
+
+
+ + +
+
+

šŸ”„ Hype Top 10

+

Editor's hottest trending picks

+
+
+ +
+
+

šŸ”„ Loading Hype Top 10...

+

Fetching editor's picks

+
+
+
+
+
+
+
diff --git a/webui/static/script.js b/webui/static/script.js index c892b87e..d74fc11c 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -9734,6 +9734,7 @@ function initializeSyncPage() { // Initialize rebuild slider if rebuild tab is selected if (tabId === 'rebuild') { initializeBeatportRebuildSlider(); + loadBeatportTop10Lists(); initializeBeatportReleasesSlider(); initializeBeatportHypePicksSlider(); initializeBeatportChartsSlider(); @@ -19719,3 +19720,127 @@ function cleanupBeatportDJSlider() { beatportDJSliderState.autoPlayInterval = null; } } + +/** + * Load top 10 lists data from API and populate both lists + */ +async function loadBeatportTop10Lists() { + try { + console.log('šŸ† Loading top 10 lists data...'); + const response = await fetch('/api/beatport/homepage/top-10-lists'); + const data = await response.json(); + + if (data.success) { + console.log(`šŸŽµ Loaded ${data.beatport_count} Beatport Top 10 + ${data.hype_count} Hype Top 10 tracks`); + + // Populate both lists + populateBeatportTop10List(data.beatport_top10); + populateHypeTop10List(data.hype_top10); + return true; + } else { + console.error('Failed to load top 10 lists:', data.error); + showTop10ListsError(data.error || 'No data available'); + return false; + } + } catch (error) { + console.error('Error loading top 10 lists:', error); + showTop10ListsError('Failed to load top 10 lists'); + return false; + } +} + +/** + * Populate Beatport Top 10 list with data + */ +function populateBeatportTop10List(tracks) { + const container = document.getElementById('beatport-top10-list'); + if (!container || !tracks || tracks.length === 0) return; + + // Generate HTML for the tracks + let tracksHtml = ` +
+

šŸŽµ Beatport Top 10

+

Most popular tracks on Beatport

+
+
+ `; + + tracks.forEach((track, index) => { + tracksHtml += ` +
+
${track.rank || index + 1}
+
+ ${track.artwork_url ? + `${track.title}` : + '
šŸŽµ
' + } +
+
+

${track.title || 'Unknown Title'}

+

${track.artist || 'Unknown Artist'}

+

${track.label || 'Unknown Label'}

+
+
+ `; + }); + + tracksHtml += '
'; + container.innerHTML = tracksHtml; +} + +/** + * Populate Hype Top 10 list with data + */ +function populateHypeTop10List(tracks) { + const container = document.getElementById('beatport-hype10-list'); + if (!container || !tracks || tracks.length === 0) return; + + // Generate HTML for the tracks + let tracksHtml = ` +
+

šŸ”„ Hype Top 10

+

Editor's trending picks

+
+
+ `; + + tracks.forEach((track, index) => { + tracksHtml += ` +
+
${track.rank || index + 1}
+
+ ${track.artwork_url ? + `${track.title}` : + '
šŸ”„
' + } +
+
+

${track.title || 'Unknown Title'}

+

${track.artist || 'Unknown Artist'}

+

${track.label || 'Unknown Label'}

+
+
+ `; + }); + + tracksHtml += '
'; + container.innerHTML = tracksHtml; +} + +/** + * Show error message for top 10 lists + */ +function showTop10ListsError(errorMessage) { + const beatportContainer = document.getElementById('beatport-top10-list'); + const hypeContainer = document.getElementById('beatport-hype10-list'); + + const errorHtml = ` +
+

āŒ Error Loading Data

+

${errorMessage}

+
+ `; + + if (beatportContainer) beatportContainer.innerHTML = errorHtml; + if (hypeContainer) hypeContainer.innerHTML = errorHtml; +} diff --git a/webui/static/style.css b/webui/static/style.css index a874fc70..b4fb0c2c 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -12040,6 +12040,472 @@ body { } } +/* ======================================= + BEATPORT TOP 10 LISTS SECTION + ======================================= */ + +.beatport-top10-section { + margin-top: 40px; + padding: 0 30px; + margin-bottom: 40px; +} + +.beatport-top10-header { + text-align: center; + margin-bottom: 40px; +} + +.beatport-top10-title { + font-size: 36px; + font-weight: 700; + color: #ffffff; + margin-bottom: 8px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); +} + +.beatport-top10-subtitle { + font-size: 16px; + color: rgba(255, 255, 255, 0.7); + margin: 0; +} + +/* Two-column container for both lists */ +.beatport-top10-container { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; + max-width: 1400px; + margin: 0 auto; + overflow: hidden; /* Prevent overflow */ +} + +/* Individual list styling */ +.beatport-top10-list, .beatport-hype10-list { + 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); + width: 100%; + min-width: 0; /* Allow shrinking */ + overflow: hidden; /* Prevent content overflow */ +} + +/* Color-coded themes */ +.beatport-top10-list { + border-left: 4px solid #00ff88; + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(0, 255, 136, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.beatport-hype10-list { + border-left: 4px solid #ff3366; + box-shadow: + 0 20px 40px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(255, 51, 102, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +/* List headers */ +.beatport-top10-list-header, .beatport-hype10-list-header { + text-align: center; + margin-bottom: 25px; + padding-bottom: 15px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.beatport-top10-list-title, .beatport-hype10-list-title { + font-size: 24px; + font-weight: 600; + color: #ffffff; + margin-bottom: 5px; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); +} + +.beatport-top10-list-subtitle, .beatport-hype10-list-subtitle { + font-size: 14px; + color: rgba(255, 255, 255, 0.6); + margin: 0; +} + +/* Track containers */ +.beatport-top10-tracks, .beatport-hype10-tracks { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Individual track cards - CORRECTED CLASS NAMES */ +.beatport-top10-card, .beatport-hype10-card { + display: flex; + align-items: center; + background: linear-gradient(135deg, + rgba(25, 25, 25, 0.9), + rgba(35, 35, 35, 0.8)); + border-radius: 16px; + padding: 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + backdrop-filter: blur(10px); + transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94); + cursor: pointer; + position: relative; + overflow: hidden; + width: 100%; + min-width: 0; /* Allow shrinking */ + box-sizing: border-box; + min-height: 80px; /* Consistent height */ + max-height: 80px; /* Prevent expansion */ +} + +/* Hover animations for cards */ +.beatport-top10-card:hover { + background: linear-gradient(135deg, + rgba(0, 255, 136, 0.12), + rgba(35, 35, 35, 0.95)); + border-color: rgba(0, 255, 136, 0.3); + transform: translateY(-3px); + box-shadow: + 0 12px 32px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(0, 255, 136, 0.2); +} + +.beatport-hype10-card:hover { + background: linear-gradient(135deg, + rgba(255, 51, 102, 0.12), + rgba(35, 35, 35, 0.95)); + border-color: rgba(255, 51, 102, 0.3); + transform: translateY(-3px); + box-shadow: + 0 12px 32px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(255, 51, 102, 0.2); +} + +/* Track rank number */ +.beatport-top10-card-rank { + font-size: 20px; + font-weight: 800; + color: #00ff88; + min-width: 35px; + text-align: center; + margin-right: 16px; + text-shadow: 0 2px 6px rgba(0, 255, 136, 0.3); + background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), rgba(0, 255, 136, 0.05)); + border-radius: 12px; + padding: 8px 0; + border: 1px solid rgba(0, 255, 136, 0.2); +} + +.beatport-hype10-card-rank { + font-size: 20px; + font-weight: 800; + color: #ff3366; + min-width: 35px; + text-align: center; + margin-right: 16px; + text-shadow: 0 2px 6px rgba(255, 51, 102, 0.3); + background: linear-gradient(135deg, rgba(255, 51, 102, 0.1), rgba(255, 51, 102, 0.05)); + border-radius: 12px; + padding: 8px 0; + border: 1px solid rgba(255, 51, 102, 0.2); +} + +/* Track artwork */ +.beatport-top10-card-artwork, .beatport-hype10-card-artwork { + width: 56px; + height: 56px; + border-radius: 12px; + margin-right: 16px; + overflow: hidden; + position: relative; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); + border: 2px solid rgba(255, 255, 255, 0.1); +} + +.beatport-top10-card-artwork img, .beatport-hype10-card-artwork img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.4s ease; +} + +.beatport-top10-card:hover .beatport-top10-card-artwork img, +.beatport-hype10-card:hover .beatport-hype10-card-artwork img { + transform: scale(1.15); +} + +/* Artwork placeholder */ +.beatport-top10-card-placeholder, .beatport-hype10-card-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + background: linear-gradient(135deg, rgba(40, 40, 40, 0.8), rgba(60, 60, 60, 0.6)); + color: rgba(255, 255, 255, 0.4); +} + +/* Track info */ +.beatport-top10-card-info, .beatport-hype10-card-info { + flex: 1; + min-width: 0; /* Critical for text truncation */ + display: flex; + flex-direction: column; + gap: 4px; + overflow: hidden; /* Prevent text overflow */ + width: 100%; +} + +.beatport-top10-card-title, .beatport-hype10-card-title { + font-size: 15px; + font-weight: 700; + color: #ffffff; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8); + max-width: 100%; + width: 100%; +} + +.beatport-top10-card-artist, .beatport-hype10-card-artist { + font-size: 13px; + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + width: 100%; +} + +.beatport-top10-card-label { + font-size: 11px; + color: rgba(0, 255, 136, 0.9); + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-transform: uppercase; + letter-spacing: 0.5px; + max-width: 100%; + width: 100%; +} + +.beatport-hype10-card-label { + font-size: 11px; + color: rgba(255, 51, 102, 0.9); + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-transform: uppercase; + letter-spacing: 0.5px; + max-width: 100%; + width: 100%; +} + +/* Error states */ +.beatport-top10-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 200px; + background: linear-gradient(135deg, + rgba(255, 51, 51, 0.1), + rgba(40, 40, 40, 0.8)); + border-radius: 16px; + border: 1px solid rgba(255, 51, 51, 0.2); + text-align: center; + padding: 30px; +} + +.beatport-top10-error h3 { + color: #ff3366; + font-size: 18px; + margin-bottom: 8px; +} + +.beatport-top10-error p { + color: rgba(255, 255, 255, 0.7); + font-size: 14px; + margin: 0; +} + +/* Loading states */ +.beatport-top10-loading, .beatport-hype10-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + 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.05); +} + +.beatport-top10-loading-content, .beatport-hype10-loading-content { + text-align: center; +} + +.beatport-top10-loading-content h4, .beatport-hype10-loading-content h4 { + font-size: 18px; + color: #ffffff; + margin-bottom: 8px; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8); +} + +.beatport-top10-loading-content p, .beatport-hype10-loading-content p { + font-size: 14px; + color: rgba(255, 255, 255, 0.6); + margin: 0; +} + +/* Top 10 Lists Responsive Design */ +@media (max-width: 1300px) { + .beatport-top10-card-title, .beatport-hype10-card-title { + font-size: 14px; + } + + .beatport-top10-card-artist, .beatport-hype10-card-artist { + font-size: 12px; + } + + .beatport-top10-card-label, .beatport-hype10-card-label { + font-size: 10px; + } + + .beatport-top10-card-rank, .beatport-hype10-card-rank { + font-size: 18px; + } +} + +@media (max-width: 1200px) { + .beatport-top10-container { + gap: 30px; + max-width: 1200px; + } + + .beatport-top10-list, .beatport-hype10-list { + padding: 25px; + max-width: 100%; + } + + .beatport-top10-card, .beatport-hype10-card { + min-height: 75px; + max-height: 75px; + } + + .beatport-top10-card-artwork, .beatport-hype10-card-artwork { + width: 48px; + height: 48px; + } +} + +@media (max-width: 900px) { + .beatport-top10-container { + grid-template-columns: 1fr; + gap: 25px; + } + + .beatport-top10-section { + padding: 0 20px; + } + + .beatport-top10-title { + font-size: 28px; + } + + .beatport-top10-list-title, .beatport-hype10-list-title { + font-size: 20px; + } + + .beatport-top10-card, .beatport-hype10-card { + padding: 12px; + min-height: 70px; + max-height: 70px; + } + + .beatport-top10-card-artwork, .beatport-hype10-card-artwork { + width: 42px; + height: 42px; + margin-right: 12px; + } + + .beatport-top10-card-rank, .beatport-hype10-card-rank { + font-size: 16px; + min-width: 30px; + margin-right: 12px; + padding: 6px 0; + } + +} + +@media (max-width: 600px) { + .beatport-top10-section { + padding: 0 15px; + margin-top: 30px; + margin-bottom: 30px; + } + + .beatport-top10-header { + margin-bottom: 30px; + } + + .beatport-top10-title { + font-size: 24px; + } + + .beatport-top10-list, .beatport-hype10-list { + padding: 20px; + } + + .beatport-top10-list-title, .beatport-hype10-list-title { + font-size: 18px; + } + + .beatport-top10-card, .beatport-hype10-card { + padding: 10px; + min-height: 65px; + max-height: 65px; + } + + .beatport-top10-card-artwork, .beatport-hype10-card-artwork { + width: 38px; + height: 38px; + margin-right: 10px; + } + + .beatport-top10-card-rank, .beatport-hype10-card-rank { + font-size: 14px; + min-width: 26px; + padding: 4px 0; + margin-right: 10px; + } + + .beatport-top10-card-title, .beatport-hype10-card-title { + font-size: 13px; + } + + .beatport-top10-card-artist, .beatport-hype10-card-artist { + font-size: 11px; + } + + .beatport-top10-card-label, .beatport-hype10-card-label { + font-size: 9px; + } +} + /* ================================ BEATPORT NEW RELEASES SECTION ================================ */