beatport progress

This commit is contained in:
Broque Thomas 2025-09-26 22:04:46 -07:00
parent 0ba4b6a079
commit f5ae3d611c
3 changed files with 898 additions and 16 deletions

View file

@ -10155,6 +10155,10 @@ def cancel_tidal_sync(playlist_id):
youtube_playlist_states = {} # Key: url_hash, Value: persistent playlist state
youtube_discovery_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="youtube_discovery")
# Global state for Beatport chart management (persistent across page reloads)
beatport_chart_states = {} # Key: url_hash, Value: persistent chart state
beatport_discovery_executor = ThreadPoolExecutor(max_workers=3, thread_name_prefix="beatport_discovery")
@app.route('/api/youtube/parse', methods=['POST'])
def parse_youtube_playlist_endpoint():
"""Parse a YouTube playlist URL and return structured track data"""
@ -12011,6 +12015,390 @@ def get_beatport_genre_image(genre_slug, genre_id):
"image_url": None
}), 500
@app.route('/api/beatport/hype-top-100', methods=['GET'])
def get_beatport_hype_top_100():
"""Get Beatport Hype Top 100 - Improved with fixed URL"""
try:
logger.info("🔥 API request for Beatport Hype Top 100")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '100'))
# Scrape Hype Top 100 using improved method
tracks = scraper.scrape_hype_top_100(limit=limit)
logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Hype Top 100")
return jsonify({
"success": True,
"tracks": tracks,
"chart_name": "Beatport Hype Top 100",
"count": len(tracks)
})
except Exception as e:
logger.error(f"❌ Error fetching Beatport Hype Top 100: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/top-100-releases', methods=['GET'])
def get_beatport_top_100_releases():
"""Get Beatport Top 100 Releases - New endpoint"""
try:
logger.info("📊 API request for Beatport Top 100 Releases")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '100'))
# Scrape Top 100 Releases using new method
tracks = scraper.scrape_top_100_releases(limit=limit)
logger.info(f"✅ Successfully scraped {len(tracks)} tracks from Beatport Top 100 Releases")
return jsonify({
"success": True,
"tracks": tracks,
"chart_name": "Beatport Top 100 Releases",
"count": len(tracks)
})
except Exception as e:
logger.error(f"❌ Error fetching Beatport Top 100 Releases: {e}")
return jsonify({
"success": False,
"error": str(e),
"tracks": [],
"count": 0
}), 500
@app.route('/api/beatport/chart-sections', methods=['GET'])
def get_beatport_chart_sections():
"""Get dynamically discovered Beatport chart sections"""
try:
logger.info("🔍 API request for Beatport chart sections discovery")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Discover chart sections dynamically
chart_sections = scraper.discover_chart_sections()
logger.info(f"✅ Successfully discovered chart sections")
return jsonify({
"success": True,
"chart_sections": chart_sections,
"summary": chart_sections.get('summary', {})
})
except Exception as e:
logger.error(f"❌ Error discovering Beatport chart sections: {e}")
return jsonify({
"success": False,
"error": str(e),
"chart_sections": {},
"summary": {}
}), 500
@app.route('/api/beatport/dj-charts-improved', methods=['GET'])
def get_beatport_dj_charts_improved():
"""Get Beatport DJ Charts using improved method"""
try:
logger.info("🎧 API request for Beatport DJ Charts (improved)")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Get query parameters
limit = int(request.args.get('limit', '20'))
# Scrape DJ Charts using improved method
charts = scraper.scrape_dj_charts(limit=limit)
logger.info(f"✅ Successfully scraped {len(charts)} DJ charts")
return jsonify({
"success": True,
"charts": charts,
"chart_name": "Beatport DJ Charts",
"count": len(charts)
})
except Exception as e:
logger.error(f"❌ Error fetching Beatport DJ Charts: {e}")
return jsonify({
"success": False,
"error": str(e),
"charts": [],
"count": 0
}), 500
@app.route('/api/beatport/discovery/start/<url_hash>', methods=['POST'])
def start_beatport_discovery(url_hash):
"""Start Spotify discovery for Beatport chart tracks"""
import json
try:
logger.info(f"🔍 Starting Beatport discovery for: {url_hash}")
# Get chart data from request body
data = request.get_json() or {}
print(f"🔍 Raw request data: {data}")
chart_data = data.get('chart_data')
print(f"🔍 Chart data extracted: {chart_data is not None}")
# Debug logging
if chart_data:
print(f"🔍 Chart data keys: {list(chart_data.keys()) if isinstance(chart_data, dict) else 'Not a dict'}")
print(f"🔍 Chart name: {chart_data.get('name') if isinstance(chart_data, dict) else 'N/A'}")
if isinstance(chart_data, dict) and 'tracks' in chart_data:
print(f"🔍 Number of tracks: {len(chart_data['tracks'])}")
if chart_data['tracks']:
print(f"🔍 First track: {chart_data['tracks'][0]}")
else:
print("🔍 No chart data received")
if not chart_data or not chart_data.get('tracks'):
return jsonify({"error": "Chart data with tracks is required"}), 400
# Initialize Beatport chart state (similar to YouTube)
if url_hash not in beatport_chart_states:
beatport_chart_states[url_hash] = {
'chart': chart_data,
'phase': 'fresh',
'discovery_results': [],
'discovery_progress': 0,
'spotify_matches': 0,
'spotify_total': len(chart_data['tracks']),
'status': 'fresh',
'last_accessed': time.time()
}
state = beatport_chart_states[url_hash]
state['last_accessed'] = time.time()
if state['phase'] == 'discovering':
return jsonify({"error": "Discovery already in progress"}), 400
# Update phase to discovering
state['phase'] = 'discovering'
state['status'] = 'discovering'
state['discovery_progress'] = 0
state['spotify_matches'] = 0
# Add activity for discovery start
chart_name = chart_data.get('name', 'Unknown Chart')
track_count = len(chart_data['tracks'])
add_activity_item("🔍", "Beatport Discovery Started", f"'{chart_name}' - {track_count} tracks", "Now")
# Start discovery worker
future = beatport_discovery_executor.submit(_run_beatport_discovery_worker, url_hash)
state['discovery_future'] = future
print(f"🔍 Started Spotify discovery for Beatport chart: {chart_name}")
return jsonify({"success": True, "message": "Discovery started", "status": "discovering"})
except Exception as e:
logger.error(f"❌ Error starting Beatport discovery: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/beatport/discovery/status/<url_hash>', methods=['GET'])
def get_beatport_discovery_status(url_hash):
"""Get real-time discovery status for a Beatport chart"""
try:
if url_hash not in beatport_chart_states:
return jsonify({"error": "Beatport chart not found"}), 404
state = beatport_chart_states[url_hash]
state['last_accessed'] = time.time()
response = {
'phase': state['phase'],
'status': state['status'],
'progress': state['discovery_progress'],
'spotify_matches': state['spotify_matches'],
'spotify_total': state['spotify_total'],
'results': state['discovery_results'],
'complete': state['phase'] == 'discovered'
}
return jsonify(response)
except Exception as e:
logger.error(f"❌ Error getting Beatport discovery status: {e}")
return jsonify({"error": str(e)}), 500
def _run_beatport_discovery_worker(url_hash):
"""Background worker for Beatport Spotify discovery process"""
try:
state = beatport_chart_states[url_hash]
chart = state['chart']
tracks = chart['tracks']
print(f"🔍 Starting Spotify discovery for {len(tracks)} Beatport tracks...")
if not spotify_client or not spotify_client.is_authenticated():
print("❌ Spotify client not authenticated")
state['status'] = 'error'
state['phase'] = 'fresh'
return
# Process each track for Spotify discovery
for i, track in enumerate(tracks):
try:
# Update progress
state['discovery_progress'] = int((i / len(tracks)) * 100)
# Get track info from Beatport data (frontend sends 'name' and 'artists' fields)
track_title = track.get('name', 'Unknown Title')
track_artists = track.get('artists', ['Unknown Artist'])
# Handle artists - could be a list or string
if isinstance(track_artists, list):
if len(track_artists) > 0 and isinstance(track_artists[0], str):
# Handle case like ["CID,Taylr Renee"] - split on comma
track_artist = track_artists[0].split(',')[0].strip()
else:
track_artist = track_artists[0] if track_artists else 'Unknown Artist'
else:
track_artist = str(track_artists)
print(f"🔍 Searching Spotify for: '{track_artist}' - '{track_title}'")
# Try multiple search strategies
spotify_track = None
# Clean track title for search (remove remix info)
import re
clean_title = re.sub(r'\s*\([^)]*\)', '', track_title).strip() # Remove (Extended Mix), (Original Mix), etc.
clean_title = re.sub(r'\s*\[[^\]]*\]', '', clean_title).strip() # Remove [brackets]
# Strategy 1: Simple search with cleaned terms
search_query = f"{track_artist} {clean_title}"
print(f"🔍 Search query: {search_query}")
try:
search_results = spotify_client.search_tracks(search_query, limit=10)
print(f"🔍 Search results type: {type(search_results)}, length: {len(search_results) if search_results else 0}")
# Find best match from search_tracks result
if search_results:
for result in search_results:
try:
# Check if artist matches (case insensitive, flexible)
result_artists = [artist.lower() for artist in result.artists]
artist_match = any(track_artist.lower() in artist for artist in result_artists) or any(artist in track_artist.lower() for artist in result_artists)
# Check if title matches (case insensitive, flexible)
title_match = clean_title.lower() in result.name.lower() or result.name.lower() in clean_title.lower()
if artist_match and title_match:
spotify_track = result
print(f"✅ Found match: {result.artists[0]} - {result.name}")
break
except Exception as e:
print(f"❌ Error processing search result: {e}")
continue
except Exception as e:
print(f"❌ Error in Spotify search: {e}")
# Strategy 2: Try artist-only search if no match
if not spotify_track:
print(f"🔍 Trying artist-only search: {track_artist}")
search_results = spotify_client.search_tracks(track_artist, limit=5)
if search_results:
for result in search_results:
result_artists = [artist.lower() for artist in result.artists]
if any(track_artist.lower() in artist for artist in result_artists):
print(f"✅ Found by artist: {result.artists[0]} - {result.name}")
spotify_track = result
break
# Create result entry
result_entry = {
'index': i, # Add index for frontend table row identification
'beatport_track': {
'title': track_title,
'artist': track_artist
},
'status': 'found' if spotify_track else 'not_found',
'status_class': 'found' if spotify_track else 'not-found' # Add status class for CSS styling
}
if spotify_track:
# Debug: show available attributes
print(f"🔍 Spotify track attributes: {dir(spotify_track)}")
# Format artists correctly for frontend compatibility
formatted_artists = []
if isinstance(spotify_track.artists, list):
# If it's already a list of strings, convert to objects with 'name' property
for artist in spotify_track.artists:
if isinstance(artist, str):
formatted_artists.append({'name': artist})
else:
# If it's already an object, use as-is
formatted_artists.append(artist)
else:
# Single artist case
formatted_artists = [{'name': str(spotify_track.artists)}]
result_entry['spotify_data'] = {
'name': spotify_track.name,
'artists': formatted_artists, # Now formatted as list of objects with 'name' property
'album': spotify_track.album, # Already a string
'id': spotify_track.id
# Remove uri for now since it's causing errors
}
state['spotify_matches'] += 1
state['discovery_results'].append(result_entry)
# Small delay to avoid rate limiting
time.sleep(0.1)
except Exception as e:
print(f"❌ Error processing Beatport track {i}: {e}")
# Add error result
state['discovery_results'].append({
'index': i, # Add index for frontend table row identification
'beatport_track': {
'title': track.get('name', 'Unknown'), # Changed from 'title' to 'name' to match track structure
'artist': track.get('artists', ['Unknown'])[0] if isinstance(track.get('artists'), list) else 'Unknown'
},
'status': 'error',
'status_class': 'error', # Add status class for CSS styling
'error': str(e)
})
# Mark discovery as complete
state['discovery_progress'] = 100
state['phase'] = 'discovered'
state['status'] = 'discovered'
# Add activity for completion
chart_name = chart.get('name', 'Unknown Chart')
add_activity_item("", "Beatport Discovery Complete",
f"'{chart_name}' - {state['spotify_matches']}/{len(tracks)} tracks found", "Now")
print(f"✅ Beatport discovery complete: {state['spotify_matches']}/{len(tracks)} tracks found")
except Exception as e:
print(f"❌ Error in Beatport discovery worker: {e}")
if url_hash in beatport_chart_states:
beatport_chart_states[url_hash]['status'] = 'error'
beatport_chart_states[url_hash]['phase'] = 'fresh'
class WebMetadataUpdateWorker:
"""Web-based metadata update worker - EXACT port of dashboard.py MetadataUpdateWorker"""

View file

@ -47,6 +47,9 @@ let tidalPlaylists = [];
let tidalPlaylistStates = {}; // Key: playlist_id, Value: playlist state with phases
let tidalPlaylistsLoaded = false;
// --- Beatport Chart State Management (Similar to YouTube/Tidal) ---
let beatportChartStates = {}; // Key: chart_hash, Value: chart state with phases
// --- Artists Page State Management ---
let artistsPageState = {
currentView: 'search', // 'search', 'results', 'detail'
@ -9237,7 +9240,9 @@ function initializeSyncPage() {
item.addEventListener('click', () => {
const chartType = item.dataset.chartType;
const chartId = item.dataset.chartId;
handleBeatportChartClick(chartType, chartId);
const chartName = item.dataset.chartName;
const chartEndpoint = item.dataset.chartEndpoint;
handleBeatportChartClick(chartType, chartId, chartName, chartEndpoint);
});
});
@ -9573,6 +9578,7 @@ function handleBeatportCategoryClick(category) {
switch(category) {
case 'top-charts':
showBeatportSubView('top-charts');
loadBeatportTopCharts(); // Load top charts dynamically
break;
case 'genres':
showBeatportSubView('genres');
@ -9749,6 +9755,287 @@ async function loadGenreImagesProgressively(genres) {
console.log(`✅ Progressive image loading complete: ${imagesLoaded}/${genres.length} images loaded`);
}
async function loadBeatportTopCharts() {
console.log('🔥 Loading Beatport top charts dynamically...');
const chartList = document.querySelector('#beatport-top-charts-view .beatport-chart-list');
if (!chartList) {
console.error('❌ Could not find chart list element');
return;
}
// Show loading state
chartList.innerHTML = `
<div class="chart-loading-placeholder">
<div class="loading-spinner"></div>
<p>🔥 Discovering current Beatport top charts...</p>
</div>
`;
try {
// Define the top charts we want to show (based on our working endpoints)
const topCharts = [
{
id: 'beatport-top-100',
name: 'Beatport Top 100',
description: 'The hottest electronic tracks right now',
icon: '🏆',
endpoint: '/api/beatport/top-100',
trackCount: '100 tracks'
},
{
id: 'hype-top-100',
name: 'Hype Top 100',
description: 'Trending tracks gaining momentum',
icon: '🚀',
endpoint: '/api/beatport/hype-top-100',
trackCount: '100 tracks'
}
];
// Generate chart cards dynamically
const chartCardsHTML = topCharts.map(chart => `
<div class="beatport-chart-item"
data-chart-type="${chart.id}"
data-chart-id="${chart.id}"
data-chart-name="${chart.name}"
data-chart-endpoint="${chart.endpoint}">
<div class="chart-icon">${chart.icon}</div>
<div class="chart-info">
<h3>${chart.name}</h3>
<p>${chart.description}</p>
<span class="track-count">${chart.trackCount}</span>
</div>
</div>
`).join('');
chartList.innerHTML = chartCardsHTML;
// Add click handlers to dynamically created chart items
const chartItems = chartList.querySelectorAll('.beatport-chart-item');
chartItems.forEach(item => {
item.addEventListener('click', () => {
const chartType = item.dataset.chartType;
const chartId = item.dataset.chartId;
const chartName = item.dataset.chartName;
const chartEndpoint = item.dataset.chartEndpoint;
handleBeatportTopChartClick(chartType, chartId, chartName, chartEndpoint);
});
});
console.log(`✅ Loaded ${topCharts.length} Beatport top charts dynamically`);
showToast(`Loaded ${topCharts.length} top charts`, 'success');
} catch (error) {
console.error('❌ Error loading Beatport top charts:', error);
chartList.innerHTML = `
<div class="chart-error-placeholder">
<p> Failed to load top charts: ${error.message}</p>
<button onclick="loadBeatportTopCharts()" class="refresh-charts-btn">🔄 Retry</button>
</div>
`;
showToast(`Error loading Beatport top charts: ${error.message}`, 'error');
}
}
async function handleBeatportTopChartClick(chartType, chartId, chartName, chartEndpoint) {
console.log(`🔥 Beatport top chart clicked: ${chartName} - CREATING PLAYLIST CARD`);
try {
// First, create a chart hash for state management
const chartHash = `${chartType}_${chartId}_${Date.now()}`;
showToast(`Loading ${chartName}...`, 'info');
// Fetch tracks from the chart endpoint
const response = await fetch(`${chartEndpoint}?limit=100`);
if (!response.ok) {
throw new Error(`Failed to fetch ${chartName}: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
throw new Error(`No tracks found in ${chartName}`);
}
// Create chart data object for playlist card
const chartData = {
hash: chartHash,
name: chartName,
chart_type: chartType,
track_count: data.tracks.length,
tracks: data.tracks.map(track => ({
name: track.title || 'Unknown Title',
artists: [track.artist || 'Unknown Artist'],
album: chartName,
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: ${chartName}`);
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 ${chartName}`);
} catch (error) {
console.error(`❌ Error loading ${chartName}:`, error);
showToast(`Error loading ${chartName}: ${error.message}`, 'error');
}
}
async function openBeatportDiscoveryModal(chartHash, chartData) {
console.log(`🎵 Opening Beatport discovery modal (reusing YouTube modal): ${chartData.name}`);
// Create YouTube-style state entry for this Beatport chart
const beatportState = {
phase: 'fresh',
playlist: {
name: chartData.name,
tracks: chartData.tracks,
description: `${chartData.track_count} tracks from ${chartData.name}`,
source: 'beatport'
},
is_beatport_playlist: true,
beatport_chart_type: chartData.chart_type,
beatport_chart_hash: chartHash // Link to Beatport card state
};
// Store in YouTube playlist states (reusing the infrastructure)
youtubePlaylistStates[chartHash] = beatportState;
// Start discovery automatically (like Tidal does)
try {
console.log(`🔍 Starting Beatport discovery for: ${chartData.name}`);
// Update card phase to discovering immediately
updateBeatportCardPhase(chartHash, 'discovering');
// Call the discovery start endpoint with chart data
const response = await fetch(`/api/beatport/discovery/start/${chartHash}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
chart_data: chartData
})
});
const result = await response.json();
if (result.success) {
// Update state to discovering
youtubePlaylistStates[chartHash].phase = 'discovering';
// Start polling for progress
startBeatportDiscoveryPolling(chartHash);
console.log(`✅ Started Beatport discovery for: ${chartData.name}`);
} else {
console.error('❌ Error starting Beatport discovery:', result.error);
showToast(`Error starting discovery: ${result.error}`, 'error');
// Revert card phase on error
updateBeatportCardPhase(chartHash, 'fresh');
}
} catch (error) {
console.error('❌ Error starting Beatport discovery:', error);
showToast(`Error starting discovery: ${error.message}`, 'error');
// Revert card phase on error
updateBeatportCardPhase(chartHash, 'fresh');
}
// Open the existing YouTube discovery modal infrastructure
openYouTubeDiscoveryModal(chartHash);
console.log(`✅ Beatport discovery modal opened for ${chartData.name} with ${chartData.tracks.length} tracks`);
}
function startBeatportDiscoveryPolling(urlHash) {
console.log(`🔄 Starting Beatport discovery polling for: ${urlHash}`);
// Stop any existing polling (reuse YouTube polling infrastructure)
if (activeYouTubePollers[urlHash]) {
clearInterval(activeYouTubePollers[urlHash]);
}
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/beatport/discovery/status/${urlHash}`);
const status = await response.json();
if (status.error) {
console.error('❌ Error polling Beatport discovery status:', status.error);
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
return;
}
// Update state and modal (reuse YouTube infrastructure like Tidal)
if (youtubePlaylistStates[urlHash]) {
// Transform Beatport results to YouTube modal format (like Tidal does)
const transformedStatus = {
progress: status.progress || 0,
spotify_matches: status.spotify_matches || 0,
spotify_total: status.spotify_total || 0,
results: (status.results || []).map((result, index) => ({
index: result.index !== undefined ? result.index : index,
yt_track: result.beatport_track ? result.beatport_track.title : 'Unknown',
yt_artist: result.beatport_track ? result.beatport_track.artist : 'Unknown',
status: result.status === 'found' ? '✅ Found' : (result.status === 'error' ? '❌ Error' : '❌ Not Found'),
status_class: result.status_class || (result.status === 'found' ? 'found' : (result.status === 'error' ? 'error' : 'not-found')),
spotify_track: result.spotify_data ? result.spotify_data.name : '-',
spotify_artist: result.spotify_data && result.spotify_data.artists ?
result.spotify_data.artists.map(a => a.name || a).join(', ') : '-',
spotify_album: result.spotify_data ? result.spotify_data.album : '-'
}))
};
// Update state with both backend and frontend formats (like Tidal)
const state = youtubePlaylistStates[urlHash];
state.discovery_progress = status.progress; // Backend format
state.discoveryProgress = status.progress; // Frontend format - for modal progress display
state.spotify_matches = status.spotify_matches; // Backend format
state.spotifyMatches = status.spotify_matches; // Frontend format - for button logic
state.discovery_results = status.results; // Backend format
state.discoveryResults = transformedStatus.results; // Frontend format - for button logic
state.phase = status.phase || 'discovering';
// Update Beatport card phase and progress
const chartHash = state.beatport_chart_hash || urlHash;
updateBeatportCardPhase(chartHash, status.phase || 'discovering');
updateBeatportCardProgress(chartHash, {
spotify_total: status.spotify_total || 0,
spotify_matches: status.spotify_matches || 0,
failed: (status.spotify_total || 0) - (status.spotify_matches || 0)
});
// Update modal display with transformed data
updateYouTubeDiscoveryModal(urlHash, transformedStatus);
}
// Stop polling when discovery is complete
if (status.phase === 'discovered' || status.phase === 'error') {
console.log(`✅ Beatport discovery polling complete for: ${urlHash}`);
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
}
} catch (error) {
console.error('❌ Error polling Beatport discovery:', error);
clearInterval(pollInterval);
delete activeYouTubePollers[urlHash];
}
}, 2000); // Poll every 2 seconds like Tidal
// Store the interval so we can clean it up later
activeYouTubePollers[urlHash] = pollInterval;
}
function showBeatportSubView(viewType) {
// Hide main category view
const mainView = document.getElementById('beatport-main-view');
@ -9785,11 +10072,211 @@ function showBeatportMainView() {
}
}
function handleBeatportChartClick(chartType, chartId) {
console.log(`🎵 Beatport chart clicked: ${chartType} - ${chartId}`);
// ===============================
// BEATPORT CHART FUNCTIONALITY
// ===============================
// Placeholder for Phase 2 - will open discovery modal
showToast(`🎵 Chart "${chartId}" selected - Discovery modal coming in Phase 2!`, 'info');
function createBeatportCard(chartData) {
const state = beatportChartStates[chartData.hash];
const phase = state ? state.phase : 'fresh';
let buttonText = getActionButtonText(phase);
let phaseText = getPhaseText(phase);
let phaseColor = getPhaseColor(phase);
return `
<div class="youtube-playlist-card beatport-chart-card" id="beatport-card-${chartData.hash}">
<div class="playlist-card-icon">🎧</div>
<div class="playlist-card-content">
<div class="playlist-card-name">${escapeHtml(chartData.name)}</div>
<div class="playlist-card-info">
<span class="playlist-card-track-count">${chartData.track_count} tracks</span>
<span class="playlist-card-phase-text" style="color: ${phaseColor};">${phaseText}</span>
</div>
</div>
<div class="playlist-card-progress ${phase === 'fresh' ? 'hidden' : ''}">
<!-- Progress will be dynamically updated based on phase -->
</div>
<button class="playlist-card-action-btn">${buttonText}</button>
</div>
`;
}
function addBeatportCardToContainer(chartData) {
const container = document.getElementById('beatport-playlist-container');
// Remove placeholder if it exists
const placeholder = container.querySelector('.playlist-placeholder');
if (placeholder) {
placeholder.remove();
}
// Check if card already exists
const existingCard = document.getElementById(`beatport-card-${chartData.hash}`);
if (existingCard) {
console.log(`Card already exists for ${chartData.name}, updating instead`);
return;
}
// Create and add the card
const cardHtml = createBeatportCard(chartData);
container.insertAdjacentHTML('beforeend', cardHtml);
// Initialize state
beatportChartStates[chartData.hash] = {
phase: 'fresh',
chart: chartData,
cardElement: document.getElementById(`beatport-card-${chartData.hash}`)
};
// Add click handler
const card = document.getElementById(`beatport-card-${chartData.hash}`);
if (card) {
card.addEventListener('click', () => handleBeatportCardClick(chartData.hash));
}
console.log(`🃏 Created Beatport card: ${chartData.name}`);
}
async function handleBeatportCardClick(chartHash) {
const state = beatportChartStates[chartHash];
if (!state) {
console.error(`❌ [Card Click] No state found for Beatport chart: ${chartHash}`);
showToast('Chart state not found - try refreshing the page', 'error');
return;
}
if (!state.chart) {
console.error(`❌ [Card Click] No chart data found for Beatport chart: ${chartHash}`);
showToast('Chart data missing - try refreshing the page', 'error');
return;
}
console.log(`🎧 [Card Click] Beatport card clicked: ${chartHash}, Phase: ${state.phase}`);
if (state.phase === 'fresh') {
// Open discovery modal and start discovery
openBeatportDiscoveryModal(chartHash, state.chart);
} else if (state.phase === 'discovering' || state.phase === 'discovered' || state.phase === 'syncing' || state.phase === 'sync_complete') {
// Reopen existing modal with preserved discovery results
console.log(`🎧 [Card Click] Opening Beatport discovery modal for ${state.phase} phase`);
openYouTubeDiscoveryModal(chartHash);
} else if (state.phase === 'downloading' || state.phase === 'download_complete') {
// Show download modal
console.log(`📥 [Card Click] Opening Beatport download modal for ${state.phase} phase`);
showToast('Download modal for Beatport coming soon!', 'info');
}
}
function updateBeatportCardPhase(chartHash, phase) {
const state = beatportChartStates[chartHash];
if (!state) return;
state.phase = phase;
// Re-render the card with new phase
const card = document.getElementById(`beatport-card-${chartHash}`);
if (card) {
const newCardHtml = createBeatportCard(state.chart);
card.outerHTML = newCardHtml;
// Re-attach click handler
const newCard = document.getElementById(`beatport-card-${chartHash}`);
if (newCard) {
newCard.addEventListener('click', () => handleBeatportCardClick(chartHash));
state.cardElement = newCard;
}
}
}
function updateBeatportCardProgress(chartHash, progress) {
const state = beatportChartStates[chartHash];
if (!state) return;
const card = document.getElementById(`beatport-card-${chartHash}`);
if (!card) return;
const progressElement = card.querySelector('.playlist-card-progress');
if (!progressElement) return;
const { spotify_total, spotify_matches, failed } = progress;
const percentage = spotify_total > 0 ? Math.round((spotify_matches / spotify_total) * 100) : 0;
progressElement.textContent = `${spotify_total} / ✓ ${spotify_matches} / ✗ ${failed} / ${percentage}%`;
progressElement.classList.remove('hidden');
console.log('🎧 Updated Beatport card progress:', chartHash, `${spotify_matches}/${spotify_total} (${percentage}%)`);
}
function switchToBeatportPlaylistsTab() {
// Switch from "Browse Charts" to "My Playlists" tab
const browseTab = document.querySelector('.beatport-tab-button[data-beatport-tab="browse"]');
const playlistsTab = document.querySelector('.beatport-tab-button[data-beatport-tab="playlists"]');
const browseContent = document.getElementById('beatport-browse-content');
const playlistsContent = document.getElementById('beatport-playlists-content');
if (browseTab && playlistsTab && browseContent && playlistsContent) {
// Update tab buttons
browseTab.classList.remove('active');
playlistsTab.classList.add('active');
// Update tab content
browseContent.classList.remove('active');
playlistsContent.classList.add('active');
console.log('🔄 Switched to Beatport "My Playlists" tab');
}
}
async function handleBeatportChartClick(chartType, chartId, chartName, chartEndpoint) {
console.log(`🎵 Beatport chart clicked: ${chartType} - ${chartId} - ${chartName}`);
try {
// First, create a chart hash for state management
const chartHash = `${chartType}_${chartId}_${Date.now()}`;
// Load chart data from backend using the specific endpoint
console.log(`🔍 Loading ${chartName} tracks from ${chartEndpoint}...`);
showToast(`Loading ${chartName}...`, 'info');
const response = await fetch(`${chartEndpoint}?limit=100`);
if (!response.ok) {
throw new Error(`Failed to fetch ${chartName}: ${response.status}`);
}
const data = await response.json();
if (!data.success || !data.tracks || data.tracks.length === 0) {
throw new Error(`No tracks found in ${chartName}`);
}
// Create chart data object
const chartData = {
hash: chartHash,
name: chartName,
chart_type: chartType,
track_count: data.tracks.length,
tracks: data.tracks.map(track => ({
name: track.title || 'Unknown Title',
artists: [track.artist || 'Unknown Artist'],
album: chartName,
duration_ms: 0,
external_urls: { beatport: track.url || '' },
source: 'beatport'
}))
};
// Add card to container (in background, like YouTube does)
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 ${chartName}`);
} catch (error) {
console.error(`❌ Error handling Beatport chart click:`, error);
showToast(`Error loading ${chartName || chartId}: ${error.message}`, 'error');
}
}
function handleBeatportGenreClick(genreSlug, genreId) {
@ -10249,10 +10736,15 @@ function openYouTubeDiscoveryModal(urlHash) {
startYouTubeDiscoveryPolling(urlHash);
}
} else {
// Create new modal (support both YouTube and Tidal like sync.py)
// Create new modal (support YouTube, Tidal, and Beatport like sync.py)
const isTidal = state.is_tidal_playlist;
const modalTitle = isTidal ? '🎵 Tidal Playlist Discovery' : '🎵 YouTube Playlist Discovery';
const sourceLabel = isTidal ? 'Tidal' : 'YT';
const isBeatport = state.is_beatport_playlist;
const modalTitle = isTidal ? '🎵 Tidal Playlist Discovery' :
isBeatport ? '🎵 Beatport Chart Discovery' :
'🎵 YouTube Playlist Discovery';
const sourceLabel = isTidal ? 'Tidal' :
isBeatport ? 'Beatport' :
'YT';
const modalHtml = `
<div class="modal-overlay" id="youtube-discovery-modal-${urlHash}">
@ -10270,7 +10762,7 @@ function openYouTubeDiscoveryModal(urlHash) {
<div class="progress-bar-container">
<div class="progress-bar-fill" id="youtube-discovery-progress-${urlHash}" style="width: 0%;"></div>
</div>
<div class="progress-text" id="youtube-discovery-progress-text-${urlHash}">${getInitialProgressText(state.phase, isTidal)}</div>
<div class="progress-text" id="youtube-discovery-progress-text-${urlHash}">${getInitialProgressText(state.phase, isTidal, isBeatport)}</div>
</div>
<div class="discovery-table-container">
@ -10283,7 +10775,7 @@ function openYouTubeDiscoveryModal(urlHash) {
<th>Spotify Track</th>
<th>Spotify Artist</th>
<th>Album</th>
${isTidal ? '' : '<th>Duration</th>'}
${(isTidal || isBeatport) ? '' : '<th>Duration</th>'}
</tr>
</thead>
<tbody id="youtube-discovery-table-${urlHash}">
@ -10334,6 +10826,7 @@ function getModalActionButtons(urlHash, phase, state = null) {
}
const isTidal = state && state.is_tidal_playlist;
const isBeatport = state && state.is_beatport_playlist;
// Validate data availability for buttons
const hasDiscoveryResults = state && state.discoveryResults && state.discoveryResults.length > 0;
@ -10449,7 +10942,7 @@ function getModalDescription(phase, isTidal = false) {
}
}
function getInitialProgressText(phase, isTidal = false) {
function getInitialProgressText(phase, isTidal = false, isBeatport = false) {
switch (phase) {
case 'fresh':
return 'Click Start Discovery to begin...';
@ -10464,6 +10957,7 @@ function getInitialProgressText(phase, isTidal = false) {
function generateTableRowsFromState(state, urlHash) {
const isTidal = state.is_tidal_playlist;
const isBeatport = state.is_beatport_playlist;
if (state.discoveryResults && state.discoveryResults.length > 0) {
// Generate rows from existing discovery results
@ -10475,16 +10969,16 @@ function generateTableRowsFromState(state, urlHash) {
<td class="spotify-track">${result.spotify_track || '-'}</td>
<td class="spotify-artist">${result.spotify_artist || '-'}</td>
<td class="spotify-album">${result.spotify_album || '-'}</td>
${isTidal ? '' : `<td class="duration">${result.duration}</td>`}
${(isTidal || isBeatport) ? '' : `<td class="duration">${result.duration}</td>`}
</tr>
`).join('');
} else {
// Generate initial rows from playlist tracks
return generateInitialTableRows(state.playlist.tracks, isTidal, urlHash);
return generateInitialTableRows(state.playlist.tracks, isTidal, urlHash, isBeatport);
}
}
function generateInitialTableRows(tracks, isTidal = false, urlHash = '') {
function generateInitialTableRows(tracks, isTidal = false, urlHash = '', isBeatport = false) {
return tracks.map((track, index) => `
<tr id="discovery-row-${urlHash}-${index}">
<td class="yt-track">${track.name}</td>
@ -10493,7 +10987,7 @@ function generateInitialTableRows(tracks, isTidal = false, urlHash = '') {
<td class="spotify-track">-</td>
<td class="spotify-artist">-</td>
<td class="spotify-album">-</td>
${isTidal ? '' : `<td class="duration">${formatDuration(track.duration_ms)}</td>`}
${(isTidal || isBeatport) ? '' : `<td class="duration">${formatDuration(track.duration_ms)}</td>`}
</tr>
`).join('');
}

View file

@ -4980,7 +4980,7 @@ body {
box-shadow: none;
}
.progress-section {
.sync-sidebar .progress-section {
flex-grow: 1;
overflow: hidden;
display: flex;