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 += ` +
+
+
+ ${release.image_url ? `${release.title}` : ''} +
+
+
${release.title}
+
${release.artist}
+
${release.label}
+
+
+
+ `; + } else { + // Placeholder card + gridHtml += ` +
+
+
+
šŸ“€
+
+
+
More Releases
+
Coming Soon
+
Beatport
+
+
+
+ `; + } + } + + const slideHtml = ` +
+
+ ${gridHtml} +
+
+ `; + + sliderTrack.innerHTML += slideHtml; + + // Create indicator + const indicatorHtml = ``; + indicatorsContainer.innerHTML += indicatorHtml; + } + + console.log(`āœ… Created ${totalSlides} slides for releases slider`); +} + +/** + * Set up navigation functionality (copied from hero slider) + */ +function setupBeatportReleasesSliderNavigation() { + const prevBtn = document.getElementById('beatport-releases-prev-btn'); + const nextBtn = document.getElementById('beatport-releases-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 releases button clicked, current slide:', beatportReleasesSliderState.currentSlide); + goToBeatportReleasesSlide(beatportReleasesSliderState.currentSlide - 1); + resetBeatportReleasesSliderAutoPlay(); + }); + } + + 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 releases button clicked, current slide:', beatportReleasesSliderState.currentSlide); + goToBeatportReleasesSlide(beatportReleasesSliderState.currentSlide + 1); + resetBeatportReleasesSliderAutoPlay(); + }); + } +} + +/** + * Set up indicator functionality (copied from hero slider) + */ +function setupBeatportReleasesSliderIndicators() { + const indicators = document.querySelectorAll('.beatport-releases-indicator'); + + indicators.forEach((indicator, index) => { + indicator.addEventListener('click', () => { + goToBeatportReleasesSlide(index); + resetBeatportReleasesSliderAutoPlay(); + }); + }); +} + +/** + * Navigate to a specific slide (copied from hero slider) + */ +function goToBeatportReleasesSlide(slideIndex) { + console.log('goToBeatportReleasesSlide called with:', slideIndex, 'current:', beatportReleasesSliderState.currentSlide); + + // Wrap around if out of bounds + if (slideIndex < 0) { + slideIndex = beatportReleasesSliderState.totalSlides - 1; + } else if (slideIndex >= beatportReleasesSliderState.totalSlides) { + slideIndex = 0; + } + + console.log('After wrapping, slideIndex:', slideIndex); + + // Update current slide + beatportReleasesSliderState.currentSlide = slideIndex; + + // Update slide visibility + const slides = document.querySelectorAll('.beatport-releases-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-releases-indicator'); + indicators.forEach((indicator, index) => { + indicator.classList.toggle('active', index === slideIndex); + }); + + console.log('Releases slide updated to:', beatportReleasesSliderState.currentSlide); +} + +/** + * Start auto-play functionality (copied from hero slider) + */ +function startBeatportReleasesSliderAutoPlay() { + if (beatportReleasesSliderState.autoPlayInterval) { + clearInterval(beatportReleasesSliderState.autoPlayInterval); + } + + beatportReleasesSliderState.autoPlayInterval = setInterval(() => { + goToBeatportReleasesSlide(beatportReleasesSliderState.currentSlide + 1); + }, beatportReleasesSliderState.autoPlayDelay); +} + +/** + * Reset auto-play timer (copied from hero slider) + */ +function resetBeatportReleasesSliderAutoPlay() { + startBeatportReleasesSliderAutoPlay(); +} + +/** + * Set up hover pause functionality (copied from hero slider) + */ +function setupBeatportReleasesSliderHoverPause() { + const sliderContainer = document.querySelector('.beatport-releases-slider-container'); + + if (sliderContainer) { + sliderContainer.addEventListener('mouseenter', () => { + if (beatportReleasesSliderState.autoPlayInterval) { + clearInterval(beatportReleasesSliderState.autoPlayInterval); + beatportReleasesSliderState.autoPlayInterval = null; + } + }); + + sliderContainer.addEventListener('mouseleave', () => { + startBeatportReleasesSliderAutoPlay(); + }); + } +} + +/** + * Show error state + */ +function showBeatportReleasesError(errorMessage) { + const sliderTrack = document.getElementById('beatport-releases-slider-track'); + if (!sliderTrack) return; + + sliderTrack.innerHTML = ` +
+
+

āŒ Error Loading Releases

+

${errorMessage}

+
+
+ `; +} + +/** + * Clean up releases slider when switching away (copied from hero slider) + */ +function cleanupBeatportReleasesSlider() { + if (beatportReleasesSliderState.autoPlayInterval) { + clearInterval(beatportReleasesSliderState.autoPlayInterval); + beatportReleasesSliderState.autoPlayInterval = null; + } +} diff --git a/webui/static/style.css b/webui/static/style.css index e622c5bd..60841f63 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -4158,7 +4158,7 @@ body { display: grid; grid-template-columns: 2.5fr 0.75fr; /* More space for main panel, smaller sidebar */ gap: 25px; - height: calc(100vh - 200px); /* Adjust height to fit within the page */ + min-height: calc(100vh - 200px); /* Minimum height, but allow expansion */ } .sync-main-panel, .sync-sidebar { @@ -4169,10 +4169,18 @@ body { padding: 20px; display: flex; flex-direction: column; - overflow: hidden; box-shadow: 0 15px 35px rgba(0,0,0,0.5); } +.sync-main-panel { + overflow-y: auto; + overflow-x: hidden; +} + +.sync-sidebar { + overflow: hidden; +} + /* Tab System */ .sync-tabs { display: flex; @@ -4351,10 +4359,19 @@ body { background-image: url('data:image/svg+xml;charset=utf-8,'); } +.rebuild-icon { + background-image: url('data:image/svg+xml;charset=utf-8,'); +} + +.beatport-tab-button.active .rebuild-icon { + background-image: url('data:image/svg+xml;charset=utf-8,'); +} + .beatport-tab-content { display: none; - height: 100%; - overflow: hidden; + overflow-y: auto; + overflow-x: hidden; + flex: 1; } .beatport-tab-content.active { @@ -4362,6 +4379,301 @@ body { flex-direction: column; } +/* ================================= */ +/* BEATPORT REBUILD TAB STYLES */ +/* ================================= */ + +/* Beatport Rebuild Slider Container */ +.beatport-rebuild-slider-container { + flex: 1; + padding: 30px; + +} + +/* Loading State */ +.beatport-rebuild-loading { + width: 100%; + height: 400px; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.95) 0%, + rgba(12, 12, 12, 0.98) 100%); + border-radius: 20px; + text-align: center; +} + +.beatport-rebuild-loading-content h2 { + color: #01FF95; + font-size: 24px; + margin-bottom: 10px; + animation: pulse 2s ease-in-out infinite; +} + +.beatport-rebuild-loading-content p { + color: rgba(255, 255, 255, 0.7); + font-size: 16px; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.beatport-rebuild-slider { + position: relative; + width: 100%; + height: 400px; + border-radius: 20px; + overflow: hidden; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.4), + 0 8px 32px rgba(0, 0, 0, 0.3), + 0 0 40px rgba(1, 255, 149, 0.1); + background: linear-gradient(135deg, + rgba(20, 20, 20, 0.95) 0%, + rgba(12, 12, 12, 0.98) 100%); +} + +.beatport-rebuild-slider-track { + position: relative; + width: 100%; + height: 100%; + transition: transform 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} + +/* Individual Slides */ +.beatport-rebuild-slide { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + transform: translateX(100px); + transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94); + display: flex; + align-items: center; + justify-content: center; +} + +.beatport-rebuild-slide.active { + opacity: 1; + transform: translateX(0); +} + +.beatport-rebuild-slide.prev { + transform: translateX(-100px); +} + +.beatport-rebuild-slide.next { + transform: translateX(100px); +} + +/* Slide Background */ +.beatport-rebuild-slide-background { + opacity: .6; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(45deg, + rgba(1, 255, 149, 0.1) 0%, + rgba(0, 224, 133, 0.08) 25%, + rgba(29, 185, 84, 0.06) 50%, + rgba(0, 185, 112, 0.04) 75%, + rgba(1, 255, 149, 0.02) 100%); + z-index: 1; +} + +/* Dynamic background images from Beatport */ +.beatport-rebuild-slide[data-image]::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: var(--slide-bg-image); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + opacity: 0.3; + z-index: 0; +} + +.beatport-rebuild-slide-gradient { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: + radial-gradient(circle at 30% 40%, rgba(1, 255, 149, 0.15) 0%, transparent 50%), + radial-gradient(circle at 70% 60%, rgba(0, 224, 133, 0.1) 0%, transparent 50%), + linear-gradient(135deg, + rgba(20, 20, 20, 0.8) 0%, + rgba(12, 12, 12, 0.9) 100%); +} + +/* Slide Content */ +.beatport-rebuild-slide-content { + position: relative; + z-index: 2; + text-align: center; + padding: 40px; + max-width: 600px; +} + +.beatport-rebuild-track-info { + margin-bottom: 25px; +} + +.beatport-rebuild-track-title { + font-size: 36px; + font-weight: 800; + color: #01FF95; + margin: 0 0 12px 0; + letter-spacing: -1px; + text-shadow: 0 2px 20px rgba(1, 255, 149, 0.3); + line-height: 1.2; +} + +.beatport-rebuild-artist-name { + font-size: 24px; + font-weight: 600; + color: #ffffff; + margin: 0 0 8px 0; + letter-spacing: -0.5px; +} + +.beatport-rebuild-album-name { + font-size: 18px; + font-weight: 400; + color: rgba(255, 255, 255, 0.8); + margin: 0; + font-style: italic; +} + +.beatport-rebuild-track-meta { + display: flex; + gap: 20px; + justify-content: center; + align-items: center; +} + +.beatport-rebuild-genre, +.beatport-rebuild-year { + padding: 8px 16px; + background: rgba(1, 255, 149, 0.15); + border: 1px solid rgba(1, 255, 149, 0.3); + border-radius: 20px; + font-size: 12px; + font-weight: 600; + color: #01FF95; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Navigation Buttons */ +.beatport-rebuild-slider-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 3; + width: 100%; + display: flex; + justify-content: space-between; + padding: 0 25px; + pointer-events: none; +} + +.beatport-rebuild-nav-btn { + width: 50px; + height: 50px; + border: none; + border-radius: 50%; + background: rgba(1, 255, 149, 0.2); + backdrop-filter: blur(10px); + color: #01FF95; + font-size: 24px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + pointer-events: all; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.beatport-rebuild-nav-btn:hover { + background: rgba(1, 255, 149, 0.3); + transform: scale(1.1); + box-shadow: 0 6px 25px rgba(1, 255, 149, 0.4); +} + +.beatport-rebuild-nav-btn:active { + transform: scale(0.95); +} + +/* Slider Indicators */ +.beatport-rebuild-slider-indicators { + position: absolute; + bottom: 25px; + left: 50%; + transform: translateX(-50%); + z-index: 3; + display: flex; + gap: 12px; +} + +.beatport-rebuild-indicator { + width: 12px; + height: 12px; + border: none; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + cursor: pointer; + transition: all 0.3s ease; +} + +.beatport-rebuild-indicator.active { + background: #01FF95; + box-shadow: 0 0 15px rgba(1, 255, 149, 0.6); + transform: scale(1.2); +} + +.beatport-rebuild-indicator:hover:not(.active) { + background: rgba(1, 255, 149, 0.6); + transform: scale(1.1); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .beatport-rebuild-slider { + height: 320px; + } + + .beatport-rebuild-track-title { + font-size: 28px; + } + + .beatport-rebuild-artist-name { + font-size: 20px; + } + + .beatport-rebuild-album-name { + font-size: 16px; + } + + .beatport-rebuild-slide-content { + padding: 30px 20px; + } +} + /* ================================= */ /* BEATPORT HERO SECTION */ /* ================================= */ @@ -11714,3 +12026,396 @@ body { width: 100%; } } + +/* ================================ + BEATPORT NEW RELEASES SECTION + ================================ */ + +.beatport-releases-section { + margin-top: 40px; + padding: 0 20px; +} + +.beatport-releases-header { + text-align: center; + margin-bottom: 30px; +} + +.beatport-releases-title { + font-size: 32px; + font-weight: bold; + background: linear-gradient(135deg, #1db954, #1ed760, #00d4aa); + -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-releases-subtitle { + font-size: 16px; + color: rgba(255, 255, 255, 0.7); + margin: 0; +} + +.beatport-releases-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-releases-slider { + position: relative; + border-radius: 16px; +} + +.beatport-releases-slider-track { + position: relative; + height: 425px; +} + +.beatport-releases-loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.1), + rgba(30, 215, 96, 0.05)); + border-radius: 16px; + border: 2px dashed rgba(29, 185, 84, 0.3); +} + +.beatport-releases-loading-content { + text-align: center; +} + +.beatport-releases-loading-content h3 { + font-size: 24px; + color: #1db954; + margin-bottom: 8px; + font-weight: 600; +} + +.beatport-releases-loading-content p { + color: rgba(255, 255, 255, 0.6); + font-size: 14px; + margin: 0; +} + +/* Individual Slide (contains 10 cards in 5x2 grid) */ +.beatport-releases-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-releases-slide.active { + opacity: 1; + z-index: 10; + transform: translateX(0); +} + +.beatport-releases-slide.prev { + transform: translateX(-100px); +} + +.beatport-releases-slide.next { + transform: translateX(100px); +} + +.beatport-releases-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; +} + +/* Release Card Styling */ +.beatport-release-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-release-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-release-card:hover { + transform: translateY(-2px) scale(1.05); + background: linear-gradient(145deg, + rgba(29, 185, 84, 0.15), + rgba(40, 40, 40, 0.95)); + border-color: rgba(29, 185, 84, 0.4); + box-shadow: + 0 8px 24px rgba(0, 0, 0, 0.4), + 0 0 16px rgba(29, 185, 84, 0.3), + inset 0 1px 0 rgba(255, 255, 255, 0.15); + z-index: 10; +} + +.beatport-release-card-content { + position: relative; + z-index: 1; + text-align: center; +} + +.beatport-release-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-release-artwork img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.beatport-release-card:hover .beatport-release-artwork img { + transform: scale(1.1); +} + +.beatport-release-info { + text-align: center; +} + +.beatport-release-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-release-artist { + font-size: 11px; + color: rgba(255, 255, 255, 0.7); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.beatport-release-label { + font-size: 10px; + color: rgba(29, 185, 84, 0.8); + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Placeholder Card Styling */ +.beatport-release-placeholder { + opacity: 0.3; + cursor: default; + pointer-events: none; +} + +.beatport-release-placeholder .placeholder-icon { + font-size: 32px; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: rgba(255, 255, 255, 0.5); +} + +.beatport-release-placeholder:hover { + transform: none; + background: linear-gradient(145deg, + rgba(40, 40, 40, 0.9), + rgba(28, 28, 28, 0.95)); +} + +/* Navigation Buttons */ +.beatport-releases-slider-nav { + position: absolute; + top: 50%; + width: 100%; + display: flex; + justify-content: space-between; + transform: translateY(-50%); + pointer-events: none; + z-index: 10; +} + +.beatport-releases-nav-btn { + pointer-events: all; + background: linear-gradient(135deg, + rgba(29, 185, 84, 0.9), + rgba(30, 215, 96, 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(29, 185, 84, 0.4); + transition: all 0.3s ease; + border: 2px solid rgba(255, 255, 255, 0.2); +} + +.beatport-releases-nav-btn:hover { + transform: scale(1.1); + background: linear-gradient(135deg, + rgba(29, 185, 84, 1), + rgba(30, 215, 96, 0.9)); + box-shadow: 0 8px 25px rgba(29, 185, 84, 0.6); +} + +.beatport-releases-prev-btn { + margin-left: -25px; +} + +.beatport-releases-next-btn { + margin-right: -25px; +} + +/* Slider Indicators */ +.beatport-releases-slider-indicators { + display: flex; + justify-content: center; + gap: 12px; + margin-top: 25px; +} + +.beatport-releases-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-releases-indicator.active { + background: linear-gradient(135deg, #1db954, #1ed760); + border-color: rgba(29, 185, 84, 0.6); + box-shadow: 0 0 15px rgba(29, 185, 84, 0.5); + transform: scale(1.2); +} + +.beatport-releases-indicator:hover { + background: rgba(255, 255, 255, 0.5); + transform: scale(1.1); +} + +/* Responsive Design */ +@media (max-width: 1400px) { + .beatport-releases-grid { + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(2, 1fr); + } +} + +@media (max-width: 1200px) { + .beatport-releases-grid { + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 16px; + } + + .beatport-release-artwork { + width: 70px; + height: 70px; + } +} + +@media (max-width: 900px) { + .beatport-releases-grid { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(3, 1fr); + gap: 14px; + } + + .beatport-releases-slider-container { + padding: 20px; + } +} + +@media (max-width: 600px) { + .beatport-releases-grid { + grid-template-columns: 1fr; + grid-template-rows: repeat(5, 1fr); + gap: 12px; + } + + .beatport-releases-section { + padding: 0 15px; + } + + .beatport-releases-title { + font-size: 24px; + } +}