Add Beatport genre Top 10 lists API and UI integration
Introduces backend scraping and API endpoint for Beatport genre Top 10 and Hype Top 10 tracks, with 1-hour caching. Updates frontend to load and display these lists in the genre browser modal, matching main page structure and click handling for chart discovery.
This commit is contained in:
parent
fcd48793e0
commit
06f3d219b4
3 changed files with 529 additions and 1 deletions
|
|
@ -3539,6 +3539,134 @@ class BeatportUnifiedScraper:
|
||||||
print(f" 📊 Successfully extracted {len(releases_data)} hero releases")
|
print(f" 📊 Successfully extracted {len(releases_data)} hero releases")
|
||||||
return releases_data
|
return releases_data
|
||||||
|
|
||||||
|
def scrape_genre_top10_tracks(self, genre_slug, genre_id):
|
||||||
|
"""Scrape Top 10 tracks lists from genre page (Beatport Top 10 + Hype Top 10 if available)"""
|
||||||
|
print(f"🎵 Scraping Top 10 tracks for {genre_slug} (ID: {genre_id})")
|
||||||
|
|
||||||
|
genre_url = f"https://www.beatport.com/genre/{genre_slug}/{genre_id}"
|
||||||
|
|
||||||
|
response = self.session.get(genre_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
soup = BeautifulSoup(response.content, 'html.parser')
|
||||||
|
|
||||||
|
# Find all tracks-list-item elements
|
||||||
|
track_items = soup.find_all(attrs={'data-testid': 'tracks-list-item'})
|
||||||
|
|
||||||
|
if not track_items:
|
||||||
|
print(f"❌ No tracks-list-item elements found on {genre_url}")
|
||||||
|
return {
|
||||||
|
'beatport_top10': [],
|
||||||
|
'hype_top10': [],
|
||||||
|
'total_tracks': 0,
|
||||||
|
'has_hype_section': False
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"📊 Found {len(track_items)} total track items")
|
||||||
|
|
||||||
|
# Extract track data from all items
|
||||||
|
all_tracks = []
|
||||||
|
for index, item in enumerate(track_items):
|
||||||
|
track_data = self.extract_track_data_from_item(item, index + 1)
|
||||||
|
if track_data:
|
||||||
|
all_tracks.append(track_data)
|
||||||
|
|
||||||
|
# Separate into Beatport Top 10 and Hype Top 10 with proper ranking
|
||||||
|
beatport_top10 = []
|
||||||
|
hype_top10 = []
|
||||||
|
|
||||||
|
for i, track in enumerate(all_tracks):
|
||||||
|
if i < 10:
|
||||||
|
# First 10 tracks = Beatport Top 10 (ranks 1-10)
|
||||||
|
track_copy = track.copy()
|
||||||
|
track_copy['rank'] = i + 1
|
||||||
|
beatport_top10.append(track_copy)
|
||||||
|
else:
|
||||||
|
# Remaining tracks = Hype Top 10 (ranks 1-10, not continuing from 11)
|
||||||
|
track_copy = track.copy()
|
||||||
|
track_copy['rank'] = (i - 10) + 1 # Reset ranking for Hype (1, 2, 3...)
|
||||||
|
hype_top10.append(track_copy)
|
||||||
|
|
||||||
|
has_hype_section = len(all_tracks) > 10
|
||||||
|
|
||||||
|
print(f"✅ Extracted {len(beatport_top10)} Beatport Top 10 + {len(hype_top10)} Hype Top 10 tracks")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'beatport_top10': beatport_top10,
|
||||||
|
'hype_top10': hype_top10,
|
||||||
|
'total_tracks': len(all_tracks),
|
||||||
|
'has_hype_section': has_hype_section
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract_track_data_from_item(self, track_item, rank):
|
||||||
|
"""Extract structured data from a tracks-list-item element"""
|
||||||
|
try:
|
||||||
|
# Extract title
|
||||||
|
title_elem = track_item.find('a') or track_item.find(class_=re.compile(r'title', re.I))
|
||||||
|
title = title_elem.get_text(strip=True) if title_elem else "Unknown Title"
|
||||||
|
|
||||||
|
# Extract URL
|
||||||
|
url = None
|
||||||
|
if title_elem and title_elem.name == 'a':
|
||||||
|
url = title_elem.get('href', '')
|
||||||
|
if url and not url.startswith('http'):
|
||||||
|
url = urljoin("https://www.beatport.com", url)
|
||||||
|
|
||||||
|
# Extract artists
|
||||||
|
artist_links = track_item.find_all('a', href=re.compile(r'/artist/'))
|
||||||
|
artists = []
|
||||||
|
artists_string = ""
|
||||||
|
|
||||||
|
if artist_links:
|
||||||
|
for artist_link in artist_links:
|
||||||
|
artist_name = artist_link.get_text(strip=True)
|
||||||
|
artist_url = artist_link.get('href', '')
|
||||||
|
if not artist_url.startswith('http'):
|
||||||
|
artist_url = urljoin("https://www.beatport.com", artist_url)
|
||||||
|
|
||||||
|
if artist_name:
|
||||||
|
artists.append({
|
||||||
|
'name': artist_name,
|
||||||
|
'url': artist_url
|
||||||
|
})
|
||||||
|
|
||||||
|
artists_string = ', '.join([a['name'] for a in artists])
|
||||||
|
else:
|
||||||
|
# Fallback: try to find artist text without links
|
||||||
|
artist_elem = track_item.find(class_=re.compile(r'artist', re.I))
|
||||||
|
artists_string = artist_elem.get_text(strip=True) if artist_elem else "Unknown Artist"
|
||||||
|
|
||||||
|
# Extract label
|
||||||
|
label_elem = track_item.find('a', href=re.compile(r'/label/'))
|
||||||
|
label = label_elem.get_text(strip=True) if label_elem else "Unknown Label"
|
||||||
|
|
||||||
|
# Extract artwork
|
||||||
|
img_elem = track_item.find('img')
|
||||||
|
artwork_url = None
|
||||||
|
if img_elem:
|
||||||
|
artwork_url = img_elem.get('src') or img_elem.get('data-src', '')
|
||||||
|
if artwork_url and not artwork_url.startswith('http'):
|
||||||
|
artwork_url = urljoin("https://www.beatport.com", artwork_url)
|
||||||
|
|
||||||
|
# Extract any additional metadata
|
||||||
|
classes = track_item.get('class', [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'title': title,
|
||||||
|
'artist': artists_string,
|
||||||
|
'artists': artists,
|
||||||
|
'label': label,
|
||||||
|
'url': url,
|
||||||
|
'artwork_url': artwork_url,
|
||||||
|
'rank': rank,
|
||||||
|
'type': 'track',
|
||||||
|
'source': 'genre_page',
|
||||||
|
'classes': classes
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error extracting track data: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def extract_hero_release_data(self, release_element) -> Dict:
|
def extract_hero_release_data(self, release_element) -> Dict:
|
||||||
"""Extract structured data from a hero release element"""
|
"""Extract structured data from a hero release element"""
|
||||||
data = {
|
data = {
|
||||||
|
|
|
||||||
|
|
@ -13208,6 +13208,77 @@ def get_beatport_genre_hero(genre_slug, genre_id):
|
||||||
"genre_id": genre_id
|
"genre_id": genre_id
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/top-10-lists', methods=['GET'])
|
||||||
|
def get_beatport_genre_top10_lists(genre_slug, genre_id):
|
||||||
|
"""Get Top 10 lists (Beatport + Hype) for a specific genre with 1-hour caching"""
|
||||||
|
try:
|
||||||
|
logger.info(f"🎵 API request for {genre_slug} Top 10 lists (ID: {genre_id})")
|
||||||
|
|
||||||
|
# Check cache first (1-hour TTL)
|
||||||
|
cached_data = get_cached_beatport_data('genre', 'top_10_lists', genre_slug)
|
||||||
|
if cached_data:
|
||||||
|
logger.info(f"✅ Returning cached Top 10 lists for {genre_slug}")
|
||||||
|
cached_data['success'] = True
|
||||||
|
cached_data['cached'] = True
|
||||||
|
return jsonify(cached_data)
|
||||||
|
|
||||||
|
# Initialize the Beatport scraper
|
||||||
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
|
# Scrape Top 10 lists from genre page
|
||||||
|
top10_data = scraper.scrape_genre_top10_tracks(genre_slug, genre_id)
|
||||||
|
|
||||||
|
if not top10_data['beatport_top10'] and not top10_data['hype_top10']:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "No Top 10 tracks found for this genre",
|
||||||
|
"beatport_top10": [],
|
||||||
|
"hype_top10": [],
|
||||||
|
"beatport_count": 0,
|
||||||
|
"hype_count": 0,
|
||||||
|
"has_hype_section": False,
|
||||||
|
"genre_slug": genre_slug,
|
||||||
|
"genre_id": genre_id,
|
||||||
|
"cached": False
|
||||||
|
})
|
||||||
|
|
||||||
|
# Prepare response data
|
||||||
|
response_data = {
|
||||||
|
"beatport_top10": top10_data['beatport_top10'],
|
||||||
|
"hype_top10": top10_data['hype_top10'],
|
||||||
|
"beatport_count": len(top10_data['beatport_top10']),
|
||||||
|
"hype_count": len(top10_data['hype_top10']),
|
||||||
|
"has_hype_section": top10_data['has_hype_section'],
|
||||||
|
"total_tracks": top10_data['total_tracks'],
|
||||||
|
"genre_slug": genre_slug,
|
||||||
|
"genre_id": genre_id,
|
||||||
|
"cached": False,
|
||||||
|
"cache_ttl": 3600 # 1 hour
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache the data (1-hour TTL)
|
||||||
|
set_cached_beatport_data('genre', 'top_10_lists', response_data, genre_slug)
|
||||||
|
|
||||||
|
logger.info(f"✅ Successfully fetched {response_data['beatport_count']} Beatport + {response_data['hype_count']} Hype Top 10 tracks for {genre_slug}")
|
||||||
|
|
||||||
|
response_data['success'] = True
|
||||||
|
return jsonify(response_data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ Error fetching Top 10 lists for {genre_slug}: {e}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"beatport_top10": [],
|
||||||
|
"hype_top10": [],
|
||||||
|
"beatport_count": 0,
|
||||||
|
"hype_count": 0,
|
||||||
|
"has_hype_section": False,
|
||||||
|
"genre_slug": genre_slug,
|
||||||
|
"genre_id": genre_id,
|
||||||
|
"cached": False
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/sections', methods=['GET'])
|
@app.route('/api/beatport/genre/<genre_slug>/<genre_id>/sections', methods=['GET'])
|
||||||
def get_beatport_genre_sections(genre_slug, genre_id):
|
def get_beatport_genre_sections(genre_slug, genre_id):
|
||||||
"""Discover all available sections for a specific Beatport genre"""
|
"""Discover all available sections for a specific Beatport genre"""
|
||||||
|
|
|
||||||
|
|
@ -21253,7 +21253,11 @@ async function handleGenreBrowserCardClick(genreSlug, genreId, genreName) {
|
||||||
showGenrePageView(genreSlug, genreId, genreName);
|
showGenrePageView(genreSlug, genreId, genreName);
|
||||||
|
|
||||||
// Load the hero slider data
|
// Load the hero slider data
|
||||||
await loadGenreHeroSlider(genreSlug, genreId, genreName);
|
// Load hero slider and Top 10 lists in parallel
|
||||||
|
await Promise.all([
|
||||||
|
loadGenreHeroSlider(genreSlug, genreId, genreName),
|
||||||
|
loadGenreTop10Lists(genreSlug, genreId, genreName)
|
||||||
|
]);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error loading genre page for ${genreName}:`, error);
|
console.error(`❌ Error loading genre page for ${genreName}:`, error);
|
||||||
|
|
@ -21309,6 +21313,12 @@ function showGenrePageView(genreSlug, genreId, genreName) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="genre-top10-lists-container" id="genre-top10-lists-container">
|
||||||
|
<div class="genre-top10-loading-container">
|
||||||
|
<div class="genre-loading-spinner"></div>
|
||||||
|
<p class="genre-loading-text">🎵 Loading Top 10 lists...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
modal.querySelector('.genre-browser-modal-content').appendChild(genrePageContent);
|
modal.querySelector('.genre-browser-modal-content').appendChild(genrePageContent);
|
||||||
|
|
@ -21679,6 +21689,325 @@ function startGenreHeroSliderAutoPlay() {
|
||||||
console.log(`▶️ Started auto-play for genre hero slider (${window.genreHeroSliderState.totalSlides} slides)`);
|
console.log(`▶️ Started auto-play for genre hero slider (${window.genreHeroSliderState.totalSlides} slides)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load Top 10 lists for a specific genre (Beatport + Hype)
|
||||||
|
*/
|
||||||
|
async function loadGenreTop10Lists(genreSlug, genreId, genreName) {
|
||||||
|
console.log(`🎵 Loading Top 10 lists for ${genreName}...`);
|
||||||
|
|
||||||
|
const container = document.getElementById('genre-top10-lists-container');
|
||||||
|
if (!container) {
|
||||||
|
console.error('❌ Genre Top 10 lists container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/beatport/genre/${genreSlug}/${genreId}/top-10-lists`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Failed to load Top 10 lists');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Loaded ${data.beatport_count} Beatport + ${data.hype_count} Hype Top 10 tracks for ${genreName}`);
|
||||||
|
|
||||||
|
// Generate HTML using exact same structure as main page (but unique IDs)
|
||||||
|
const top10ListsHTML = createGenreTop10ListsHTML(data, genreName);
|
||||||
|
container.innerHTML = top10ListsHTML;
|
||||||
|
|
||||||
|
// Add container-level click handlers exactly like main page
|
||||||
|
addGenreTop10ClickHandlers();
|
||||||
|
|
||||||
|
console.log(`✅ Successfully populated genre Top 10 lists for ${genreName}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error loading Top 10 lists for ${genreName}:`, error);
|
||||||
|
|
||||||
|
// Show error state
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="genre-top10-error">
|
||||||
|
<h3>❌ Error Loading Top 10 Lists</h3>
|
||||||
|
<p>Could not load Top 10 tracks for ${genreName}</p>
|
||||||
|
<p class="error-detail">${error.message}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create HTML for genre Top 10 lists (exact structure as main page, unique IDs)
|
||||||
|
*/
|
||||||
|
function createGenreTop10ListsHTML(data, genreName) {
|
||||||
|
const { beatport_top10, hype_top10, has_hype_section } = data;
|
||||||
|
|
||||||
|
// Use exact same structure as main page but with genre-specific IDs
|
||||||
|
let html = `
|
||||||
|
<div class="beatport-top10-section">
|
||||||
|
<div class="beatport-top10-header">
|
||||||
|
<h2 class="beatport-top10-title">🏆 ${genreName} Top 10 Lists</h2>
|
||||||
|
<p class="beatport-top10-subtitle">Current trending ${genreName.toLowerCase()} tracks</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="beatport-top10-container">
|
||||||
|
<!-- Beatport Top 10 List (same classes, unique ID) -->
|
||||||
|
<div class="beatport-top10-list" id="genre-beatport-top10-list">
|
||||||
|
<div class="beatport-top10-list-header">
|
||||||
|
<h3 class="beatport-top10-list-title">🎵 Beatport Top 10</h3>
|
||||||
|
<p class="beatport-top10-list-subtitle">Most popular ${genreName.toLowerCase()} tracks</p>
|
||||||
|
</div>
|
||||||
|
<div class="beatport-top10-tracks">
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add Beatport Top 10 tracks (same classes as main page)
|
||||||
|
beatport_top10.forEach((track, index) => {
|
||||||
|
const cleanTitle = cleanTrackText(track.title || 'Unknown Title');
|
||||||
|
const cleanArtist = cleanTrackText(track.artist || 'Unknown Artist');
|
||||||
|
const cleanLabel = cleanTrackText(track.label || 'Unknown Label');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="beatport-top10-card" data-url="${track.url || '#'}">
|
||||||
|
<div class="beatport-top10-card-rank">${track.rank || index + 1}</div>
|
||||||
|
<div class="beatport-top10-card-artwork">
|
||||||
|
${track.artwork_url ?
|
||||||
|
`<img src="${track.artwork_url}" alt="${cleanTitle}" loading="lazy">` :
|
||||||
|
'<div class="beatport-top10-card-placeholder">🎵</div>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="beatport-top10-card-info">
|
||||||
|
<h4 class="beatport-top10-card-title">${cleanTitle}</h4>
|
||||||
|
<p class="beatport-top10-card-artist">${cleanArtist}</p>
|
||||||
|
<p class="beatport-top10-card-label">${cleanLabel}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add Hype Top 10 section (same classes, unique ID)
|
||||||
|
if (has_hype_section && hype_top10.length > 0) {
|
||||||
|
html += `
|
||||||
|
<!-- Hype Top 10 List (same classes, unique ID) -->
|
||||||
|
<div class="beatport-hype10-list" id="genre-beatport-hype10-list">
|
||||||
|
<div class="beatport-hype10-list-header">
|
||||||
|
<h3 class="beatport-hype10-list-title">🔥 Hype Top 10</h3>
|
||||||
|
<p class="beatport-hype10-list-subtitle">Editor's trending ${genreName.toLowerCase()} picks</p>
|
||||||
|
</div>
|
||||||
|
<div class="beatport-hype10-tracks">
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add Hype Top 10 tracks (same classes as main page)
|
||||||
|
hype_top10.forEach((track, index) => {
|
||||||
|
const cleanTitle = cleanTrackText(track.title || 'Unknown Title');
|
||||||
|
const cleanArtist = cleanTrackText(track.artist || 'Unknown Artist');
|
||||||
|
const cleanLabel = cleanTrackText(track.label || 'Unknown Label');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="beatport-hype10-card" data-url="${track.url || '#'}">
|
||||||
|
<div class="beatport-hype10-card-rank">${track.rank || index + 1}</div>
|
||||||
|
<div class="beatport-hype10-card-artwork">
|
||||||
|
${track.artwork_url ?
|
||||||
|
`<img src="${track.artwork_url}" alt="${cleanTitle}" loading="lazy">` :
|
||||||
|
'<div class="beatport-hype10-card-placeholder">🔥</div>'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="beatport-hype10-card-info">
|
||||||
|
<h4 class="beatport-hype10-card-title">${cleanTitle}</h4>
|
||||||
|
<p class="beatport-hype10-card-artist">${cleanArtist}</p>
|
||||||
|
<p class="beatport-hype10-card-label">${cleanLabel}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Add empty hype placeholder to maintain side-by-side layout
|
||||||
|
html += `
|
||||||
|
<!-- Empty Hype Placeholder -->
|
||||||
|
<div class="beatport-hype10-list" id="genre-beatport-hype10-list">
|
||||||
|
<div class="beatport-hype10-list-header">
|
||||||
|
<h3 class="beatport-hype10-list-title">🔥 Hype Top 10</h3>
|
||||||
|
<p class="beatport-hype10-list-subtitle">No trending picks available</p>
|
||||||
|
</div>
|
||||||
|
<div class="beatport-hype10-tracks">
|
||||||
|
<div class="beatport-hype10-empty">
|
||||||
|
<p>No Hype Top 10 tracks for this genre</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add container-level click handlers for genre Top 10 lists (exact parity with main page)
|
||||||
|
*/
|
||||||
|
function addGenreTop10ClickHandlers() {
|
||||||
|
console.log('🔗 Adding container-level click handlers for genre Top 10 lists...');
|
||||||
|
|
||||||
|
// Add container-level click handler for Beatport Top 10 (exact match to main page)
|
||||||
|
const beatportContainer = document.getElementById('genre-beatport-top10-list');
|
||||||
|
if (beatportContainer) {
|
||||||
|
beatportContainer.addEventListener('click', () => {
|
||||||
|
console.log('🎵 Genre Beatport Top 10 container clicked');
|
||||||
|
handleGenreBeatportTop10Click();
|
||||||
|
});
|
||||||
|
console.log('✅ Added Beatport Top 10 container click handler');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add container-level click handler for Hype Top 10 (exact match to main page)
|
||||||
|
const hypeContainer = document.getElementById('genre-beatport-hype10-list');
|
||||||
|
if (hypeContainer) {
|
||||||
|
hypeContainer.addEventListener('click', () => {
|
||||||
|
console.log('🔥 Genre Hype Top 10 container clicked');
|
||||||
|
handleGenreHypeTop10Click();
|
||||||
|
});
|
||||||
|
console.log('✅ Added Hype Top 10 container click handler');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Set up container-level click handlers for genre Top 10 lists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle genre Beatport Top 10 container click (exact parity with main page)
|
||||||
|
*/
|
||||||
|
async function handleGenreBeatportTop10Click() {
|
||||||
|
console.log('🎵 Handling Genre Beatport Top 10 click');
|
||||||
|
|
||||||
|
// Use exact same pattern as main page
|
||||||
|
await handleGenreChartClick('genre_beatport_top10', 'Genre Beatport Top 10', 'genre_beatport_top10');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle genre Hype Top 10 container click (exact parity with main page)
|
||||||
|
*/
|
||||||
|
async function handleGenreHypeTop10Click() {
|
||||||
|
console.log('🔥 Handling Genre Hype Top 10 click');
|
||||||
|
|
||||||
|
// Use exact same pattern as main page
|
||||||
|
await handleGenreChartClick('genre_hype_top10', 'Genre Hype Top 10', 'genre_hype_top10');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle genre chart click (based on main page handleRebuildChartClick)
|
||||||
|
*/
|
||||||
|
async function handleGenreChartClick(trackDataKey, chartName, chartType) {
|
||||||
|
try {
|
||||||
|
// Create chart hash (following main page pattern)
|
||||||
|
const chartHash = `${chartType}_${Date.now()}`;
|
||||||
|
|
||||||
|
// Check if we already have an existing state (following main page pattern)
|
||||||
|
const existingState = Object.values(beatportChartStates).find(state =>
|
||||||
|
state.chart && state.chart.name === chartName && state.chart.chart_type === chartType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingState) {
|
||||||
|
console.log(`🔄 Found existing ${chartName} card, opening existing modal`);
|
||||||
|
// Use existing card click handler (following main page pattern)
|
||||||
|
handleBeatportCardClick(existingState.chart.hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract track data from DOM cards (exact same pattern as main page)
|
||||||
|
const trackData = await getGenrePageTrackData(trackDataKey);
|
||||||
|
if (!trackData || trackData.length === 0) {
|
||||||
|
throw new Error(`No track data found for ${chartName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform DOM data to Browse Charts format EXACTLY like main page
|
||||||
|
const chartData = {
|
||||||
|
hash: chartHash,
|
||||||
|
name: chartName,
|
||||||
|
chart_type: chartType,
|
||||||
|
track_count: trackData.length,
|
||||||
|
tracks: trackData.map(track => ({
|
||||||
|
name: cleanTrackText(track.title || 'Unknown Title'),
|
||||||
|
artists: [cleanTrackText(track.artist || 'Unknown Artist')],
|
||||||
|
album: chartName,
|
||||||
|
duration_ms: 0,
|
||||||
|
external_urls: { beatport: track.url || '' },
|
||||||
|
source: 'beatport'
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Follow main page pattern EXACTLY:
|
||||||
|
// 1. Add card to container (creates playlist card)
|
||||||
|
console.log(`🃏 Creating Beatport playlist card for: ${chartData.name}`);
|
||||||
|
addBeatportCardToContainer(chartData);
|
||||||
|
|
||||||
|
// 2. Automatically open discovery modal (like when you click a card in fresh state)
|
||||||
|
handleBeatportCardClick(chartHash);
|
||||||
|
|
||||||
|
console.log(`✅ Created ${chartName} card and opened discovery modal`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error handling ${chartName} click:`, error);
|
||||||
|
showToast(`Error loading ${chartName}: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract track data from genre page DOM (based on main page getRebuildPageTrackData)
|
||||||
|
*/
|
||||||
|
async function getGenrePageTrackData(trackDataKey) {
|
||||||
|
console.log(`🔍 Extracting ${trackDataKey} data from genre page DOM`);
|
||||||
|
|
||||||
|
let containerSelector, cardSelector;
|
||||||
|
if (trackDataKey === 'genre_beatport_top10') {
|
||||||
|
containerSelector = '#genre-beatport-top10-list';
|
||||||
|
cardSelector = '.beatport-top10-card[data-url]';
|
||||||
|
} else if (trackDataKey === 'genre_hype_top10') {
|
||||||
|
containerSelector = '#genre-beatport-hype10-list';
|
||||||
|
cardSelector = '.beatport-hype10-card[data-url]';
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown track data key: ${trackDataKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.querySelector(containerSelector);
|
||||||
|
if (!container) {
|
||||||
|
throw new Error(`Container ${containerSelector} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackCards = container.querySelectorAll(cardSelector);
|
||||||
|
if (trackCards.length === 0) {
|
||||||
|
throw new Error(`No track cards found in ${containerSelector}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract track data from DOM cards (exact same pattern as main page)
|
||||||
|
const tracks = Array.from(trackCards).map(card => {
|
||||||
|
const title = card.querySelector('.beatport-top10-card-title, .beatport-hype10-card-title')?.textContent?.trim() || 'Unknown Title';
|
||||||
|
const artist = card.querySelector('.beatport-top10-card-artist, .beatport-hype10-card-artist')?.textContent?.trim() || 'Unknown Artist';
|
||||||
|
const label = card.querySelector('.beatport-top10-card-label, .beatport-hype10-card-label')?.textContent?.trim() || 'Unknown Label';
|
||||||
|
const url = card.getAttribute('data-url') || '';
|
||||||
|
const rank = card.querySelector('.beatport-top10-card-rank, .beatport-hype10-card-rank')?.textContent?.trim() || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
artist: artist,
|
||||||
|
label: label,
|
||||||
|
url: url,
|
||||||
|
rank: rank
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📋 Extracted ${tracks.length} tracks from ${containerSelector}`);
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle genre-specific Top 100 button click - create discovery process for genre top 100 tracks
|
* Handle genre-specific Top 100 button click - create discovery process for genre top 100 tracks
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue