diff --git a/beatport_unified_scraper.py b/beatport_unified_scraper.py
index 860720dd..0b3c8eb2 100644
--- a/beatport_unified_scraper.py
+++ b/beatport_unified_scraper.py
@@ -1284,6 +1284,470 @@ class BeatportUnifiedScraper:
print(f" ā Error converting 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...")
+
+ soup = self.get_page(self.base_url)
+ if not soup:
+ return []
+
+ tracks = []
+
+ # Method 1: Look for the specific wrapper class you mentioned
+ hero_wrapper = soup.find('div', class_='Homepage-style__NewOnBeatportWrapper-sc-deeb4244-2 iyIchZ')
+ if hero_wrapper:
+ print(" ā Found Homepage NewOnBeatportWrapper")
+ tracks.extend(self._extract_from_hero_wrapper(hero_wrapper, limit))
+
+ # Method 2: Look for carousel with aria attributes you mentioned
+ if len(tracks) < 5: # Only try if we don't have enough tracks
+ carousel = soup.find('div', {'aria-roledescription': 'carousel', 'aria-label': 'Carousel'})
+ if carousel:
+ print(" ā Found carousel with aria-roledescription and aria-label")
+ additional_tracks = self._extract_from_carousel(carousel, limit)
+ # Merge without duplicates
+ existing_urls = {track.get('url') for track in tracks}
+ for track in additional_tracks:
+ if track.get('url') not in existing_urls:
+ tracks.append(track)
+
+ # Method 3: Look for individual slide items more broadly
+ if len(tracks) < 5:
+ print(" š Looking for individual carousel items...")
+ carousel_items = soup.find_all(['div', 'article'], class_=re.compile(r'carousel.*item|item.*carousel|slide', re.I))
+ print(f" Found {len(carousel_items)} potential carousel items")
+
+ for i, item in enumerate(carousel_items[:limit * 2]): # Check more items
+ track_data = self._extract_track_from_slide(item, f"Carousel Item {i+1}")
+ if track_data and track_data.get('url'):
+ # Check for duplicate URLs
+ existing_urls = {track.get('url') for track in tracks}
+ if track_data['url'] not in existing_urls:
+ tracks.append(track_data)
+
+ print(f" š Extracted {len(tracks)} tracks from New on Beatport hero")
+ return tracks[:limit]
+
+ def _extract_from_hero_wrapper(self, wrapper, limit: int) -> List[Dict]:
+ """Extract tracks from the specific NewOnBeatportWrapper"""
+ tracks = []
+
+ # Method 1: Look for all release/track links within the wrapper
+ release_links = wrapper.find_all('a', href=re.compile(r'/release/|/track/'))
+
+ seen_urls = set()
+ for i, link in enumerate(release_links):
+ href = link.get('href')
+ if href and href not in seen_urls:
+ seen_urls.add(href)
+
+ # Find the parent container that likely contains all track info
+ parent = link.find_parent(['div', 'article', 'section'])
+ if parent:
+ track_data = self._extract_track_from_slide(parent, f"Hero Release {i+1}")
+ if track_data:
+ tracks.append(track_data)
+
+ # Method 2: If not enough tracks, try broader slide detection
+ if len(tracks) < 5:
+ slides = wrapper.find_all(['div', 'article', 'section'], class_=re.compile(r'slide|item|card', re.I))
+
+ for i, slide in enumerate(slides[:limit]):
+ track_data = self._extract_track_from_slide(slide, f"Hero Slide {i+1}")
+ if track_data:
+ # Check for duplicates by URL
+ url = track_data.get('url')
+ if url and url not in seen_urls:
+ seen_urls.add(url)
+ tracks.append(track_data)
+
+ # Method 3: If still not enough, try finding all elements with images
+ if len(tracks) < 5:
+ image_containers = wrapper.find_all(['div', 'figure'], recursive=True)
+
+ for i, container in enumerate(image_containers):
+ if container.find('img') and container.find('a'):
+ track_data = self._extract_track_from_slide(container, f"Hero Image {i+1}")
+ if track_data:
+ url = track_data.get('url')
+ if url and url not in seen_urls:
+ seen_urls.add(url)
+ tracks.append(track_data)
+ if len(tracks) >= limit:
+ break
+
+ return tracks
+
+ def _extract_from_carousel(self, carousel, limit: int) -> List[Dict]:
+ """Extract tracks from carousel element"""
+ tracks = []
+
+ # Look for individual slides within carousel
+ slides = carousel.find_all(['div', 'article', 'li'], class_=re.compile(r'slide|item|card', re.I))
+
+ if not slides:
+ # Try alternative selectors
+ slides = carousel.find_all(['div', 'article'], recursive=True)
+ slides = [s for s in slides if s.find('a') or s.find('img') or 'track' in str(s.get('class', '')).lower()]
+
+ for i, slide in enumerate(slides[:limit]):
+ track_data = self._extract_track_from_slide(slide, f"Carousel Slide {i+1}")
+ if track_data:
+ tracks.append(track_data)
+
+ return tracks
+
+ def _extract_from_hero_element(self, element, limit: int) -> List[Dict]:
+ """Extract tracks from general hero element"""
+ tracks = []
+
+ # Look for any trackable items
+ items = element.find_all(['div', 'article', 'a'], recursive=True)
+ track_items = []
+
+ for item in items:
+ # Filter for elements likely to contain track info
+ if (item.find('img') or
+ 'track' in str(item.get('class', '')).lower() or
+ 'release' in str(item.get('class', '')).lower() or
+ item.get('href', '').count('/') > 2):
+ track_items.append(item)
+
+ for i, item in enumerate(track_items[:limit]):
+ track_data = self._extract_track_from_slide(item, f"Hero Item {i+1}")
+ if track_data:
+ tracks.append(track_data)
+
+ return tracks
+
+ def _extract_track_from_slide(self, slide, context: str) -> Optional[Dict]:
+ """Extract track information from a slide/item element"""
+ try:
+ track_data = {}
+
+ # Extract image
+ img = slide.find('img')
+ if img:
+ track_data['image_url'] = img.get('src') or img.get('data-src')
+ track_data['alt_text'] = img.get('alt', '')
+
+ # Extract link URL
+ link = slide.find('a')
+ if link:
+ href = link.get('href')
+ if href:
+ track_data['url'] = urljoin(self.base_url, href)
+
+ # Enhanced title/track name extraction
+ title_selectors = [
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+ '[class*="title"]', '[class*="name"]', '[class*="track"]',
+ '[data-testid*="title"]', '[data-testid*="name"]',
+ # Beatport-specific selectors
+ '[class*="TrackTitle"]', '[class*="ReleaseTitle"]',
+ '[class*="Title"]', 'span:contains(".")'
+ ]
+
+ for selector in title_selectors:
+ title_elem = slide.select_one(selector)
+ if title_elem and title_elem.get_text(strip=True):
+ title_text = title_elem.get_text(strip=True)
+ # Filter out common non-title text
+ if title_text not in ['New on Beatport', 'Previous slide', 'Next slide', 'EXCLUSIVE', 'HYPE']:
+ track_data['title'] = title_text
+ break
+
+ # Enhanced artist extraction
+ artist_selectors = [
+ '[class*="artist"]', '[class*="by"]', '[class*="author"]',
+ '[data-testid*="artist"]', '[data-testid*="by"]',
+ # Beatport-specific selectors
+ '[class*="Artist"]', '[class*="Label"]'
+ ]
+
+ for selector in artist_selectors:
+ artist_elem = slide.select_one(selector)
+ if artist_elem and artist_elem.get_text(strip=True):
+ track_data['artist'] = artist_elem.get_text(strip=True)
+ break
+
+ # Extract any text content for analysis
+ all_text = slide.get_text(strip=True)
+ if all_text:
+ track_data['raw_text'] = all_text[:400] # More chars for analysis
+
+ # Try to parse title and artist from raw text if not found
+ if not track_data.get('title') or not track_data.get('artist'):
+ parsed_data = self._parse_title_artist_from_raw_text(all_text)
+ if parsed_data.get('title') and not track_data.get('title'):
+ track_data['title'] = parsed_data['title']
+ if parsed_data.get('artist') and not track_data.get('artist'):
+ track_data['artist'] = parsed_data['artist']
+
+ # FALLBACK: Extract title from URL slug if still no title/artist found
+ if (not track_data.get('title') or not track_data.get('artist')) and track_data.get('url'):
+ url_data = self._extract_title_artist_from_url(track_data['url'])
+ if url_data.get('title') and not track_data.get('title'):
+ track_data['title'] = url_data['title']
+ if url_data.get('artist') and not track_data.get('artist'):
+ track_data['artist'] = url_data.get('artist', 'Various Artists')
+
+ # Apply final cleaning to all extracted data
+ if track_data.get('title'):
+ track_data['title'] = self._clean_title(track_data['title'])
+ if track_data.get('artist'):
+ track_data['artist'] = self._clean_artist(track_data['artist'])
+
+ # Extract all class names for debugging
+ classes = slide.get('class', [])
+ if classes:
+ track_data['element_classes'] = ' '.join(classes)
+
+ # Only return if we found at least some useful data
+ if track_data.get('title') or track_data.get('artist') or track_data.get('url') or track_data.get('image_url'):
+ track_data['source'] = f"New on Beatport Hero - {context}"
+ track_data['scraped_at'] = time.time()
+ print(f" ā {context}: {track_data.get('title', 'No title')} - {track_data.get('artist', 'No artist')}")
+ return track_data
+ else:
+ print(f" ā {context}: No usable data found")
+ return None
+
+ except Exception as e:
+ print(f" ā Error extracting from {context}: {e}")
+ return None
+
+ def _extract_title_artist_from_url(self, url: str) -> Dict[str, str]:
+ """Extract title and artist from Beatport URL slug as fallback"""
+ result = {}
+
+ try:
+ # Extract the slug from URL like: https://beatport.com/release/gods-window-pt-1/5291662
+ if '/release/' in url:
+ parts = url.split('/release/')
+ if len(parts) > 1:
+ slug_part = parts[1].split('/')[0] # Get "gods-window-pt-1"
+
+ # Convert slug to title (replace hyphens with spaces, title case)
+ title = slug_part.replace('-', ' ').title()
+
+ # Clean up common patterns
+ title = title.replace(' Pt ', ' Pt. ')
+ title = title.replace(' Ep', ' EP')
+ title = title.replace(' Feat ', ' feat. ')
+ title = title.replace(' Vs ', ' vs. ')
+ title = title.replace(' Remix', ' Remix')
+
+ result['title'] = title
+
+ elif '/track/' in url:
+ parts = url.split('/track/')
+ if len(parts) > 1:
+ slug_part = parts[1].split('/')[0]
+ title = slug_part.replace('-', ' ').title()
+ result['title'] = title
+
+ except Exception as e:
+ pass # Silently handle URL extraction errors
+
+ return result
+
+ def _parse_title_artist_from_raw_text(self, raw_text: str) -> Dict[str, str]:
+ """Parse title and artist from raw text using patterns"""
+ result = {}
+
+ if not raw_text:
+ return result
+
+ # Remove common Beatport UI elements
+ text = raw_text.replace('New on Beatport', '').replace('Previous slide', '').replace('Next slide', '')
+ text = text.replace('EXCLUSIVE', '').replace('HYPE', '').replace('PlayAdd to queueAdd to playlist', '')
+
+ # Pattern 1: Look for track title followed by artist names (common Beatport pattern)
+ # Example: "Gods window, Pt. 1Thakzin,Thandazo,Xelimpilo"
+ lines = [line.strip() for line in text.split('\n') if line.strip()]
+
+ for i, line in enumerate(lines):
+ # Look for lines that might contain title and artists
+ if len(line) > 5 and '$' not in line and 'Music' in line:
+ # This might be a title line
+ # Check if the next part contains artist names
+ words = line.split()
+ for j in range(1, len(words)):
+ potential_title = ' '.join(words[:j])
+ potential_artists = ' '.join(words[j:])
+
+ # Check if we have a reasonable title and artist split
+ if (len(potential_title) > 2 and len(potential_artists) > 2 and
+ ',' in potential_artists): # Artists often comma-separated
+ result['title'] = potential_title
+ result['artist'] = potential_artists.split(',')[0] # First artist
+ break
+
+ if result.get('title'):
+ break
+
+ # Pattern 2: Look for specific patterns in the text
+ patterns = [
+ # Pattern: "Title"Artist1,Artist2 (with capital letter start for artist)
+ r'([A-Za-z\'\s\(\)][^,]{2,40})([A-Z][a-z][^,]{2,}(?:,[A-Z][^,]+)*)',
+ # Pattern: Look for quoted titles
+ r'"([^"]+)"([^$]+)',
+ r"'([^']+)'([^$]+)",
+ # Pattern: Title followed by artist names (looser)
+ r'([A-Za-z\'\s\(\)][^,]{2,25})\s+([A-Z][a-z][A-Za-z\s]{2,25})',
+ ]
+
+ for pattern in patterns:
+ match = re.search(pattern, text)
+ if match and not result.get('title'):
+ potential_title = match.group(1).strip()
+ potential_artist = match.group(2).strip()
+
+ # Additional validation
+ if (len(potential_title) > 2 and len(potential_artist) > 2 and
+ not potential_title.endswith('Music') and
+ not potential_artist.startswith('$')):
+ result['title'] = potential_title
+ result['artist'] = potential_artist.split(',')[0] # First artist
+ break
+
+ # Pattern 3: Handle concatenated cases like "Come to MeDarius Syrossian"
+ if not result.get('title') and not result.get('artist'):
+ # Look for cases where title+artist are concatenated
+ concatenated_pattern = r'([A-Za-z\'\s\(\)][^A-Z]{3,25})([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)'
+ match = re.search(concatenated_pattern, text)
+ if match:
+ potential_title = match.group(1).strip()
+ potential_artist = match.group(2).strip()
+
+ # Make sure it looks reasonable
+ if (len(potential_title) > 2 and len(potential_artist) > 2 and
+ ' ' in potential_artist and # Artist should have space (first + last name)
+ not potential_title.endswith('Music')):
+ result['title'] = potential_title
+ result['artist'] = potential_artist
+
+ # Clean up results
+ if result.get('title'):
+ # Clean title - preserve common music characters
+ title = result['title']
+ title = re.sub(r'[^\w\s\(\)\-\.\'\&]', ' ', title)
+ title = re.sub(r'\s+', ' ', title).strip()
+ result['title'] = title
+
+ if result.get('artist'):
+ # Clean artist - handle multiple artists and remove label names
+ artist = result['artist']
+
+ # Remove common label/publisher suffixes
+ label_patterns = [
+ r'\s*Music\s*$', r'\s*Records?\s*$', r'\s*Recordings?\s*$',
+ r'\s*Entertainment\s*$', r'\s*Productions?\s*$',
+ r'\s*Label\s*$', r'elrow\s*Music\s*$',
+ r'Happy\s*Techno\s*Music\s*$', r'In\s*It\s*Together\s*Records?\s*$'
+ ]
+
+ for pattern in label_patterns:
+ artist = re.sub(pattern, '', artist, flags=re.IGNORECASE)
+
+ # Take only the first artist if comma-separated
+ if ',' in artist:
+ artist = artist.split(',')[0].strip()
+
+ # Clean special characters but preserve common artist name characters
+ artist = re.sub(r'[^\w\s\-\.\'\&]', ' ', artist)
+ artist = re.sub(r'\s+', ' ', artist).strip()
+
+ # Remove trailing/leading words that don't look like artist names
+ words = artist.split()
+ cleaned_words = []
+ for word in words:
+ # Skip words that are clearly not part of artist names
+ if word.lower() not in ['music', 'records', 'record', 'entertainment',
+ 'productions', 'production', 'label', 'remix',
+ 'featuring', 'feat', 'ft']:
+ cleaned_words.append(word)
+ else:
+ break # Stop at first label-like word
+
+ if cleaned_words:
+ result['artist'] = ' '.join(cleaned_words)
+ else:
+ result['artist'] = artist # Fallback to original if all words filtered
+
+ return result
+
+ def _clean_title(self, title: str) -> str:
+ """Clean and standardize track title"""
+ if not title:
+ return title
+
+ # Remove common suffixes that get attached
+ title = re.sub(r'(Darius\s+Syrossian.*|Happy\s+Techno.*|Ron\s*$)', '', title, flags=re.IGNORECASE)
+
+ # Clean title - preserve common music characters
+ title = re.sub(r'[^\w\s\(\)\-\.\'\&]', ' ', title)
+ title = re.sub(r'\s+', ' ', title).strip()
+
+ # Remove trailing words that don't belong in titles
+ words = title.split()
+ cleaned_words = []
+ for word in words:
+ # Stop at artist names or label words
+ if (word[0].isupper() and len(word) > 2 and
+ word.lower() not in ['the', 'of', 'and', 'in', 'on', 'at', 'to', 'for', 'pt']):
+ # This might be an artist name starting
+ break
+ cleaned_words.append(word)
+
+ if cleaned_words:
+ return ' '.join(cleaned_words)
+ return title
+
+ def _clean_artist(self, artist: str) -> str:
+ """Clean and standardize artist name"""
+ if not artist:
+ return artist
+
+ # Remove common label/publisher suffixes
+ label_patterns = [
+ r'\s*Music\s*$', r'\s*Records?\s*$', r'\s*Recordings?\s*$',
+ r'\s*Entertainment\s*$', r'\s*Productions?\s*$',
+ r'\s*Label\s*$', r'elrow\s*Music\s*$',
+ r'Happy\s*Techno\s*Music\s*$', r'In\s*It\s*Together\s*Records?\s*$',
+ r'Musicelrow\s*Music\s*$', r'Freenzy\s*Musicelrow\s*Music\s*$'
+ ]
+
+ for pattern in label_patterns:
+ artist = re.sub(pattern, '', artist, flags=re.IGNORECASE)
+
+ # Take only the first artist if comma-separated
+ if ',' in artist:
+ artist = artist.split(',')[0].strip()
+
+ # Clean special characters but preserve common artist name characters
+ artist = re.sub(r'[^\w\s\-\.\'\&]', ' ', artist)
+ artist = re.sub(r'\s+', ' ', artist).strip()
+
+ # Remove trailing/leading words that don't look like artist names
+ words = artist.split()
+ cleaned_words = []
+ for word in words:
+ # Skip words that are clearly not part of artist names
+ if word.lower() not in ['music', 'records', 'record', 'entertainment',
+ 'productions', 'production', 'label', 'remix',
+ 'featuring', 'feat', 'ft', 'musicelrow', 'elrow',
+ 'freenzy', 'happy', 'techno']:
+ cleaned_words.append(word)
+ else:
+ break # Stop at first label-like word
+
+ if cleaned_words:
+ return ' '.join(cleaned_words)
+ return artist
+
def scrape_top_10_releases_homepage(self, limit: int = 10) -> List[Dict]:
"""Scrape Top 10 Releases from homepage - Extract individual tracks using URL crawling"""
print("\nš Scraping Top 10 Releases from homepage...")
@@ -2597,7 +3061,19 @@ def main():
scraper = BeatportUnifiedScraper()
- # Test improved chart sections first
+ # Test New on Beatport Hero first
+ print("\nšÆ NEW ON BEATPORT HERO TEST")
+ hero_tracks = scraper.scrape_new_on_beatport_hero(limit=10)
+ if hero_tracks:
+ print(f"ā Successfully extracted {len(hero_tracks)} tracks from hero slideshow")
+ for i, track in enumerate(hero_tracks[:3]): # Show first 3
+ print(f" {i+1}. {track.get('title', 'No title')} - {track.get('artist', 'No artist')}")
+ print(f" URL: {track.get('url', 'No URL')}")
+ print(f" Classes: {track.get('element_classes', 'No classes')}")
+ else:
+ print("ā No tracks found in hero slideshow")
+
+ # Test improved chart sections
print("\nš IMPROVED CHART SECTIONS TEST")
improved_results = test_improved_chart_sections()
diff --git a/web_server.py b/web_server.py
index 0175d54f..7d3bba85 100644
--- a/web_server.py
+++ b/web_server.py
@@ -13,6 +13,7 @@ import glob
import uuid
import re
from pathlib import Path
+from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import Flask, render_template, request, jsonify, redirect, send_file, Response
@@ -1991,6 +1992,224 @@ def tidal_callback():
return f"
ā An Error Occurred
An unexpected error occurred during the authentication process: {e}
", 500
+# --- Beatport Data API ---
+
+@app.route('/api/beatport/hero-tracks')
+def get_beatport_hero_tracks():
+ """Get fresh tracks from Beatport hero slideshow for the rebuild slider"""
+ try:
+ logger.info("šÆ Fetching Beatport hero tracks...")
+
+ # Initialize scraper
+ scraper = BeatportUnifiedScraper()
+
+ # Get tracks from hero slideshow (increased limit to capture all slides)
+ tracks = scraper.scrape_new_on_beatport_hero(limit=15)
+
+ # SMART FILTERING - Remove duplicates and invalid tracks
+ valid_tracks = []
+ seen_urls = set()
+
+ logger.info(f"š Processing {len(tracks)} raw tracks from scraper (SMART FILTERING)...")
+
+ for i, track in enumerate(tracks):
+ logger.info(f" Track {i+1}: {track.get('title', 'NO_TITLE')} - {track.get('artist', 'NO_ARTIST')}")
+ logger.info(f" URL: {track.get('url', 'NO_URL')}")
+ logger.info(f" Image: {'YES' if track.get('image_url') else 'NO'}")
+
+ # Extract and clean basic data
+ title = track.get('title', '').strip()
+ artist = track.get('artist', '').strip()
+ url = track.get('url', '').strip()
+ image_url = track.get('image_url', '').strip()
+
+ # Validation filters
+ is_valid = True
+ skip_reasons = []
+
+ # Filter 1: Must have title (artist can be fallback)
+ if not title or title in ['No title', 'MISSING', 'Unknown Title']:
+ is_valid = False
+ skip_reasons.append("Missing/invalid title")
+
+ # If no artist, use fallback based on URL or default
+ if not artist or artist in ['No artist', 'MISSING', 'Unknown Artist', 'NO_ARTIST']:
+ if url and '/release/' in url:
+ artist = 'Various Artists' # Release pages often have multiple artists
+ else:
+ artist = 'Unknown Artist'
+
+ # Filter 2: Must have valid URL and image
+ if not url or not image_url:
+ is_valid = False
+ skip_reasons.append("Missing URL or image")
+
+ # Filter 3: URL must be a track/release page (not promotional pages)
+ if url and not any(pattern in url for pattern in ['/release/', '/track/']):
+ is_valid = False
+ skip_reasons.append("URL is not a track/release page")
+
+ # Filter 4: Deduplication by URL (most reliable method)
+ if url in seen_urls:
+ is_valid = False
+ skip_reasons.append("Duplicate URL")
+
+ if not is_valid:
+ logger.info(f" ā Track {i+1} filtered out: {', '.join(skip_reasons)}")
+ continue
+
+ # Mark URL as seen for deduplication
+ seen_urls.add(url)
+
+ # Clean up title
+ title = title.replace(" t ", "'t ").replace("(Extended)DJ", "(Extended)")
+
+ # Clean up artist names
+ if 'SyrossianHappy' in artist:
+ artist = 'Darius Syrossian'
+ if 'Carroll,' in artist:
+ artist = 'Ron Carroll'
+ if artist.endswith('DJ') and ' ' not in artist[-4:]:
+ artist = artist[:-2].strip()
+
+ # Create clean track data
+ track_data = {
+ 'title': title,
+ 'artist': artist,
+ 'url': url,
+ 'image_url': image_url,
+ 'genre': 'Electronic', # Default genre
+ 'year': datetime.now().year
+ }
+
+ # Determine genre based on artist
+ genre_mapping = {
+ 'thakzin': 'Afro House',
+ 'yaya': 'Tech House',
+ 'darius syrossian': 'Techno',
+ 'ron carroll': 'House',
+ 'dj minx': 'House',
+ 'durante': 'Progressive House'
+ }
+
+ for artist_key, mapped_genre in genre_mapping.items():
+ if artist_key in artist.lower():
+ track_data['genre'] = mapped_genre
+ break
+
+ valid_tracks.append(track_data)
+ logger.info(f" ā Track {i+1} added: {title} - {artist}")
+
+ logger.info(f"ā Retrieved {len(valid_tracks)} valid unique Beatport tracks (SMART FILTERING)")
+
+ return jsonify({
+ 'success': True,
+ 'tracks': valid_tracks,
+ 'count': len(valid_tracks),
+ 'timestamp': datetime.now().isoformat()
+ })
+
+ except Exception as e:
+ logger.error(f"ā Error fetching Beatport tracks: {str(e)}")
+ return jsonify({
+ 'success': False,
+ 'error': str(e),
+ 'tracks': []
+ }), 500
+
+@app.route('/api/beatport/new-releases')
+def get_beatport_new_releases():
+ """Get new releases from Beatport for the rebuild slider grid"""
+ try:
+ logger.info("š Fetching Beatport new releases...")
+
+ # 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 release cards using the working CSS selector
+ release_cards = soup.select('.ReleaseCard-style__Wrapper-sc-7c61989b-12.duhBUN')
+ releases = []
+
+ logger.info(f"š Found {len(release_cards)} release cards")
+
+ for i, card in enumerate(release_cards[:100]): # Limit to 100 for 10 slides
+ release_data = {}
+
+ # Extract title
+ 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 ['New Releases', 'Buy', 'Play']:
+ release_data['title'] = title_text
+
+ # Extract artist
+ 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
+ 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
+ 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
+ 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
+ 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
+ if release_data.get('title') and release_data.get('url'):
+ # Add fallbacks for missing data
+ 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)} new releases")
+
+ return jsonify({
+ 'success': True,
+ 'releases': releases,
+ 'count': len(releases),
+ 'slides': (len(releases) + 9) // 10, # Calculate number of slides needed
+ 'timestamp': datetime.now().isoformat()
+ })
+
+ except Exception as e:
+ logger.error(f"ā Error fetching new releases: {str(e)}")
+ return jsonify({
+ 'success': False,
+ 'error': str(e),
+ 'releases': []
+ }), 500
+
# --- Placeholder API Endpoints for Other Pages ---
@app.route('/api/activity')
diff --git a/webui/index.html b/webui/index.html
index cfa48463..86b1b827 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -389,6 +389,9 @@
+
@@ -775,6 +778,67 @@
Your created Beatport playlists will appear here.
+
+
+
+
+
+
+
+
+
+
šÆ Loading Fresh Beatport Tracks...
+
Fetching the latest music from Beatport
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
š New Releases
+
Latest albums and EPs from Beatport
+
+
+
+
+
+
+
+
+
š Loading New Releases...
+
Fetching the latest albums and EPs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webui/static/script.js b/webui/static/script.js
index cfd628cf..71428b59 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -9730,6 +9730,12 @@ function initializeSyncPage() {
content.classList.remove('active');
});
document.getElementById(`beatport-${tabId}-content`).classList.add('active');
+
+ // Initialize rebuild slider if rebuild tab is selected
+ if (tabId === 'rebuild') {
+ initializeBeatportRebuildSlider();
+ initializeBeatportReleasesSlider();
+ }
});
});
@@ -18218,3 +18224,645 @@ async function updateLibraryWatchlistButtonStatus(artistId) {
console.warn('Failed to check library watchlist status:', error);
}
}
+
+// =================================
+// BEATPORT REBUILD SLIDER FUNCTIONALITY
+// =================================
+
+let beatportRebuildSliderState = {
+ currentSlide: 0,
+ totalSlides: 4,
+ autoPlayInterval: null,
+ autoPlayDelay: 5000
+};
+
+/**
+ * Initialize the beatport rebuild slider functionality
+ */
+function initializeBeatportRebuildSlider() {
+ console.log('š Initializing beatport rebuild slider...');
+
+ const slider = document.getElementById('beatport-rebuild-slider');
+ if (!slider) {
+ console.warn('Beatport rebuild slider not found');
+ return;
+ }
+
+ // Check if already initialized to prevent duplicate event listeners
+ if (slider.dataset.initialized === 'true') {
+ console.log('Beatport rebuild slider already initialized, skipping...');
+ startBeatportRebuildSliderAutoPlay(); // Just restart autoplay
+ return;
+ }
+
+ // Mark as initialized
+ slider.dataset.initialized = 'true';
+
+ // Load real Beatport data first
+ loadBeatportHeroTracks();
+
+ console.log('ā Beatport rebuild slider initialized successfully');
+}
+
+/**
+ * Load real Beatport hero tracks and populate the slider
+ */
+async function loadBeatportHeroTracks() {
+ console.log('šÆ Loading real Beatport hero tracks...');
+
+ try {
+ const response = await fetch('/api/beatport/hero-tracks');
+ const data = await response.json();
+
+ if (data.success && data.tracks && data.tracks.length > 0) {
+ console.log(`ā Loaded ${data.tracks.length} Beatport tracks`);
+ populateBeatportSlider(data.tracks);
+ } else {
+ console.warn('ā No tracks received from Beatport API, using placeholder data');
+ setupBeatportSliderWithPlaceholders();
+ }
+ } catch (error) {
+ console.error('ā Error loading Beatport tracks:', error);
+ setupBeatportSliderWithPlaceholders();
+ }
+}
+
+/**
+ * Populate the slider with real Beatport track data
+ */
+function populateBeatportSlider(tracks) {
+ const sliderTrack = document.getElementById('beatport-rebuild-slider-track');
+ const indicatorsContainer = document.querySelector('.beatport-rebuild-slider-indicators');
+
+ if (!sliderTrack || !indicatorsContainer) {
+ console.warn('Slider elements not found');
+ return;
+ }
+
+ // Clear existing content
+ sliderTrack.innerHTML = '';
+ indicatorsContainer.innerHTML = '';
+
+ // Update state
+ beatportRebuildSliderState.totalSlides = tracks.length;
+ beatportRebuildSliderState.currentSlide = 0;
+
+ // Generate slides HTML
+ tracks.forEach((track, index) => {
+ const slideHtml = `
+
+
+
+
+
+
+
${track.title}
+
${track.artist}
+
New on Beatport
+
+
+ ${track.genre}
+ ${track.year}
+
+
+
+ `;
+ sliderTrack.insertAdjacentHTML('beforeend', slideHtml);
+
+ // Add indicator
+ const indicatorHtml = ``;
+ indicatorsContainer.insertAdjacentHTML('beforeend', indicatorHtml);
+ });
+
+ // Now set up all the functionality
+ setupBeatportSliderFunctionality();
+
+ console.log(`ā Populated slider with ${tracks.length} real Beatport tracks`);
+}
+
+/**
+ * Set up placeholder data if API fails
+ */
+function setupBeatportSliderWithPlaceholders() {
+ console.log('š Setting up slider with placeholder data...');
+
+ // The HTML already has placeholder slides, just set up functionality
+ setupBeatportSliderFunctionality();
+}
+
+/**
+ * Set up all slider functionality after content is loaded
+ */
+function setupBeatportSliderFunctionality() {
+ // Set up navigation buttons
+ setupBeatportRebuildSliderNavigation();
+
+ // Set up indicators
+ setupBeatportRebuildSliderIndicators();
+
+ // Set up slide click handlers
+ setupBeatportRebuildSlideClickHandlers();
+
+ // Start auto-play
+ startBeatportRebuildSliderAutoPlay();
+
+ // Set up pause on hover
+ setupBeatportRebuildSliderHoverPause();
+}
+
+/**
+ * Set up navigation button functionality
+ */
+function setupBeatportRebuildSliderNavigation() {
+ const prevBtn = document.getElementById('beatport-rebuild-prev-btn');
+ const nextBtn = document.getElementById('beatport-rebuild-next-btn');
+
+ if (prevBtn) {
+ prevBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('Previous button clicked, current slide:', beatportRebuildSliderState.currentSlide);
+ goToBeatportRebuildSlide(beatportRebuildSliderState.currentSlide - 1);
+ resetBeatportRebuildSliderAutoPlay();
+ });
+ }
+
+ if (nextBtn) {
+ nextBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ console.log('Next button clicked, current slide:', beatportRebuildSliderState.currentSlide);
+ goToBeatportRebuildSlide(beatportRebuildSliderState.currentSlide + 1);
+ resetBeatportRebuildSliderAutoPlay();
+ });
+ }
+}
+
+/**
+ * Set up indicator functionality
+ */
+function setupBeatportRebuildSliderIndicators() {
+ const indicators = document.querySelectorAll('.beatport-rebuild-indicator');
+
+ indicators.forEach((indicator, index) => {
+ indicator.addEventListener('click', () => {
+ goToBeatportRebuildSlide(index);
+ resetBeatportRebuildSliderAutoPlay();
+ });
+ });
+}
+
+/**
+ * Navigate to a specific slide
+ */
+function goToBeatportRebuildSlide(slideIndex) {
+ console.log('goToBeatportRebuildSlide called with:', slideIndex, 'current:', beatportRebuildSliderState.currentSlide);
+
+ // Wrap around if out of bounds
+ if (slideIndex < 0) {
+ slideIndex = beatportRebuildSliderState.totalSlides - 1;
+ } else if (slideIndex >= beatportRebuildSliderState.totalSlides) {
+ slideIndex = 0;
+ }
+
+ console.log('After wrapping, slideIndex:', slideIndex);
+
+ // Update current slide
+ beatportRebuildSliderState.currentSlide = slideIndex;
+
+ // Update slide visibility
+ const slides = document.querySelectorAll('.beatport-rebuild-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-rebuild-indicator');
+ indicators.forEach((indicator, index) => {
+ indicator.classList.toggle('active', index === slideIndex);
+ });
+
+ console.log('Slide updated to:', beatportRebuildSliderState.currentSlide);
+}
+
+/**
+ * Start auto-play functionality
+ */
+function startBeatportRebuildSliderAutoPlay() {
+ if (beatportRebuildSliderState.autoPlayInterval) {
+ clearInterval(beatportRebuildSliderState.autoPlayInterval);
+ }
+
+ beatportRebuildSliderState.autoPlayInterval = setInterval(() => {
+ goToBeatportRebuildSlide(beatportRebuildSliderState.currentSlide + 1);
+ }, beatportRebuildSliderState.autoPlayDelay);
+}
+
+/**
+ * Reset auto-play timer
+ */
+function resetBeatportRebuildSliderAutoPlay() {
+ startBeatportRebuildSliderAutoPlay();
+}
+
+/**
+ * Set up hover pause functionality
+ */
+function setupBeatportRebuildSliderHoverPause() {
+ const sliderContainer = document.querySelector('.beatport-rebuild-slider-container');
+
+ if (sliderContainer) {
+ sliderContainer.addEventListener('mouseenter', () => {
+ if (beatportRebuildSliderState.autoPlayInterval) {
+ clearInterval(beatportRebuildSliderState.autoPlayInterval);
+ }
+ });
+
+ sliderContainer.addEventListener('mouseleave', () => {
+ startBeatportRebuildSliderAutoPlay();
+ });
+ }
+}
+
+/**
+ * Set up click handlers for slides to open Beatport URLs
+ */
+function setupBeatportRebuildSlideClickHandlers() {
+ const slides = document.querySelectorAll('.beatport-rebuild-slide');
+
+ slides.forEach((slide, index) => {
+ slide.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const url = slide.dataset.url;
+ if (url && url !== '#') {
+ console.log(`šµ Opening Beatport track: ${url}`);
+ window.open(url, '_blank');
+ } else {
+ console.log(`ā¹ļø No URL available for slide ${index + 1}`);
+ }
+ });
+
+ // Add cursor pointer style for clickable slides
+ if (slide.dataset.url && slide.dataset.url !== '#') {
+ slide.style.cursor = 'pointer';
+
+ // Add hover effect
+ slide.addEventListener('mouseenter', () => {
+ slide.style.transform = 'scale(1.02)';
+ slide.style.transition = 'transform 0.3s ease';
+ });
+
+ slide.addEventListener('mouseleave', () => {
+ slide.style.transform = 'scale(1)';
+ });
+ }
+ });
+
+ console.log(`š Set up click handlers for ${slides.length} slides`);
+}
+
+/**
+ * Clean up beatport rebuild slider when switching away
+ */
+function cleanupBeatportRebuildSlider() {
+ if (beatportRebuildSliderState.autoPlayInterval) {
+ clearInterval(beatportRebuildSliderState.autoPlayInterval);
+ beatportRebuildSliderState.autoPlayInterval = null;
+ }
+}
+
+// ===================================
+// BEATPORT NEW RELEASES SLIDER
+// ===================================
+
+// State management for new releases slider (copied from hero slider)
+let beatportReleasesSliderState = {
+ currentSlide: 0,
+ totalSlides: 0,
+ autoPlayInterval: null,
+ autoPlayDelay: 8000,
+ isInitialized: false
+};
+
+/**
+ * Initialize the beatport new releases slider functionality (based on hero slider)
+ */
+function initializeBeatportReleasesSlider() {
+ console.log('š Initializing beatport new releases slider...');
+
+ const slider = document.getElementById('beatport-releases-slider');
+ if (!slider) {
+ console.warn('Beatport releases slider not found');
+ return;
+ }
+
+ // Prevent double initialization
+ if (slider.dataset.initialized === 'true') {
+ console.log('Releases slider already initialized');
+ return;
+ }
+
+ const sliderTrack = document.getElementById('beatport-releases-slider-track');
+ const indicatorsContainer = document.getElementById('beatport-releases-slider-indicators');
+
+ if (!sliderTrack || !indicatorsContainer) {
+ console.warn('Releases slider elements not found');
+ return;
+ }
+
+ // Load data and initialize
+ loadBeatportNewReleases().then(success => {
+ if (success) {
+ setupBeatportReleasesSliderNavigation();
+ setupBeatportReleasesSliderIndicators();
+ setupBeatportReleasesSliderHoverPause();
+ startBeatportReleasesSliderAutoPlay();
+ slider.dataset.initialized = 'true';
+ beatportReleasesSliderState.isInitialized = true;
+ console.log('ā New releases slider initialized successfully');
+ }
+ });
+}
+
+/**
+ * Load new releases data from API
+ */
+async function loadBeatportNewReleases() {
+ try {
+ console.log('š” Fetching new releases data...');
+
+ const response = await fetch('/api/beatport/new-releases');
+ const data = await response.json();
+
+ if (data.success && data.releases && data.releases.length > 0) {
+ console.log(`š Loaded ${data.releases.length} releases`);
+ populateBeatportReleasesSlider(data.releases);
+ return true;
+ } else {
+ console.error('Failed to load releases:', data.error || 'No releases found');
+ showBeatportReleasesError(data.error || 'No releases available');
+ return false;
+ }
+ } catch (error) {
+ console.error('Error loading new releases:', error);
+ showBeatportReleasesError('Failed to load releases');
+ return false;
+ }
+}
+
+/**
+ * Populate the releases slider with data (based on hero slider)
+ */
+function populateBeatportReleasesSlider(releases) {
+ const sliderTrack = document.getElementById('beatport-releases-slider-track');
+ const indicatorsContainer = document.getElementById('beatport-releases-slider-indicators');
+
+ if (!sliderTrack || !indicatorsContainer) return;
+
+ // Calculate slides needed (10 cards per slide)
+ const cardsPerSlide = 10;
+ const totalSlides = Math.ceil(releases.length / cardsPerSlide);
+
+ // Clear existing content
+ sliderTrack.innerHTML = '';
+ indicatorsContainer.innerHTML = '';
+
+ // Update state
+ beatportReleasesSliderState.totalSlides = totalSlides;
+ beatportReleasesSliderState.currentSlide = 0;
+
+ console.log(`šÆ Creating ${totalSlides} slides with ${cardsPerSlide} cards each`);
+
+ // Generate slides HTML (similar to hero slider)
+ for (let slideIndex = 0; slideIndex < totalSlides; slideIndex++) {
+ const startIndex = slideIndex * cardsPerSlide;
+ const endIndex = Math.min(startIndex + cardsPerSlide, releases.length);
+ const slideReleases = releases.slice(startIndex, endIndex);
+
+ // Create grid HTML for this slide
+ let gridHtml = '';
+ for (let i = 0; i < cardsPerSlide; i++) {
+ if (i < slideReleases.length) {
+ const release = slideReleases[i];
+ gridHtml += `
+