rebuild beatport section

This commit is contained in:
Broque Thomas 2025-09-29 23:05:18 -07:00
parent 2aaad34ead
commit 6e169056d3
5 changed files with 2117 additions and 5 deletions

View file

@ -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()

View file

@ -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"<h1>❌ An Error Occurred</h1><p>An unexpected error occurred during the authentication process: {e}</p>", 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')

View file

@ -389,6 +389,9 @@
<button class="beatport-tab-button" data-beatport-tab="playlists">
<span class="tab-icon playlist-icon"></span> My Playlists
</button>
<button class="beatport-tab-button" data-beatport-tab="rebuild">
<span class="tab-icon rebuild-icon"></span> Rebuild
</button>
</div>
<!-- Browse Charts Tab Content -->
@ -775,6 +778,67 @@
<div class="playlist-placeholder">Your created Beatport playlists will appear here.</div>
</div>
</div>
<!-- Rebuild Tab Content -->
<div class="beatport-tab-content" id="beatport-rebuild-content">
<div class="beatport-rebuild-slider-container">
<div class="beatport-rebuild-slider" id="beatport-rebuild-slider">
<div class="beatport-rebuild-slider-track" id="beatport-rebuild-slider-track">
<!-- Loading placeholder -->
<div class="beatport-rebuild-loading">
<div class="beatport-rebuild-loading-content">
<h2>🎯 Loading Fresh Beatport Tracks...</h2>
<p>Fetching the latest music from Beatport</p>
</div>
</div>
</div>
<!-- Slider Navigation -->
<div class="beatport-rebuild-slider-nav">
<button class="beatport-rebuild-nav-btn beatport-rebuild-prev-btn" id="beatport-rebuild-prev-btn"></button>
<button class="beatport-rebuild-nav-btn beatport-rebuild-next-btn" id="beatport-rebuild-next-btn"></button>
</div>
<!-- Slider Indicators -->
<div class="beatport-rebuild-slider-indicators">
<!-- Indicators will be dynamically generated -->
</div>
</div>
</div>
<!-- New Releases Grid Slideshow Section -->
<div class="beatport-releases-section">
<div class="beatport-releases-header">
<h2 class="beatport-releases-title">🆕 New Releases</h2>
<p class="beatport-releases-subtitle">Latest albums and EPs from Beatport</p>
</div>
<div class="beatport-releases-slider-container">
<div class="beatport-releases-slider" id="beatport-releases-slider">
<div class="beatport-releases-slider-track" id="beatport-releases-slider-track">
<!-- Loading placeholder -->
<div class="beatport-releases-loading">
<div class="beatport-releases-loading-content">
<h3>📀 Loading New Releases...</h3>
<p>Fetching the latest albums and EPs</p>
</div>
</div>
</div>
<!-- Slider Navigation -->
<div class="beatport-releases-slider-nav">
<button class="beatport-releases-nav-btn beatport-releases-prev-btn" id="beatport-releases-prev-btn"></button>
<button class="beatport-releases-nav-btn beatport-releases-next-btn" id="beatport-releases-next-btn"></button>
</div>
<!-- Slider Indicators -->
<div class="beatport-releases-slider-indicators" id="beatport-releases-slider-indicators">
<!-- Indicators will be dynamically generated -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -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 = `
<div class="beatport-rebuild-slide ${index === 0 ? 'active' : ''}"
data-slide="${index}"
data-url="${track.url}"
data-image="${track.image_url}"
style="--slide-bg-image: url('${track.image_url}')">
<div class="beatport-rebuild-slide-background">
<div class="beatport-rebuild-slide-gradient"></div>
</div>
<div class="beatport-rebuild-slide-content">
<div class="beatport-rebuild-track-info">
<h2 class="beatport-rebuild-track-title">${track.title}</h2>
<p class="beatport-rebuild-artist-name">${track.artist}</p>
<p class="beatport-rebuild-album-name">New on Beatport</p>
</div>
<div class="beatport-rebuild-track-meta">
<span class="beatport-rebuild-genre">${track.genre}</span>
<span class="beatport-rebuild-year">${track.year}</span>
</div>
</div>
</div>
`;
sliderTrack.insertAdjacentHTML('beforeend', slideHtml);
// Add indicator
const indicatorHtml = `<button class="beatport-rebuild-indicator ${index === 0 ? 'active' : ''}" data-slide="${index}"></button>`;
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 += `
<div class="beatport-release-card" data-url="${release.url}" onclick="window.open('${release.url}', '_blank')" style="--card-bg-image: url('${release.image_url}')">
<div class="beatport-release-card-content">
<div class="beatport-release-artwork">
${release.image_url ? `<img src="${release.image_url}" alt="${release.title}" loading="lazy">` : ''}
</div>
<div class="beatport-release-info">
<div class="beatport-release-title" title="${release.title}">${release.title}</div>
<div class="beatport-release-artist" title="${release.artist}">${release.artist}</div>
<div class="beatport-release-label" title="${release.label}">${release.label}</div>
</div>
</div>
</div>
`;
} else {
// Placeholder card
gridHtml += `
<div class="beatport-release-card beatport-release-placeholder">
<div class="beatport-release-card-content">
<div class="beatport-release-artwork">
<div class="placeholder-icon">📀</div>
</div>
<div class="beatport-release-info">
<div class="beatport-release-title">More Releases</div>
<div class="beatport-release-artist">Coming Soon</div>
<div class="beatport-release-label">Beatport</div>
</div>
</div>
</div>
`;
}
}
const slideHtml = `
<div class="beatport-releases-slide ${slideIndex === 0 ? 'active' : ''}"
data-slide="${slideIndex}">
<div class="beatport-releases-grid">
${gridHtml}
</div>
</div>
`;
sliderTrack.innerHTML += slideHtml;
// Create indicator
const indicatorHtml = `<button class="beatport-releases-indicator ${slideIndex === 0 ? 'active' : ''}" data-slide="${slideIndex}"></button>`;
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 = `
<div class="beatport-releases-loading">
<div class="beatport-releases-loading-content">
<h3> Error Loading Releases</h3>
<p>${errorMessage}</p>
</div>
</div>
`;
}
/**
* Clean up releases slider when switching away (copied from hero slider)
*/
function cleanupBeatportReleasesSlider() {
if (beatportReleasesSliderState.autoPlayInterval) {
clearInterval(beatportReleasesSliderState.autoPlayInterval);
beatportReleasesSliderState.autoPlayInterval = null;
}
}

View file

@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23000000"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>');
}
.rebuild-icon {
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23888888"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>');
}
.beatport-tab-button.active .rebuild-icon {
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23000000"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>');
}
.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;
}
}