beatport progress
This commit is contained in:
parent
0ba4b6a079
commit
f5ae3d611c
3 changed files with 898 additions and 16 deletions
388
web_server.py
388
web_server.py
|
|
@ -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"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4980,7 +4980,7 @@ body {
|
|||
box-shadow: none;
|
||||
}
|
||||
|
||||
.progress-section {
|
||||
.sync-sidebar .progress-section {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue