beatport progress
This commit is contained in:
parent
6aa936103c
commit
7e42e20887
5 changed files with 1906 additions and 6 deletions
|
|
@ -831,7 +831,7 @@ class BeatportUnifiedScraper:
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
def scrape_genre_charts(self, genre: Dict, limit: int = 100) -> List[Dict]:
|
def scrape_genre_charts(self, genre: Dict, limit: int = 100) -> List[Dict]:
|
||||||
"""Scrape charts for a specific genre"""
|
"""Scrape charts for a specific genre (default: top tracks)"""
|
||||||
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
|
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
|
||||||
|
|
||||||
soup = self.get_page(genre_url)
|
soup = self.get_page(genre_url)
|
||||||
|
|
@ -839,6 +839,214 @@ class BeatportUnifiedScraper:
|
||||||
|
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
|
def scrape_genre_top_10(self, genre: Dict) -> List[Dict]:
|
||||||
|
"""Scrape top 10 tracks for a specific genre"""
|
||||||
|
return self.scrape_genre_charts(genre, limit=10)
|
||||||
|
|
||||||
|
def scrape_genre_releases(self, genre: Dict, limit: int = 100) -> List[Dict]:
|
||||||
|
"""Scrape top releases for a specific genre"""
|
||||||
|
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
|
||||||
|
|
||||||
|
soup = self.get_page(genre_url)
|
||||||
|
if not soup:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Try to find releases section on genre page
|
||||||
|
releases = self.extract_releases_from_page(soup, f"{genre['name']} Top Releases", limit)
|
||||||
|
|
||||||
|
# If no releases found with release extraction, try track extraction
|
||||||
|
if not releases:
|
||||||
|
print(f" ⚠️ No releases found with release method, trying track method for {genre['name']}")
|
||||||
|
releases = self.extract_tracks_from_page(soup, f"{genre['name']} Top Releases", limit)
|
||||||
|
# Mark these as releases
|
||||||
|
for release in releases:
|
||||||
|
release['type'] = 'release'
|
||||||
|
|
||||||
|
return releases
|
||||||
|
|
||||||
|
def scrape_genre_staff_picks(self, genre: Dict, limit: int = 50) -> List[Dict]:
|
||||||
|
"""Scrape staff picks for a specific genre"""
|
||||||
|
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
|
||||||
|
|
||||||
|
soup = self.get_page(genre_url)
|
||||||
|
if not soup:
|
||||||
|
return []
|
||||||
|
|
||||||
|
tracks = []
|
||||||
|
|
||||||
|
# Look for staff picks, editorial, or featured sections on genre page
|
||||||
|
staff_sections = [
|
||||||
|
'staff pick', 'editorial', 'featured', 'editor', 'hype pick',
|
||||||
|
'weekend pick', 'best new', 'exclusives'
|
||||||
|
]
|
||||||
|
|
||||||
|
for section_name in staff_sections:
|
||||||
|
# Find section headings that match staff pick patterns
|
||||||
|
section_heading = soup.find(['h1', 'h2', 'h3', 'h4'],
|
||||||
|
string=re.compile(rf'{section_name}', re.I))
|
||||||
|
|
||||||
|
if section_heading:
|
||||||
|
print(f" 📝 Found staff picks section: {section_heading.get_text(strip=True)}")
|
||||||
|
section_container = section_heading.find_parent()
|
||||||
|
if section_container:
|
||||||
|
content_area = section_container.find_next_sibling()
|
||||||
|
if content_area:
|
||||||
|
section_tracks = self.extract_tracks_from_page(
|
||||||
|
content_area, f"{genre['name']} Staff Picks", limit
|
||||||
|
)
|
||||||
|
if section_tracks:
|
||||||
|
tracks.extend(section_tracks)
|
||||||
|
break # Found staff picks, no need to continue
|
||||||
|
|
||||||
|
# If no specific staff picks section found, try to find any editorial content
|
||||||
|
if not tracks:
|
||||||
|
print(f" 🔍 No specific staff picks section found, looking for editorial content...")
|
||||||
|
# Look for DJ charts or featured charts on the genre page
|
||||||
|
chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
|
||||||
|
for chart_link in chart_links[:10]: # Limit to first 10 charts
|
||||||
|
chart_name = chart_link.get_text(strip=True)
|
||||||
|
if chart_name and len(chart_name) > 3:
|
||||||
|
track_info = {
|
||||||
|
'position': len(tracks) + 1,
|
||||||
|
'artist': 'Various Artists',
|
||||||
|
'title': chart_name,
|
||||||
|
'list_name': f"{genre['name']} Staff Picks",
|
||||||
|
'url': urljoin(self.base_url, chart_link.get('href', '')),
|
||||||
|
'chart_type': 'staff_pick'
|
||||||
|
}
|
||||||
|
tracks.append(track_info)
|
||||||
|
if len(tracks) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
def scrape_genre_latest_releases(self, genre: Dict, limit: int = 50) -> List[Dict]:
|
||||||
|
"""Scrape latest releases for a specific genre"""
|
||||||
|
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
|
||||||
|
|
||||||
|
soup = self.get_page(genre_url)
|
||||||
|
if not soup:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Look for latest releases, new releases, or recent sections
|
||||||
|
latest_sections = ['latest', 'new releases', 'recent', 'newest']
|
||||||
|
tracks = []
|
||||||
|
|
||||||
|
for section_name in latest_sections:
|
||||||
|
section_heading = soup.find(['h1', 'h2', 'h3', 'h4'],
|
||||||
|
string=re.compile(rf'{section_name}', re.I))
|
||||||
|
|
||||||
|
if section_heading:
|
||||||
|
print(f" 🕒 Found latest releases section: {section_heading.get_text(strip=True)}")
|
||||||
|
section_container = section_heading.find_parent()
|
||||||
|
if section_container:
|
||||||
|
content_area = section_container.find_next_sibling()
|
||||||
|
if content_area:
|
||||||
|
section_tracks = self.extract_tracks_from_page(
|
||||||
|
content_area, f"Latest {genre['name']} Releases", limit
|
||||||
|
)
|
||||||
|
if section_tracks:
|
||||||
|
tracks.extend(section_tracks)
|
||||||
|
break
|
||||||
|
|
||||||
|
# If no specific latest section found, try releases extraction
|
||||||
|
if not tracks:
|
||||||
|
print(f" 🔍 No specific latest releases section found, trying general releases...")
|
||||||
|
tracks = self.scrape_genre_releases(genre, limit)
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
def scrape_genre_new_charts(self, genre: Dict, limit: int = 50) -> List[Dict]:
|
||||||
|
"""Scrape new charts (DJ/artist curated) for a specific genre"""
|
||||||
|
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
|
||||||
|
|
||||||
|
soup = self.get_page(genre_url)
|
||||||
|
if not soup:
|
||||||
|
return []
|
||||||
|
|
||||||
|
tracks = []
|
||||||
|
|
||||||
|
# Look for DJ charts, artist charts, or curated content on genre page
|
||||||
|
chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
|
||||||
|
|
||||||
|
for chart_link in chart_links[:limit]:
|
||||||
|
chart_name = chart_link.get_text(strip=True)
|
||||||
|
chart_href = chart_link.get('href', '')
|
||||||
|
|
||||||
|
if chart_name and chart_href and len(chart_name) > 3:
|
||||||
|
# Extract additional info if available (artist name, etc.)
|
||||||
|
chart_container = chart_link.find_parent()
|
||||||
|
artist_name = "Various Artists"
|
||||||
|
|
||||||
|
# Try to find artist info near the chart
|
||||||
|
if chart_container:
|
||||||
|
# Look for artist links in the same container
|
||||||
|
artist_link = chart_container.find('a', href=re.compile(r'/artist/'))
|
||||||
|
if artist_link:
|
||||||
|
artist_name = artist_link.get_text(strip=True)
|
||||||
|
|
||||||
|
chart_info = {
|
||||||
|
'position': len(tracks) + 1,
|
||||||
|
'artist': artist_name,
|
||||||
|
'title': chart_name,
|
||||||
|
'list_name': f"New {genre['name']} Charts",
|
||||||
|
'url': urljoin(self.base_url, chart_href),
|
||||||
|
'chart_type': 'new_chart'
|
||||||
|
}
|
||||||
|
tracks.append(chart_info)
|
||||||
|
|
||||||
|
return tracks
|
||||||
|
|
||||||
|
def discover_genre_page_sections(self, genre: Dict) -> Dict:
|
||||||
|
"""Analyze a genre page to discover all available sections"""
|
||||||
|
genre_url = f"{self.base_url}/genre/{genre['slug']}/{genre['id']}"
|
||||||
|
|
||||||
|
print(f"🔍 Discovering sections for {genre['name']} genre page...")
|
||||||
|
|
||||||
|
soup = self.get_page(genre_url)
|
||||||
|
if not soup:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
sections = {
|
||||||
|
'top_tracks': [],
|
||||||
|
'top_releases': [],
|
||||||
|
'staff_picks': [],
|
||||||
|
'latest_releases': [],
|
||||||
|
'new_charts': [],
|
||||||
|
'other_sections': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find all section headings
|
||||||
|
headings = soup.find_all(['h1', 'h2', 'h3', 'h4'])
|
||||||
|
|
||||||
|
for heading in headings:
|
||||||
|
text = heading.get_text(strip=True).lower()
|
||||||
|
|
||||||
|
if any(keyword in text for keyword in ['top 100', 'top 10', 'chart']):
|
||||||
|
sections['top_tracks'].append(heading.get_text(strip=True))
|
||||||
|
elif any(keyword in text for keyword in ['release', 'album', 'ep']):
|
||||||
|
sections['top_releases'].append(heading.get_text(strip=True))
|
||||||
|
elif any(keyword in text for keyword in ['staff', 'editor', 'pick', 'featured']):
|
||||||
|
sections['staff_picks'].append(heading.get_text(strip=True))
|
||||||
|
elif any(keyword in text for keyword in ['latest', 'new', 'recent']):
|
||||||
|
sections['latest_releases'].append(heading.get_text(strip=True))
|
||||||
|
elif 'chart' in text:
|
||||||
|
sections['new_charts'].append(heading.get_text(strip=True))
|
||||||
|
else:
|
||||||
|
sections['other_sections'].append(heading.get_text(strip=True))
|
||||||
|
|
||||||
|
# Count DJ/artist charts
|
||||||
|
chart_links = soup.find_all('a', href=re.compile(r'/chart/'))
|
||||||
|
sections['chart_count'] = len(chart_links)
|
||||||
|
|
||||||
|
print(f"✅ Discovered sections for {genre['name']}:")
|
||||||
|
for section_type, items in sections.items():
|
||||||
|
if items and section_type != 'chart_count':
|
||||||
|
print(f" • {section_type}: {len(items)} sections")
|
||||||
|
print(f" • Individual charts found: {sections['chart_count']}")
|
||||||
|
|
||||||
|
return sections
|
||||||
|
|
||||||
def scrape_all_genres(self, tracks_per_genre: int = 100, max_workers: int = 5, include_images: bool = False) -> Dict[str, List[Dict]]:
|
def scrape_all_genres(self, tracks_per_genre: int = 100, max_workers: int = 5, include_images: bool = False) -> Dict[str, List[Dict]]:
|
||||||
"""Scrape all genres in parallel"""
|
"""Scrape all genres in parallel"""
|
||||||
# Discover genres dynamically if not already done
|
# Discover genres dynamically if not already done
|
||||||
|
|
|
||||||
269
web_server.py
269
web_server.py
|
|
@ -11942,6 +11942,275 @@ def get_beatport_genre_tracks(genre_slug, genre_id):
|
||||||
"count": 0
|
"count": 0
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/top-10', methods=['GET'])
|
||||||
|
def get_beatport_genre_top_10(genre_slug, genre_id):
|
||||||
|
"""Get top 10 tracks for a specific Beatport genre"""
|
||||||
|
try:
|
||||||
|
logger.info(f"🔥 API request for {genre_slug} genre top 10 tracks (ID: {genre_id})")
|
||||||
|
|
||||||
|
# Initialize the Beatport scraper
|
||||||
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
|
# Create genre dict for scraper
|
||||||
|
genre = {
|
||||||
|
'name': genre_slug.replace('-', ' ').title(),
|
||||||
|
'slug': genre_slug,
|
||||||
|
'id': genre_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Scrape top 10 tracks for this genre
|
||||||
|
tracks = scraper.scrape_genre_top_10(genre)
|
||||||
|
|
||||||
|
logger.info(f"✅ Successfully scraped {len(tracks)} top 10 tracks for {genre_slug}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"tracks": tracks,
|
||||||
|
"genre": genre,
|
||||||
|
"count": len(tracks)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error fetching top 10 tracks for {genre_slug}: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"tracks": [],
|
||||||
|
"count": 0
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/releases-top-10', methods=['GET'])
|
||||||
|
def get_beatport_genre_releases_top_10(genre_slug, genre_id):
|
||||||
|
"""Get top 10 releases for a specific Beatport genre"""
|
||||||
|
try:
|
||||||
|
logger.info(f"📊 API request for {genre_slug} genre top 10 releases (ID: {genre_id})")
|
||||||
|
|
||||||
|
# Initialize the Beatport scraper
|
||||||
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
|
# Create genre dict for scraper
|
||||||
|
genre = {
|
||||||
|
'name': genre_slug.replace('-', ' ').title(),
|
||||||
|
'slug': genre_slug,
|
||||||
|
'id': genre_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Scrape top 10 releases for this genre
|
||||||
|
releases = scraper.scrape_genre_releases(genre, limit=10)
|
||||||
|
|
||||||
|
logger.info(f"✅ Successfully scraped {len(releases)} top 10 releases for {genre_slug}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"tracks": releases,
|
||||||
|
"genre": genre,
|
||||||
|
"count": len(releases)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error fetching top 10 releases for {genre_slug}: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"tracks": [],
|
||||||
|
"count": 0
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/releases-top-100', methods=['GET'])
|
||||||
|
def get_beatport_genre_releases_top_100(genre_slug, genre_id):
|
||||||
|
"""Get top 100 releases for a specific Beatport genre"""
|
||||||
|
try:
|
||||||
|
logger.info(f"📊 API request for {genre_slug} genre top 100 releases (ID: {genre_id})")
|
||||||
|
|
||||||
|
# Initialize the Beatport scraper
|
||||||
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
|
# Get query parameters
|
||||||
|
limit = int(request.args.get('limit', '100'))
|
||||||
|
|
||||||
|
# Create genre dict for scraper
|
||||||
|
genre = {
|
||||||
|
'name': genre_slug.replace('-', ' ').title(),
|
||||||
|
'slug': genre_slug,
|
||||||
|
'id': genre_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Scrape top releases for this genre
|
||||||
|
releases = scraper.scrape_genre_releases(genre, limit=limit)
|
||||||
|
|
||||||
|
logger.info(f"✅ Successfully scraped {len(releases)} top 100 releases for {genre_slug}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"tracks": releases,
|
||||||
|
"genre": genre,
|
||||||
|
"count": len(releases)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error fetching top 100 releases for {genre_slug}: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"tracks": [],
|
||||||
|
"count": 0
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/staff-picks', methods=['GET'])
|
||||||
|
def get_beatport_genre_staff_picks(genre_slug, genre_id):
|
||||||
|
"""Get staff picks for a specific Beatport genre"""
|
||||||
|
try:
|
||||||
|
logger.info(f"⭐ API request for {genre_slug} genre staff picks (ID: {genre_id})")
|
||||||
|
|
||||||
|
# Initialize the Beatport scraper
|
||||||
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
|
# Get query parameters
|
||||||
|
limit = int(request.args.get('limit', '50'))
|
||||||
|
|
||||||
|
# Create genre dict for scraper
|
||||||
|
genre = {
|
||||||
|
'name': genre_slug.replace('-', ' ').title(),
|
||||||
|
'slug': genre_slug,
|
||||||
|
'id': genre_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Scrape staff picks for this genre
|
||||||
|
tracks = scraper.scrape_genre_staff_picks(genre, limit=limit)
|
||||||
|
|
||||||
|
logger.info(f"✅ Successfully scraped {len(tracks)} staff picks for {genre_slug}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"tracks": tracks,
|
||||||
|
"genre": genre,
|
||||||
|
"count": len(tracks)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error fetching staff picks for {genre_slug}: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"tracks": [],
|
||||||
|
"count": 0
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/latest-releases', methods=['GET'])
|
||||||
|
def get_beatport_genre_latest_releases(genre_slug, genre_id):
|
||||||
|
"""Get latest releases for a specific Beatport genre"""
|
||||||
|
try:
|
||||||
|
logger.info(f"🕒 API request for {genre_slug} genre latest releases (ID: {genre_id})")
|
||||||
|
|
||||||
|
# Initialize the Beatport scraper
|
||||||
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
|
# Get query parameters
|
||||||
|
limit = int(request.args.get('limit', '50'))
|
||||||
|
|
||||||
|
# Create genre dict for scraper
|
||||||
|
genre = {
|
||||||
|
'name': genre_slug.replace('-', ' ').title(),
|
||||||
|
'slug': genre_slug,
|
||||||
|
'id': genre_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Scrape latest releases for this genre
|
||||||
|
tracks = scraper.scrape_genre_latest_releases(genre, limit=limit)
|
||||||
|
|
||||||
|
logger.info(f"✅ Successfully scraped {len(tracks)} latest releases for {genre_slug}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"tracks": tracks,
|
||||||
|
"genre": genre,
|
||||||
|
"count": len(tracks)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error fetching latest releases for {genre_slug}: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"tracks": [],
|
||||||
|
"count": 0
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/new-charts', methods=['GET'])
|
||||||
|
def get_beatport_genre_new_charts(genre_slug, genre_id):
|
||||||
|
"""Get new charts for a specific Beatport genre"""
|
||||||
|
try:
|
||||||
|
logger.info(f"📈 API request for {genre_slug} genre new charts (ID: {genre_id})")
|
||||||
|
|
||||||
|
# Initialize the Beatport scraper
|
||||||
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
|
# Get query parameters
|
||||||
|
limit = int(request.args.get('limit', '50'))
|
||||||
|
|
||||||
|
# Create genre dict for scraper
|
||||||
|
genre = {
|
||||||
|
'name': genre_slug.replace('-', ' ').title(),
|
||||||
|
'slug': genre_slug,
|
||||||
|
'id': genre_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Scrape new charts for this genre
|
||||||
|
tracks = scraper.scrape_genre_new_charts(genre, limit=limit)
|
||||||
|
|
||||||
|
logger.info(f"✅ Successfully scraped {len(tracks)} new charts for {genre_slug}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"tracks": tracks,
|
||||||
|
"genre": genre,
|
||||||
|
"count": len(tracks)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error fetching new charts for {genre_slug}: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"tracks": [],
|
||||||
|
"count": 0
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/sections', methods=['GET'])
|
||||||
|
def get_beatport_genre_sections(genre_slug, genre_id):
|
||||||
|
"""Discover all available sections for a specific Beatport genre"""
|
||||||
|
try:
|
||||||
|
logger.info(f"🔍 API request for {genre_slug} genre sections discovery (ID: {genre_id})")
|
||||||
|
|
||||||
|
# Initialize the Beatport scraper
|
||||||
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
|
# Create genre dict for scraper
|
||||||
|
genre = {
|
||||||
|
'name': genre_slug.replace('-', ' ').title(),
|
||||||
|
'slug': genre_slug,
|
||||||
|
'id': genre_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# Discover sections for this genre
|
||||||
|
sections = scraper.discover_genre_page_sections(genre)
|
||||||
|
|
||||||
|
logger.info(f"✅ Successfully discovered sections for {genre_slug}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"sections": sections,
|
||||||
|
"genre": genre
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error discovering sections for {genre_slug}: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"sections": {}
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/beatport/top-100', methods=['GET'])
|
@app.route('/api/beatport/top-100', methods=['GET'])
|
||||||
def get_beatport_top_100():
|
def get_beatport_top_100():
|
||||||
"""Get Beatport Top 100 tracks"""
|
"""Get Beatport Top 100 tracks"""
|
||||||
|
|
|
||||||
122
webui/index.html
122
webui/index.html
|
|
@ -539,6 +539,128 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Genre Detail Sub-View -->
|
||||||
|
<div class="beatport-sub-view" id="beatport-genre-detail-view">
|
||||||
|
<div class="beatport-breadcrumb">
|
||||||
|
<button class="breadcrumb-back" id="genre-detail-back">← Back to Genre Explorer</button>
|
||||||
|
<span class="breadcrumb-path" id="genre-detail-breadcrumb">Browse Charts > Genre Explorer > Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="genre-detail-header">
|
||||||
|
<div class="genre-detail-info">
|
||||||
|
<h2 id="genre-detail-title">Loading Genre...</h2>
|
||||||
|
<p id="genre-detail-description">Explore all chart types for this genre</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Main Chart Types Section -->
|
||||||
|
<div class="genre-main-charts-section">
|
||||||
|
<h3 class="section-title">📊 Main Charts</h3>
|
||||||
|
<div class="genre-chart-types-grid">
|
||||||
|
<div class="genre-chart-type-card" data-chart-type="top-10">
|
||||||
|
<div class="chart-type-icon">🔥</div>
|
||||||
|
<div class="chart-type-info">
|
||||||
|
<h3 id="genre-top-10-title">Top 10</h3>
|
||||||
|
<p>Current hottest tracks</p>
|
||||||
|
<span class="track-count">10 tracks</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="genre-chart-type-card" data-chart-type="top-100">
|
||||||
|
<div class="chart-type-icon">💯</div>
|
||||||
|
<div class="chart-type-info">
|
||||||
|
<h3 id="genre-top-100-title">Top 100</h3>
|
||||||
|
<p>Complete chart rankings</p>
|
||||||
|
<span class="track-count">100 tracks</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Releases Section -->
|
||||||
|
<div class="genre-releases-section">
|
||||||
|
<h3 class="section-title">🎵 Releases</h3>
|
||||||
|
<div class="genre-chart-types-grid">
|
||||||
|
<div class="genre-chart-type-card" data-chart-type="releases-top-10">
|
||||||
|
<div class="chart-type-icon">🆕</div>
|
||||||
|
<div class="chart-type-info">
|
||||||
|
<h3 id="genre-releases-top-10-title">Top 10 Releases</h3>
|
||||||
|
<p>Newest releases trending</p>
|
||||||
|
<span class="track-count">10 releases</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="genre-chart-type-card" data-chart-type="releases-top-100">
|
||||||
|
<div class="chart-type-icon">📊</div>
|
||||||
|
<div class="chart-type-info">
|
||||||
|
<h3 id="genre-releases-top-100-title">Top 100 Releases</h3>
|
||||||
|
<p>All trending releases</p>
|
||||||
|
<span class="track-count">100 releases</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="genre-chart-type-card" data-chart-type="latest-releases">
|
||||||
|
<div class="chart-type-icon">🕒</div>
|
||||||
|
<div class="chart-type-info">
|
||||||
|
<h3 id="genre-latest-releases-title">Latest Releases</h3>
|
||||||
|
<p>Recently published</p>
|
||||||
|
<span class="track-count">~ releases</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editorial Section -->
|
||||||
|
<div class="genre-editorial-section">
|
||||||
|
<h3 class="section-title">⭐ Editorial</h3>
|
||||||
|
<div class="genre-chart-types-grid">
|
||||||
|
<div class="genre-chart-type-card" data-chart-type="staff-picks">
|
||||||
|
<div class="chart-type-icon">⭐</div>
|
||||||
|
<div class="chart-type-info">
|
||||||
|
<h3 id="genre-staff-picks-title">Staff Picks</h3>
|
||||||
|
<p>Editor curated selection</p>
|
||||||
|
<span class="track-count">~ tracks</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Charts Section (Always Visible) -->
|
||||||
|
<div class="genre-new-charts-section">
|
||||||
|
<h3 class="section-title">📈 New Charts Collection</h3>
|
||||||
|
<p class="section-description">Artist and DJ curated chart collections</p>
|
||||||
|
|
||||||
|
<!-- Always Visible Charts List -->
|
||||||
|
<div class="new-charts-content" id="new-charts-content">
|
||||||
|
<div class="charts-loading-inline" id="charts-loading-inline">
|
||||||
|
<div class="loading-spinner-small"></div>
|
||||||
|
<p>Loading chart collections...</p>
|
||||||
|
</div>
|
||||||
|
<div class="new-charts-grid" id="new-charts-grid">
|
||||||
|
<!-- Charts will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Genre Charts List Sub-View -->
|
||||||
|
<div class="beatport-sub-view" id="beatport-genre-charts-list-view">
|
||||||
|
<div class="beatport-breadcrumb">
|
||||||
|
<button class="breadcrumb-back" id="genre-charts-list-back">← Back to Genre Charts</button>
|
||||||
|
<span class="breadcrumb-path" id="genre-charts-list-breadcrumb">Browse Charts > Genre Explorer > Genre Charts > New Charts</span>
|
||||||
|
</div>
|
||||||
|
<div class="genre-charts-list-header">
|
||||||
|
<div class="genre-charts-list-info">
|
||||||
|
<h2 id="genre-charts-list-title">Loading Charts...</h2>
|
||||||
|
<p id="genre-charts-list-description">Browse all available chart collections for this genre</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="genre-charts-list-container">
|
||||||
|
<div class="charts-loading-placeholder" id="charts-loading-placeholder">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<p>🔍 Loading chart collections...</p>
|
||||||
|
</div>
|
||||||
|
<div class="genre-charts-grid" id="genre-charts-grid">
|
||||||
|
<!-- Charts will be populated dynamically -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Staff Picks Sub-View -->
|
<!-- Staff Picks Sub-View -->
|
||||||
<div class="beatport-sub-view" id="beatport-staff-picks-view">
|
<div class="beatport-sub-view" id="beatport-staff-picks-view">
|
||||||
<div class="beatport-breadcrumb">
|
<div class="beatport-breadcrumb">
|
||||||
|
|
|
||||||
|
|
@ -9746,7 +9746,14 @@ function initializeSyncPage() {
|
||||||
const beatportBackButtons = document.querySelectorAll('.breadcrumb-back');
|
const beatportBackButtons = document.querySelectorAll('.breadcrumb-back');
|
||||||
beatportBackButtons.forEach(button => {
|
beatportBackButtons.forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
showBeatportMainView();
|
// Handle different back button types
|
||||||
|
if (button.id === 'genre-detail-back') {
|
||||||
|
showBeatportGenresView();
|
||||||
|
} else if (button.id === 'genre-charts-list-back') {
|
||||||
|
showBeatportGenreDetailViewFromBack();
|
||||||
|
} else {
|
||||||
|
showBeatportMainView();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -11345,11 +11352,624 @@ async function handleBeatportChartClick(chartType, chartId, chartName, chartEndp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBeatportGenreClick(genreSlug, genreId) {
|
function handleBeatportGenreClick(genreSlug, genreId, genreName) {
|
||||||
console.log(`🎵 Beatport genre clicked: ${genreSlug} (${genreId})`);
|
console.log(`🎵 Beatport genre clicked: ${genreName} (${genreSlug}/${genreId}) - SHOWING GENRE DETAIL VIEW`);
|
||||||
|
console.log(`📝 Debug: Parameters received - Slug: ${genreSlug}, ID: ${genreId}, Name: ${genreName}`);
|
||||||
|
|
||||||
// Placeholder for Phase 2 - will open discovery modal
|
// Navigate to genre detail view with proper parameters
|
||||||
showToast(`🎵 Genre "${genreSlug}" selected - Discovery modal coming in Phase 2!`, 'info');
|
showBeatportGenreDetailView(genreSlug, genreId, genreName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBeatportGenreDetailView(genreSlug, genreId, genreName) {
|
||||||
|
console.log(`🎯 Showing genre detail view for: ${genreName}`);
|
||||||
|
console.log(`📝 Debug: Function called with - Slug: ${genreSlug}, ID: ${genreId}, Name: ${genreName}`);
|
||||||
|
|
||||||
|
// Hide all other beatport views
|
||||||
|
document.querySelectorAll('.beatport-sub-view').forEach(view => {
|
||||||
|
view.classList.remove('active');
|
||||||
|
});
|
||||||
|
const mainView = document.getElementById('beatport-main-view');
|
||||||
|
if (mainView) {
|
||||||
|
mainView.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show genre detail view
|
||||||
|
const genreDetailView = document.getElementById('beatport-genre-detail-view');
|
||||||
|
if (genreDetailView) {
|
||||||
|
genreDetailView.classList.add('active');
|
||||||
|
console.log(`📝 Debug: Genre detail view element found and activated`);
|
||||||
|
|
||||||
|
// Update view content
|
||||||
|
const titleElement = document.getElementById('genre-detail-title');
|
||||||
|
const breadcrumbElement = document.getElementById('genre-detail-breadcrumb');
|
||||||
|
|
||||||
|
console.log(`📝 Debug: Title element found: ${!!titleElement}, Breadcrumb element found: ${!!breadcrumbElement}`);
|
||||||
|
|
||||||
|
if (titleElement) {
|
||||||
|
titleElement.textContent = genreName;
|
||||||
|
console.log(`📝 Debug: Updated title to: ${genreName}`);
|
||||||
|
}
|
||||||
|
if (breadcrumbElement) {
|
||||||
|
breadcrumbElement.textContent = `Browse Charts > Genre Explorer > ${genreName} Charts`;
|
||||||
|
console.log(`📝 Debug: Updated breadcrumb`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update chart type titles with genre name
|
||||||
|
const chartTitles = [
|
||||||
|
'genre-top-10-title',
|
||||||
|
'genre-top-100-title',
|
||||||
|
'genre-releases-top-10-title',
|
||||||
|
'genre-releases-top-100-title',
|
||||||
|
'genre-staff-picks-title',
|
||||||
|
'genre-latest-releases-title',
|
||||||
|
'genre-new-charts-title'
|
||||||
|
];
|
||||||
|
|
||||||
|
chartTitles.forEach(titleId => {
|
||||||
|
const element = document.getElementById(titleId);
|
||||||
|
if (element) {
|
||||||
|
console.log(`📝 Debug: Found chart title element: ${titleId}`);
|
||||||
|
} else {
|
||||||
|
console.log(`📝 Debug: Missing chart title element: ${titleId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('genre-top-10-title').textContent = `Top 10 ${genreName}`;
|
||||||
|
document.getElementById('genre-top-100-title').textContent = `Top 100 ${genreName}`;
|
||||||
|
document.getElementById('genre-releases-top-10-title').textContent = `Top 10 ${genreName} Releases`;
|
||||||
|
document.getElementById('genre-releases-top-100-title').textContent = `Top 100 ${genreName} Releases`;
|
||||||
|
document.getElementById('genre-staff-picks-title').textContent = `${genreName} Staff Picks`;
|
||||||
|
document.getElementById('genre-latest-releases-title').textContent = `Latest ${genreName} Releases`;
|
||||||
|
|
||||||
|
// Load new charts directly (no expansion needed)
|
||||||
|
console.log(`🔄 Auto-loading new charts for ${genreName}...`);
|
||||||
|
loadNewChartsInline(genreSlug, genreId, genreName);
|
||||||
|
|
||||||
|
// Store current genre data for chart type handlers
|
||||||
|
genreDetailView.dataset.genreSlug = genreSlug;
|
||||||
|
genreDetailView.dataset.genreId = genreId;
|
||||||
|
genreDetailView.dataset.genreName = genreName;
|
||||||
|
|
||||||
|
// Add click handlers to chart type cards
|
||||||
|
setupGenreChartTypeHandlers();
|
||||||
|
|
||||||
|
console.log(`✅ Genre detail view shown for ${genreName}`);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Genre detail view element not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupGenreChartTypeHandlers() {
|
||||||
|
const chartTypeCards = document.querySelectorAll('#beatport-genre-detail-view .genre-chart-type-card');
|
||||||
|
|
||||||
|
chartTypeCards.forEach(card => {
|
||||||
|
// Remove existing listeners
|
||||||
|
card.replaceWith(card.cloneNode(true));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-select after cloning
|
||||||
|
const newChartTypeCards = document.querySelectorAll('#beatport-genre-detail-view .genre-chart-type-card');
|
||||||
|
|
||||||
|
newChartTypeCards.forEach(card => {
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
const chartType = card.dataset.chartType;
|
||||||
|
const genreDetailView = document.getElementById('beatport-genre-detail-view');
|
||||||
|
const genreSlug = genreDetailView.dataset.genreSlug;
|
||||||
|
const genreId = genreDetailView.dataset.genreId;
|
||||||
|
const genreName = genreDetailView.dataset.genreName;
|
||||||
|
|
||||||
|
// All chart types now go directly to discovery modal
|
||||||
|
handleGenreChartTypeClick(genreSlug, genreId, genreName, chartType);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBeatportGenresView() {
|
||||||
|
// Hide genre detail view and show genres view
|
||||||
|
document.querySelectorAll('.beatport-sub-view').forEach(view => {
|
||||||
|
view.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
const genresView = document.getElementById('beatport-genres-view');
|
||||||
|
if (genresView) {
|
||||||
|
genresView.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleNewChartsExpansion(genreSlug, genreId, genreName) {
|
||||||
|
console.log(`📈 Toggling new charts expansion for: ${genreName}`);
|
||||||
|
|
||||||
|
const expandedContent = document.getElementById('new-charts-expanded');
|
||||||
|
const expandIndicator = document.getElementById('expand-indicator');
|
||||||
|
const chartsCount = document.getElementById('new-charts-count');
|
||||||
|
|
||||||
|
if (!expandedContent || !expandIndicator) {
|
||||||
|
console.error('❌ New charts expansion elements not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already expanded
|
||||||
|
const isExpanded = expandedContent.style.display !== 'none';
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
// Collapse
|
||||||
|
expandedContent.style.display = 'none';
|
||||||
|
expandIndicator.classList.remove('expanded');
|
||||||
|
console.log('📉 Collapsed new charts section');
|
||||||
|
} else {
|
||||||
|
// Expand and load charts
|
||||||
|
expandedContent.style.display = 'block';
|
||||||
|
expandIndicator.classList.add('expanded');
|
||||||
|
|
||||||
|
// Load charts if not already loaded
|
||||||
|
await loadNewChartsInline(genreSlug, genreId, genreName);
|
||||||
|
console.log('📈 Expanded new charts section');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadNewChartsInline(genreSlug, genreId, genreName) {
|
||||||
|
const chartsGrid = document.getElementById('new-charts-grid');
|
||||||
|
const loadingInline = document.getElementById('charts-loading-inline');
|
||||||
|
|
||||||
|
if (!chartsGrid || !loadingInline) {
|
||||||
|
console.error('❌ Inline charts elements not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
loadingInline.style.display = 'block';
|
||||||
|
chartsGrid.style.display = 'none';
|
||||||
|
chartsGrid.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`🔍 Loading inline charts for ${genreName}...`);
|
||||||
|
|
||||||
|
// Fetch charts from the new-charts endpoint
|
||||||
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/new-charts?limit=20`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch charts: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||||
|
// Show empty state
|
||||||
|
chartsGrid.innerHTML = `
|
||||||
|
<div class="new-charts-empty">
|
||||||
|
<h4>No Charts Available</h4>
|
||||||
|
<p>No curated charts found for ${genreName} at the moment.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Populate charts grid
|
||||||
|
const chartsHTML = data.tracks.map((chart, index) => {
|
||||||
|
const chartName = chart.title || 'Untitled Chart';
|
||||||
|
const artistName = chart.artist || 'Various Artists';
|
||||||
|
const chartUrl = chart.url || '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="new-chart-item" data-chart-url="${chartUrl}" data-chart-name="${chartName}" data-chart-artist="${artistName}">
|
||||||
|
<div class="new-chart-header">
|
||||||
|
<div class="new-chart-icon">📈</div>
|
||||||
|
<div class="new-chart-title">
|
||||||
|
<h5>${chartName}</h5>
|
||||||
|
<p class="new-chart-artist">by ${artistName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="new-chart-description">
|
||||||
|
Curated ${genreName} chart collection
|
||||||
|
</div>
|
||||||
|
<div class="new-chart-footer">
|
||||||
|
<div class="new-chart-type">Chart</div>
|
||||||
|
<div class="new-chart-action">Explore →</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
chartsGrid.innerHTML = chartsHTML;
|
||||||
|
|
||||||
|
// Add click handlers to chart items
|
||||||
|
setupNewChartItemHandlers(genreSlug, genreId, genreName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide loading and show grid
|
||||||
|
loadingInline.style.display = 'none';
|
||||||
|
chartsGrid.style.display = 'grid';
|
||||||
|
|
||||||
|
console.log(`✅ Loaded ${data.tracks?.length || 0} inline charts for ${genreName}`);
|
||||||
|
showToast(`Found ${data.tracks?.length || 0} chart collections`, 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error loading inline charts for ${genreName}:`, error);
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
chartsGrid.innerHTML = `
|
||||||
|
<div class="new-charts-empty">
|
||||||
|
<h4>Error Loading Charts</h4>
|
||||||
|
<p>Unable to load chart collections for ${genreName}.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
loadingInline.style.display = 'none';
|
||||||
|
chartsGrid.style.display = 'grid';
|
||||||
|
|
||||||
|
showToast(`Error loading charts: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupNewChartItemHandlers(genreSlug, genreId, genreName) {
|
||||||
|
const chartItems = document.querySelectorAll('#new-charts-grid .new-chart-item');
|
||||||
|
|
||||||
|
chartItems.forEach(item => {
|
||||||
|
item.addEventListener('click', async () => {
|
||||||
|
const chartName = item.dataset.chartName;
|
||||||
|
const chartArtist = item.dataset.chartArtist;
|
||||||
|
const chartUrl = item.dataset.chartUrl;
|
||||||
|
|
||||||
|
console.log(`🎵 Chart clicked: ${chartName} by ${chartArtist}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a virtual chart data object
|
||||||
|
const chartHash = `individual_chart_${genreSlug}_${Date.now()}`;
|
||||||
|
const fullChartName = `${chartName} (${genreName})`;
|
||||||
|
|
||||||
|
showToast(`Loading ${chartName}...`, 'info');
|
||||||
|
|
||||||
|
// For demonstration, we'll use the genre tracks as chart content
|
||||||
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/tracks?limit=20`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch chart content: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||||
|
throw new Error(`No tracks found in chart`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chart data object for playlist card
|
||||||
|
const chartData = {
|
||||||
|
hash: chartHash,
|
||||||
|
name: fullChartName,
|
||||||
|
chart_type: 'individual_chart',
|
||||||
|
track_count: data.tracks.length,
|
||||||
|
tracks: data.tracks.map(track => ({
|
||||||
|
name: track.title || 'Unknown Title',
|
||||||
|
artists: [track.artist || 'Unknown Artist'],
|
||||||
|
album: fullChartName,
|
||||||
|
duration_ms: 0,
|
||||||
|
external_urls: { beatport: track.url || chartUrl },
|
||||||
|
source: 'beatport'
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add card to container (in background, like YouTube does)
|
||||||
|
console.log(`🃏 Creating Beatport playlist card for: ${fullChartName}`);
|
||||||
|
addBeatportCardToContainer(chartData);
|
||||||
|
|
||||||
|
// Automatically open discovery modal
|
||||||
|
handleBeatportCardClick(chartHash);
|
||||||
|
|
||||||
|
console.log(`✅ Created Beatport card and opened discovery modal for ${fullChartName}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error loading chart: ${error.message}`);
|
||||||
|
showToast(`Error loading chart: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBeatportGenreDetailViewFromBack() {
|
||||||
|
// Show genre detail view (used by charts list back button)
|
||||||
|
document.querySelectorAll('.beatport-sub-view').forEach(view => {
|
||||||
|
view.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
const genreDetailView = document.getElementById('beatport-genre-detail-view');
|
||||||
|
if (genreDetailView) {
|
||||||
|
genreDetailView.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showBeatportGenreChartsListView(genreSlug, genreId, genreName) {
|
||||||
|
console.log(`📈 Showing charts list for: ${genreName}`);
|
||||||
|
|
||||||
|
// Hide all other beatport views
|
||||||
|
document.querySelectorAll('.beatport-sub-view').forEach(view => {
|
||||||
|
view.classList.remove('active');
|
||||||
|
});
|
||||||
|
const mainView = document.getElementById('beatport-main-view');
|
||||||
|
if (mainView) {
|
||||||
|
mainView.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show charts list view
|
||||||
|
const chartsListView = document.getElementById('beatport-genre-charts-list-view');
|
||||||
|
if (chartsListView) {
|
||||||
|
chartsListView.classList.add('active');
|
||||||
|
|
||||||
|
// Update view content
|
||||||
|
document.getElementById('genre-charts-list-title').textContent = `New ${genreName} Charts`;
|
||||||
|
document.getElementById('genre-charts-list-breadcrumb').textContent = `Browse Charts > Genre Explorer > ${genreName} Charts > New Charts`;
|
||||||
|
|
||||||
|
// Store current genre data for individual chart handlers
|
||||||
|
chartsListView.dataset.genreSlug = genreSlug;
|
||||||
|
chartsListView.dataset.genreId = genreId;
|
||||||
|
chartsListView.dataset.genreName = genreName;
|
||||||
|
|
||||||
|
// Load charts for this genre
|
||||||
|
await loadGenreChartsList(genreSlug, genreId, genreName);
|
||||||
|
|
||||||
|
console.log(`✅ Charts list view shown for ${genreName}`);
|
||||||
|
} else {
|
||||||
|
console.error('❌ Charts list view element not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGenreChartsList(genreSlug, genreId, genreName) {
|
||||||
|
const chartsGrid = document.getElementById('genre-charts-grid');
|
||||||
|
const loadingPlaceholder = document.getElementById('charts-loading-placeholder');
|
||||||
|
|
||||||
|
if (!chartsGrid || !loadingPlaceholder) {
|
||||||
|
console.error('❌ Charts grid or loading placeholder not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
loadingPlaceholder.style.display = 'block';
|
||||||
|
chartsGrid.style.display = 'none';
|
||||||
|
chartsGrid.innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`🔍 Loading charts for ${genreName}...`);
|
||||||
|
|
||||||
|
// Fetch charts from the new-charts endpoint
|
||||||
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/new-charts?limit=50`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch charts: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||||
|
// Show empty state
|
||||||
|
chartsGrid.innerHTML = `
|
||||||
|
<div class="genre-charts-empty">
|
||||||
|
<h3>No Charts Available</h3>
|
||||||
|
<p>No curated charts found for ${genreName} at the moment.<br>Check back later for new DJ and artist chart collections.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Populate charts grid
|
||||||
|
const chartsHTML = data.tracks.map((chart, index) => {
|
||||||
|
const chartName = chart.title || 'Untitled Chart';
|
||||||
|
const artistName = chart.artist || 'Various Artists';
|
||||||
|
const chartUrl = chart.url || '';
|
||||||
|
|
||||||
|
// Extract chart ID from URL for click handling
|
||||||
|
const chartId = chartUrl.split('/').pop() || `chart_${index}`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="genre-chart-item" data-chart-url="${chartUrl}" data-chart-name="${chartName}" data-chart-artist="${artistName}">
|
||||||
|
<div class="chart-item-header">
|
||||||
|
<div class="chart-item-icon">📈</div>
|
||||||
|
<div class="chart-item-title">
|
||||||
|
<h4>${chartName}</h4>
|
||||||
|
<p class="chart-item-artist">by ${artistName}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-item-description">
|
||||||
|
Curated chart collection featuring ${genreName} tracks
|
||||||
|
</div>
|
||||||
|
<div class="chart-item-footer">
|
||||||
|
<div class="chart-item-type">Chart</div>
|
||||||
|
<div class="chart-item-action">Click to explore →</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
chartsGrid.innerHTML = chartsHTML;
|
||||||
|
|
||||||
|
// Add click handlers to chart items
|
||||||
|
setupGenreChartItemHandlers(genreSlug, genreId, genreName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide loading and show grid
|
||||||
|
loadingPlaceholder.style.display = 'none';
|
||||||
|
chartsGrid.style.display = 'grid';
|
||||||
|
|
||||||
|
console.log(`✅ Loaded ${data.tracks?.length || 0} charts for ${genreName}`);
|
||||||
|
showToast(`Found ${data.tracks?.length || 0} chart collections`, 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error loading charts for ${genreName}:`, error);
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
chartsGrid.innerHTML = `
|
||||||
|
<div class="genre-charts-empty">
|
||||||
|
<h3>Error Loading Charts</h3>
|
||||||
|
<p>Unable to load chart collections for ${genreName}.<br>Please try again later.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
loadingPlaceholder.style.display = 'none';
|
||||||
|
chartsGrid.style.display = 'grid';
|
||||||
|
|
||||||
|
showToast(`Error loading charts: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupGenreChartItemHandlers(genreSlug, genreId, genreName) {
|
||||||
|
const chartItems = document.querySelectorAll('#genre-charts-grid .genre-chart-item');
|
||||||
|
|
||||||
|
chartItems.forEach(item => {
|
||||||
|
item.addEventListener('click', async () => {
|
||||||
|
const chartName = item.dataset.chartName;
|
||||||
|
const chartArtist = item.dataset.chartArtist;
|
||||||
|
const chartUrl = item.dataset.chartUrl;
|
||||||
|
|
||||||
|
console.log(`🎵 Chart clicked: ${chartName} by ${chartArtist}`);
|
||||||
|
|
||||||
|
// For now, we'll create a virtual playlist from this chart
|
||||||
|
// This would eventually fetch the actual chart contents from Beatport
|
||||||
|
try {
|
||||||
|
// Create a virtual chart data object
|
||||||
|
const chartHash = `individual_chart_${genreSlug}_${Date.now()}`;
|
||||||
|
const fullChartName = `${chartName} (${genreName})`;
|
||||||
|
|
||||||
|
showToast(`Loading ${chartName}...`, 'info');
|
||||||
|
|
||||||
|
// For demonstration, we'll use the genre tracks as chart content
|
||||||
|
// In a real implementation, this would fetch the specific chart tracks
|
||||||
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/tracks?limit=20`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch chart content: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||||
|
throw new Error(`No tracks found in chart`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chart data object for playlist card
|
||||||
|
const chartData = {
|
||||||
|
hash: chartHash,
|
||||||
|
name: fullChartName,
|
||||||
|
chart_type: 'individual_chart',
|
||||||
|
track_count: data.tracks.length,
|
||||||
|
tracks: data.tracks.map(track => ({
|
||||||
|
name: track.title || 'Unknown Title',
|
||||||
|
artists: [track.artist || 'Unknown Artist'],
|
||||||
|
album: fullChartName,
|
||||||
|
duration_ms: 0,
|
||||||
|
external_urls: { beatport: track.url || chartUrl },
|
||||||
|
source: 'beatport'
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add card to container (in background, like YouTube does)
|
||||||
|
console.log(`🃏 Creating Beatport playlist card for: ${fullChartName}`);
|
||||||
|
addBeatportCardToContainer(chartData);
|
||||||
|
|
||||||
|
// Automatically open discovery modal
|
||||||
|
handleBeatportCardClick(chartHash);
|
||||||
|
|
||||||
|
console.log(`✅ Created Beatport card and opened discovery modal for ${fullChartName}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error loading chart: ${error.message}`);
|
||||||
|
showToast(`Error loading chart: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGenreChartTypeClick(genreSlug, genreId, genreName, chartType) {
|
||||||
|
console.log(`🎯 Genre chart type clicked: ${chartType} for ${genreName} (${genreSlug}/${genreId})`);
|
||||||
|
|
||||||
|
// Map chart types to API endpoints and create descriptive names
|
||||||
|
const chartTypeMap = {
|
||||||
|
'top-10': {
|
||||||
|
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/top-10`,
|
||||||
|
name: `Top 10 ${genreName}`,
|
||||||
|
limit: 10
|
||||||
|
},
|
||||||
|
'top-100': {
|
||||||
|
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/tracks`,
|
||||||
|
name: `Top 100 ${genreName}`,
|
||||||
|
limit: 100
|
||||||
|
},
|
||||||
|
'releases-top-10': {
|
||||||
|
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/releases-top-10`,
|
||||||
|
name: `Top 10 ${genreName} Releases`,
|
||||||
|
limit: 10
|
||||||
|
},
|
||||||
|
'releases-top-100': {
|
||||||
|
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/releases-top-100`,
|
||||||
|
name: `Top 100 ${genreName} Releases`,
|
||||||
|
limit: 100
|
||||||
|
},
|
||||||
|
'staff-picks': {
|
||||||
|
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/staff-picks`,
|
||||||
|
name: `${genreName} Staff Picks`,
|
||||||
|
limit: 50
|
||||||
|
},
|
||||||
|
'latest-releases': {
|
||||||
|
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/latest-releases`,
|
||||||
|
name: `Latest ${genreName} Releases`,
|
||||||
|
limit: 50
|
||||||
|
},
|
||||||
|
'new-charts': {
|
||||||
|
endpoint: `/api/beatport/genre/${genreSlug}/${genreId}/new-charts`,
|
||||||
|
name: `New ${genreName} Charts`,
|
||||||
|
limit: 50
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartConfig = chartTypeMap[chartType];
|
||||||
|
if (!chartConfig) {
|
||||||
|
console.error(`❌ Unknown chart type: ${chartType}`);
|
||||||
|
showToast(`Unknown chart type: ${chartType}`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if we already have a card for this specific chart type
|
||||||
|
const existingState = Object.values(beatportChartStates).find(state =>
|
||||||
|
state.chart && state.chart.name === chartConfig.name && state.chart.chart_type === `genre_${chartType}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingState) {
|
||||||
|
console.log(`🔄 Found existing Beatport card for ${chartConfig.name}, opening existing modal`);
|
||||||
|
handleBeatportCardClick(existingState.chart.hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a chart hash for state management
|
||||||
|
const chartHash = `genre_${chartType}_${genreSlug}_${genreId}_${Date.now()}`;
|
||||||
|
|
||||||
|
showToast(`Loading ${chartConfig.name}...`, 'info');
|
||||||
|
|
||||||
|
// Fetch tracks from the specific endpoint
|
||||||
|
const response = await fetch(`${chartConfig.endpoint}?limit=${chartConfig.limit}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch ${chartConfig.name}: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success || !data.tracks || data.tracks.length === 0) {
|
||||||
|
throw new Error(`No tracks found in ${chartConfig.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create chart data object for playlist card
|
||||||
|
const chartData = {
|
||||||
|
hash: chartHash,
|
||||||
|
name: chartConfig.name,
|
||||||
|
chart_type: `genre_${chartType}`,
|
||||||
|
track_count: data.tracks.length,
|
||||||
|
tracks: data.tracks.map(track => ({
|
||||||
|
name: track.title || 'Unknown Title',
|
||||||
|
artists: [track.artist || 'Unknown Artist'],
|
||||||
|
album: chartConfig.name,
|
||||||
|
duration_ms: 0,
|
||||||
|
external_urls: { beatport: track.url || '' },
|
||||||
|
source: 'beatport'
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add card to container (in background, like YouTube does)
|
||||||
|
console.log(`🃏 Creating Beatport playlist card for: ${chartConfig.name}`);
|
||||||
|
addBeatportCardToContainer(chartData);
|
||||||
|
|
||||||
|
// Automatically open discovery modal (like when you click a YouTube or Tidal card in fresh state)
|
||||||
|
handleBeatportCardClick(chartHash);
|
||||||
|
|
||||||
|
console.log(`✅ Created Beatport card and opened discovery modal for ${chartConfig.name}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error loading ${chartConfig.name}:`, error);
|
||||||
|
showToast(`Error loading ${chartConfig.name}: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
|
||||||
|
|
@ -4862,6 +4862,622 @@ body {
|
||||||
0 0 20px rgba(1, 255, 149, 0.3);
|
0 0 20px rgba(1, 255, 149, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================= */
|
||||||
|
/* BEATPORT GENRE DETAIL VIEW */
|
||||||
|
/* ================================= */
|
||||||
|
|
||||||
|
.genre-detail-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-detail-info h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
background: linear-gradient(135deg, #01ff95, #00d4ff);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-detail-info p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section Styling */
|
||||||
|
.genre-main-charts-section,
|
||||||
|
.genre-releases-section,
|
||||||
|
.genre-editorial-section,
|
||||||
|
.genre-new-charts-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(20, 20, 20, 0.8) 0%,
|
||||||
|
rgba(15, 15, 15, 0.9) 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Types Grid */
|
||||||
|
.genre-chart-types-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Type Cards */
|
||||||
|
.genre-chart-type-card {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(25, 25, 25, 0.95) 0%,
|
||||||
|
rgba(15, 15, 15, 0.98) 100%);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-type-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(1, 255, 149, 0.1) 0%,
|
||||||
|
transparent 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-type-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
border-color: rgba(1, 255, 149, 0.3);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 25px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 15px rgba(1, 255, 149, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-type-card:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-type-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
min-width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(1, 255, 149, 0.2) 0%,
|
||||||
|
rgba(0, 212, 255, 0.2) 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(1, 255, 149, 0.3);
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-type-info {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-type-info h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-type-info p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(1, 255, 149, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
background: rgba(1, 255, 149, 0.1);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Special Chart Card (New Charts) */
|
||||||
|
.genre-new-charts-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-type-card.special-chart {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(30, 20, 40, 0.95) 0%,
|
||||||
|
rgba(20, 15, 30, 0.98) 100%);
|
||||||
|
border: 2px solid rgba(138, 43, 226, 0.3);
|
||||||
|
max-width: 400px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-type-card.special-chart::before {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(138, 43, 226, 0.15) 0%,
|
||||||
|
transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-type-card.special-chart:hover {
|
||||||
|
border-color: rgba(138, 43, 226, 0.5);
|
||||||
|
box-shadow:
|
||||||
|
0 12px 30px rgba(0, 0, 0, 0.4),
|
||||||
|
0 0 20px rgba(138, 43, 226, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-type-card.special-chart .chart-type-icon {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(138, 43, 226, 0.3) 0%,
|
||||||
|
rgba(75, 0, 130, 0.3) 100%);
|
||||||
|
border-color: rgba(138, 43, 226, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: linear-gradient(135deg, #8a2be2, #4b0082);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 12px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(138, 43, 226, 0.8);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 5;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-indicator.expanded {
|
||||||
|
transform: translateY(-50%) rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Always Visible New Charts Content */
|
||||||
|
.new-charts-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(35, 25, 45, 0.9) 0%,
|
||||||
|
rgba(25, 20, 35, 0.95) 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(138, 43, 226, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes expandDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
max-height: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
max-height: 1000px;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-loading-inline {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-loading-inline .loading-spinner-small {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border: 3px solid rgba(138, 43, 226, 0.3);
|
||||||
|
border-top: 3px solid #8a2be2;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 12px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-loading-inline p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-charts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-item {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(25, 15, 35, 0.95) 0%,
|
||||||
|
rgba(15, 10, 25, 0.98) 100%);
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid rgba(138, 43, 226, 0.2);
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(138, 43, 226, 0.1) 0%,
|
||||||
|
transparent 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-item:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
border-color: rgba(138, 43, 226, 0.4);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 20px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 15px rgba(138, 43, 226, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-item:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(138, 43, 226, 0.3) 0%,
|
||||||
|
rgba(75, 0, 130, 0.3) 100%);
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid rgba(138, 43, 226, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-title h5 {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 2px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-artist {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(138, 43, 226, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-description {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-type {
|
||||||
|
font-size: 9px;
|
||||||
|
background: rgba(138, 43, 226, 0.2);
|
||||||
|
color: rgba(138, 43, 226, 0.9);
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-chart-action {
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(1, 255, 149, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-charts-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-charts-empty h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-charts-empty p {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================= */
|
||||||
|
/* BEATPORT GENRE CHARTS LIST VIEW */
|
||||||
|
/* ================================= */
|
||||||
|
|
||||||
|
.genre-charts-list-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-charts-list-info h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
background: linear-gradient(135deg, #8a2be2, #4b0082);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-charts-list-info p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-charts-list-container {
|
||||||
|
position: relative;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-loading-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-loading-placeholder .loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid rgba(138, 43, 226, 0.3);
|
||||||
|
border-top: 4px solid #8a2be2;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts-loading-placeholder p {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-charts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-item {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(30, 20, 40, 0.95) 0%,
|
||||||
|
rgba(20, 15, 30, 0.98) 100%);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(138, 43, 226, 0.2);
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-item::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(138, 43, 226, 0.1) 0%,
|
||||||
|
transparent 50%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: rgba(138, 43, 226, 0.4);
|
||||||
|
box-shadow:
|
||||||
|
0 15px 30px rgba(0, 0, 0, 0.3),
|
||||||
|
0 0 20px rgba(138, 43, 226, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-item:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
rgba(138, 43, 226, 0.3) 0%,
|
||||||
|
rgba(75, 0, 130, 0.3) 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
border: 1px solid rgba(138, 43, 226, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-title h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-artist {
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(138, 43, 226, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-type {
|
||||||
|
font-size: 11px;
|
||||||
|
background: rgba(138, 43, 226, 0.2);
|
||||||
|
color: rgba(138, 43, 226, 0.9);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-action {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(1, 255, 149, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-charts-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-charts-empty h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-charts-empty p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
/* ================================= */
|
/* ================================= */
|
||||||
/* RESPONSIVE DESIGN */
|
/* RESPONSIVE DESIGN */
|
||||||
/* ================================= */
|
/* ================================= */
|
||||||
|
|
@ -4872,6 +5488,71 @@ body {
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Genre Detail Responsive */
|
||||||
|
.genre-detail-info h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-types-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-type-card {
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-type-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-type-info h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-main-charts-section,
|
||||||
|
.genre-releases-section,
|
||||||
|
.genre-editorial-section,
|
||||||
|
.genre-new-charts-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Genre Charts List Responsive */
|
||||||
|
.genre-charts-list-info h2 {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-charts-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.genre-chart-item {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-header {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-icon {
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-item-title h4 {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.beatport-chart-item {
|
.beatport-chart-item {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue