Beatport enrichment progress, metadata cache fixes, per-source cache clearing

- Fix enrichment progress never updating: remove `continue` that skipped
  progress_callback for successful tracks in enrich_chart_tracks
- Split chart/extract into two-step flow: extract raw tracks, then enrich
  via polling endpoint with live progress overlay updates
- Move Beatport enrichment cache to persistent metadata cache system
- Fix metadata cache detail modal for Beatport (URL entity_ids with slashes)
- Add per-source Clear dropdown (Spotify/iTunes/Deezer/Beatport) to cache browser
- Remove debug logging from enrichment progress tracking
This commit is contained in:
Broque Thomas 2026-03-18 08:39:02 -07:00
parent a7c7d9e6ed
commit 2656927f79
6 changed files with 345 additions and 132 deletions

View file

@ -1133,15 +1133,16 @@ class BeatportUnifiedScraper:
enriched.append(rich)
if (i + 1) <= 3 or (i + 1) % 25 == 0:
print(f" ✅ [{i+1}/{total}] {rich.get('artist', '?')} - {rich.get('title', '?')} | {rich.get('release_name', 'no release')}")
continue
enriched.append(track)
else:
enriched.append(track)
else:
enriched.append(track)
except Exception as e:
print(f" ⚠️ [{i+1}/{total}] Error enriching track: {e}")
enriched.append(track)
# Report progress
# Report progress (always runs — success, failure, or exception)
if progress_callback:
track_name = track.get('title', 'Unknown')
progress_callback(i + 1, total, track_name)

View file

@ -612,6 +612,8 @@ class MetadataCache:
return self._extract_itunes_fields(entity_type, raw_data)
elif source == 'deezer':
return self._extract_deezer_fields(entity_type, raw_data)
elif source == 'beatport':
return self._extract_beatport_fields(entity_type, raw_data)
return {'name': str(raw_data.get('name', raw_data.get('trackName', '')))}
def _extract_spotify_fields(self, entity_type: str, data: dict) -> dict:
@ -780,3 +782,27 @@ class MetadataCache:
fields['external_urls'] = json.dumps(urls)
return fields
def _extract_beatport_fields(self, entity_type: str, data: dict) -> dict:
"""Extract fields from Beatport enriched track data."""
fields = {}
if entity_type == 'track':
fields['name'] = data.get('title', '')
fields['artist_name'] = data.get('artist', '')
fields['album_name'] = data.get('release_name', '')
fields['album_id'] = data.get('release_id', '')
fields['image_url'] = data.get('release_image', '')
fields['label'] = data.get('label', '')
fields['release_date'] = data.get('release_date', '')
# Beatport duration is in seconds, convert to ms
duration = data.get('duration', 0)
fields['duration_ms'] = int(duration) * 1000 if duration else 0
fields['track_number'] = data.get('position')
fields['genres'] = json.dumps([data['genre']]) if data.get('genre') else '[]'
urls = {}
if data.get('url'):
urls['beatport'] = data['url']
fields['external_urls'] = json.dumps(urls)
return fields

View file

@ -1859,27 +1859,8 @@ beatport_data_cache = {
'cache_lock': threading.Lock()
}
# --- Beatport Enrichment Cache ---
# Cache enriched track data (per-track page visits) to avoid re-scraping on repeat chart clicks.
# Keyed by track URL, expires after 2 hours.
_beatport_enrichment_cache = {}
_beatport_enrichment_lock = threading.Lock()
_BEATPORT_ENRICHMENT_TTL = 7200 # 2 hours in seconds
def get_cached_enrichment(track_url):
"""Return cached enrichment data for a track URL, or None if missing/expired."""
with _beatport_enrichment_lock:
entry = _beatport_enrichment_cache.get(track_url)
if entry and (time.time() - entry['ts']) < _BEATPORT_ENRICHMENT_TTL:
return dict(entry['data']) # shallow copy — prevent callers from mutating cache
elif entry:
del _beatport_enrichment_cache[track_url]
return None
def set_cached_enrichment(track_url, data):
"""Store enrichment data for a track URL."""
with _beatport_enrichment_lock:
_beatport_enrichment_cache[track_url] = {'data': dict(data), 'ts': time.time()}
def get_cached_beatport_data(section_type, data_key, genre_slug=None):
"""
@ -19672,7 +19653,7 @@ def metadata_cache_browse():
logger.error(f"Error browsing metadata cache: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/metadata-cache/entity/<source>/<entity_type>/<entity_id>', methods=['GET'])
@app.route('/api/metadata-cache/entity/<source>/<entity_type>/<path:entity_id>', methods=['GET'])
def metadata_cache_entity_detail(source, entity_type, entity_id):
"""Get detailed view of a single cached entity."""
try:
@ -35360,14 +35341,31 @@ def extract_beatport_chart_tracks():
"count": 0
}), 400
enrich = data.get('enrich', True)
logger.info(f"🔍 API request to extract tracks from chart: {chart_name}")
logger.info(f"🔗 Chart URL: {chart_url}")
# Initialize the Beatport scraper
scraper = BeatportUnifiedScraper()
# Extract tracks from the specific chart URL
tracks = scraper.extract_tracks_from_chart(chart_url, chart_name, limit)
if enrich:
# Full extraction + enrichment (legacy synchronous path)
tracks = scraper.extract_tracks_from_chart(chart_url, chart_name, limit)
else:
# Extract raw track list only (no per-track enrichment)
soup = scraper.get_page(chart_url)
tracks = []
if soup:
tracks = scraper.extract_tracks_from_chart_table(soup, chart_name, limit)
if len(tracks) < 10:
general_tracks = scraper.extract_tracks_from_page(soup, f"New Chart: {chart_name}", limit)
if len(general_tracks) > len(tracks):
tracks = general_tracks
if len(tracks) < 10:
table_tracks = scraper.extract_tracks_from_table_format(soup, chart_name, limit)
if len(table_tracks) > len(tracks):
tracks = table_tracks
logger.info(f"✅ Successfully extracted {len(tracks)} tracks from chart: {chart_name}")
@ -36416,9 +36414,13 @@ def get_beatport_release_metadata():
logger.error(f"Full traceback: {traceback.format_exc()}")
return jsonify({"success": False, "error": str(e)}), 500
# Active enrichment tasks — progress tracked here, polled by frontend
_enrichment_tasks = {} # enrichment_id -> {completed, total, current_track, done, tracks, error}
_enrichment_tasks_lock = threading.Lock()
@app.route('/api/beatport/enrich-tracks', methods=['POST'])
def enrich_beatport_tracks():
"""Enrich basic track data (from DOM extraction) by visiting each track's page"""
"""Start Beatport track enrichment. Returns immediately; poll /enrich-progress for updates."""
try:
data = request.get_json()
if not data:
@ -36432,15 +36434,18 @@ def enrich_beatport_tracks():
logger.info(f"🎯 Enriching {len(tracks)} Beatport tracks with per-track metadata (id: {enrichment_id})")
# --- Check enrichment cache ---
cached_results = {} # index -> enriched track data
uncached_tracks = [] # tracks that need scraping
uncached_indices = [] # original indices of uncached tracks
# --- Check enrichment cache (fast, do before spawning background) ---
cached_results = {}
uncached_tracks = []
uncached_indices = []
from core.metadata_cache import get_metadata_cache
mcache = get_metadata_cache()
for i, track in enumerate(tracks):
url = track.get('url') or track.get('track_url') or ''
if url:
cached = get_cached_enrichment(url)
cached = mcache.get_entity('beatport', 'track', url)
if cached:
cached_results[i] = cached
continue
@ -36449,77 +36454,125 @@ def enrich_beatport_tracks():
cache_hits = len(cached_results)
cache_misses = len(uncached_tracks)
cache_size = len(_beatport_enrichment_cache)
logger.info(f"📦 Enrichment cache: {cache_hits} hits, {cache_misses} misses (cache has {cache_size} entries)")
if cache_misses > 0 and cache_size > 0:
sample_input_url = (uncached_tracks[0].get('url') or uncached_tracks[0].get('track_url') or 'NO_URL')
sample_cached_url = next(iter(_beatport_enrichment_cache), 'EMPTY')
logger.info(f"📦 Cache debug — input URL: {sample_input_url[:80]} | cached URL: {sample_cached_url[:80]}")
logger.info(f"📦 Enrichment cache: {cache_hits} hits, {cache_misses} misses")
# Emit instant progress for cached tracks
if cache_hits > 0:
socketio.emit('beatport:enrich_progress', {
'enrichment_id': enrichment_id,
# All cached — return immediately (no background task needed)
if cache_misses == 0:
merged = [None] * len(tracks)
for idx, d in cached_results.items():
merged[idx] = d
for i in range(len(merged)):
if merged[i] is None:
merged[i] = tracks[i]
return jsonify({"success": True, "tracks": merged})
# --- Initialize progress tracker and start background task ---
with _enrichment_tasks_lock:
_enrichment_tasks[enrichment_id] = {
'completed': cache_hits,
'total': len(tracks),
'current_track': f'{cache_hits} tracks (cached)'
})
'current_track': f'{cache_hits} tracks (cached)' if cache_hits > 0 else '',
'done': False,
'tracks': None,
'error': None,
}
# --- Enrich uncached tracks ---
newly_enriched = []
if uncached_tracks:
def on_progress(completed, total, track_name):
socketio.emit('beatport:enrich_progress', {
'enrichment_id': enrichment_id,
'completed': cache_hits + completed,
'total': len(tracks),
'current_track': track_name
})
def _run_enrichment():
try:
def on_progress(completed, total, track_name):
new_completed = cache_hits + completed
with _enrichment_tasks_lock:
task = _enrichment_tasks.get(enrichment_id)
if task:
task['completed'] = new_completed
task['current_track'] = track_name
else:
logger.warning(f"⚠️ on_progress: task {enrichment_id} not found in _enrichment_tasks!")
scraper = BeatportUnifiedScraper()
newly_enriched = scraper.enrich_chart_tracks(uncached_tracks, progress_callback=on_progress)
scraper = BeatportUnifiedScraper()
newly_enriched = scraper.enrich_chart_tracks(uncached_tracks, progress_callback=on_progress)
# --- Apply text cleaning and cache newly enriched tracks ---
def clean_track(track):
if track.get('title'):
track['title'] = clean_beatport_text(track['title'])
if track.get('artist'):
track['artist'] = clean_beatport_text(track['artist'])
if track.get('release_name'):
track['release_name'] = clean_beatport_text(track['release_name'])
if track.get('label'):
track['label'] = clean_beatport_text(track['label'])
return track
# Clean and cache
for track in newly_enriched:
if track.get('title'):
track['title'] = clean_beatport_text(track['title'])
if track.get('artist'):
track['artist'] = clean_beatport_text(track['artist'])
if track.get('release_name'):
track['release_name'] = clean_beatport_text(track['release_name'])
if track.get('label'):
track['label'] = clean_beatport_text(track['label'])
url = track.get('url') or track.get('track_url') or ''
if url:
mcache.store_entity('beatport', 'track', url, track)
for track in newly_enriched:
clean_track(track)
url = track.get('url') or track.get('track_url') or ''
if url:
set_cached_enrichment(url, track)
# Merge in original order
merged = [None] * len(tracks)
for idx, d in cached_results.items():
merged[idx] = d
for j, idx in enumerate(uncached_indices):
if j < len(newly_enriched):
merged[idx] = newly_enriched[j]
for i in range(len(merged)):
if merged[i] is None:
merged[i] = tracks[i]
# --- Merge results in original order ---
merged = [None] * len(tracks)
for idx, data in cached_results.items():
merged[idx] = data
for j, idx in enumerate(uncached_indices):
if j < len(newly_enriched):
merged[idx] = newly_enriched[j]
logger.info(f"✅ Enriched {len(merged)} tracks ({cache_hits} cached, {cache_misses} scraped)")
# Fill any gaps with original track data (safety)
for i in range(len(merged)):
if merged[i] is None:
merged[i] = tracks[i]
with _enrichment_tasks_lock:
task = _enrichment_tasks.get(enrichment_id)
if task:
task['done'] = True
task['tracks'] = merged
task['completed'] = len(tracks)
logger.info(f"✅ Enriched {len(merged)} tracks ({cache_hits} cached, {cache_misses} scraped)")
except Exception as e:
logger.error(f"❌ Error enriching tracks: {e}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
with _enrichment_tasks_lock:
task = _enrichment_tasks.get(enrichment_id)
if task:
task['done'] = True
task['error'] = str(e)
task['tracks'] = tracks # Return originals as fallback
return jsonify({"success": True, "tracks": merged})
threading.Thread(target=_run_enrichment, daemon=True).start()
return jsonify({"success": True, "enrichment_id": enrichment_id, "async": True})
except Exception as e:
logger.error(f"❌ Error enriching tracks: {e}")
logger.error(f"❌ Error starting enrichment: {e}")
import traceback
logger.error(f"Full traceback: {traceback.format_exc()}")
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/beatport/enrich-progress/<enrichment_id>', methods=['GET'])
def get_enrichment_progress(enrichment_id):
"""Poll enrichment progress. Returns current state; includes tracks when done."""
with _enrichment_tasks_lock:
task = _enrichment_tasks.get(enrichment_id)
if not task:
return jsonify({"success": False, "error": "Unknown enrichment ID"}), 404
result = {
'success': True,
'completed': task['completed'],
'total': task['total'],
'current_track': task['current_track'],
'done': task['done'],
}
if task['done']:
result['tracks'] = task['tracks']
result['error'] = task['error']
# Clean up — task is finished
del _enrichment_tasks[enrichment_id]
resp = jsonify(result)
resp.headers['Cache-Control'] = 'no-store'
return resp
@app.route('/api/beatport/homepage/featured-charts', methods=['GET'])
def get_beatport_homepage_featured_charts():
"""Get Beatport Featured Charts from homepage section"""

View file

@ -5703,7 +5703,17 @@
<div class="mcache-modal-header">
<h3 class="mcache-modal-title">Metadata Cache Browser</h3>
<div class="mcache-modal-header-actions">
<button class="mcache-btn-clear" onclick="clearMetadataCache()" title="Clear all cached data">Clear</button>
<div class="mcache-clear-dropdown" id="mcache-clear-dropdown">
<button class="mcache-btn-clear" onclick="toggleMcacheClearDropdown(event)" title="Clear cached data">Clear ▾</button>
<div class="mcache-clear-dropdown-menu" id="mcache-clear-dropdown-menu">
<button onclick="clearMetadataCacheBySource('spotify')"><span class="mcache-source-badge spotify" style="margin-right:6px">spotify</span>Clear Spotify</button>
<button onclick="clearMetadataCacheBySource('itunes')"><span class="mcache-source-badge itunes" style="margin-right:6px">itunes</span>Clear iTunes</button>
<button onclick="clearMetadataCacheBySource('deezer')"><span class="mcache-source-badge deezer" style="margin-right:6px">deezer</span>Clear Deezer</button>
<button onclick="clearMetadataCacheBySource('beatport')"><span class="mcache-source-badge beatport" style="margin-right:6px">beatport</span>Clear Beatport</button>
<div style="border-top:1px solid rgba(255,255,255,0.08);margin:4px 0"></div>
<button onclick="clearMetadataCache()">Clear All</button>
</div>
</div>
<button class="mcache-modal-close" onclick="closeMetadataCacheModal()">&times;</button>
</div>
</div>
@ -5720,6 +5730,10 @@
<span class="mcache-stat-pill-label">Deezer</span>
<span class="mcache-stat-pill-value" id="mcache-browse-deezer-count">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">Beatport</span>
<span class="mcache-stat-pill-value" id="mcache-browse-beatport-count">0</span>
</div>
<div class="mcache-stat-pill">
<span class="mcache-stat-pill-label">Total Hits</span>
<span class="mcache-stat-pill-value" id="mcache-browse-hits">0</span>
@ -5741,6 +5755,7 @@
<option value="spotify">Spotify</option>
<option value="itunes">iTunes</option>
<option value="deezer">Deezer</option>
<option value="beatport">Beatport</option>
</select>
<select class="mcache-sort-filter" id="mcache-sort-filter" onchange="loadMetadataCacheBrowse()">
<option value="last_accessed_at">Recently Accessed</option>

View file

@ -18102,9 +18102,9 @@ async function loadMetadataCacheStats() {
const tracksEl = document.getElementById('mcache-stat-tracks');
const hitsEl = document.getElementById('mcache-stat-hits');
if (artistsEl) artistsEl.textContent = (stats.artists?.spotify || 0) + (stats.artists?.itunes || 0) + (stats.artists?.deezer || 0);
if (albumsEl) albumsEl.textContent = (stats.albums?.spotify || 0) + (stats.albums?.itunes || 0) + (stats.albums?.deezer || 0);
if (tracksEl) tracksEl.textContent = (stats.tracks?.spotify || 0) + (stats.tracks?.itunes || 0) + (stats.tracks?.deezer || 0);
if (artistsEl) artistsEl.textContent = (stats.artists?.spotify || 0) + (stats.artists?.itunes || 0) + (stats.artists?.deezer || 0) + (stats.artists?.beatport || 0);
if (albumsEl) albumsEl.textContent = (stats.albums?.spotify || 0) + (stats.albums?.itunes || 0) + (stats.albums?.deezer || 0) + (stats.albums?.beatport || 0);
if (tracksEl) tracksEl.textContent = (stats.tracks?.spotify || 0) + (stats.tracks?.itunes || 0) + (stats.tracks?.deezer || 0) + (stats.tracks?.beatport || 0);
if (hitsEl) hitsEl.textContent = stats.total_hits || 0;
} catch (e) {
// Silently fail — cache may not be initialized yet
@ -18856,9 +18856,11 @@ async function loadMetadataCacheBrowseStats() {
const spotifyTotal = (stats.artists?.spotify || 0) + (stats.albums?.spotify || 0) + (stats.tracks?.spotify || 0);
const itunesTotal = (stats.artists?.itunes || 0) + (stats.albums?.itunes || 0) + (stats.tracks?.itunes || 0);
const deezerTotal = (stats.artists?.deezer || 0) + (stats.albums?.deezer || 0) + (stats.tracks?.deezer || 0);
const beatportTotal = (stats.artists?.beatport || 0) + (stats.albums?.beatport || 0) + (stats.tracks?.beatport || 0);
el('mcache-browse-spotify-count', spotifyTotal);
el('mcache-browse-itunes-count', itunesTotal);
el('mcache-browse-deezer-count', deezerTotal);
el('mcache-browse-beatport-count', beatportTotal);
el('mcache-browse-hits', stats.total_hits || 0);
el('mcache-browse-searches', stats.searches || 0);
} catch (e) { /* ignore */ }
@ -18959,7 +18961,7 @@ function renderMetadataCacheGrid(items, entityType) {
}
return `
<div class="mcache-card" onclick="openMetadataCacheDetail('${source}', '${entityType}', '${item.entity_id}')">
<div class="mcache-card" onclick="openMetadataCacheDetail('${source}', '${entityType}', '${encodeURIComponent(item.entity_id)}')">
<div class="mcache-card-top">
${imageHtml}
<div class="mcache-card-info">
@ -19126,8 +19128,26 @@ function closeMetadataCacheDetail() {
if (modal) modal.style.display = 'none';
}
function toggleMcacheClearDropdown(event) {
event.stopPropagation();
const menu = document.getElementById('mcache-clear-dropdown-menu');
if (!menu) return;
const isOpen = menu.style.display === 'block';
menu.style.display = isOpen ? 'none' : 'block';
if (!isOpen) {
const closeHandler = (e) => {
if (!e.target.closest('#mcache-clear-dropdown')) {
menu.style.display = 'none';
document.removeEventListener('click', closeHandler);
}
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
}
}
async function clearMetadataCache() {
if (!confirm('Clear all cached metadata? This will remove all cached API responses from Spotify and iTunes.')) return;
if (!confirm('Clear ALL cached metadata? This removes all cached API responses.')) return;
document.getElementById('mcache-clear-dropdown-menu').style.display = 'none';
try {
const response = await fetch('/api/metadata-cache/clear', { method: 'DELETE' });
@ -19145,6 +19165,26 @@ async function clearMetadataCache() {
}
}
async function clearMetadataCacheBySource(source) {
if (!confirm(`Clear all ${source} cached metadata?`)) return;
document.getElementById('mcache-clear-dropdown-menu').style.display = 'none';
try {
const response = await fetch(`/api/metadata-cache/clear?source=${source}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
showToast(`Cleared ${data.cleared} ${source} cache entries`, 'success');
loadMetadataCacheBrowseStats();
loadMetadataCacheBrowse();
loadMetadataCacheStats();
} else {
showToast(`Failed to clear ${source} cache`, 'error');
}
} catch (e) {
showToast(`Error clearing ${source} cache`, 'error');
}
}
function debouncedMetadataCacheSearch() {
if (_mcacheSearchTimeout) clearTimeout(_mcacheSearchTimeout);
_mcacheSearchTimeout = setTimeout(() => {
@ -26434,12 +26474,12 @@ function setupDJChartItemHandlers() {
try {
showToast(`Loading ${chartName}...`, 'info');
showLoadingOverlay(`Loading ${chartName}...`);
showLoadingOverlay(`Scraping ${chartName}...`);
const response = await fetch('/api/beatport/chart/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chart_url: chartUrl, chart_name: chartName, limit: 100 })
body: JSON.stringify({ chart_url: chartUrl, chart_name: chartName, limit: 100, enrich: false })
});
if (!response.ok) {
@ -26451,9 +26491,11 @@ function setupDJChartItemHandlers() {
throw new Error('No tracks found in chart');
}
console.log(`✅ Extracted ${data.tracks.length} tracks from DJ chart: ${chartName}`);
console.log(`✅ Extracted ${data.tracks.length} raw tracks from DJ chart, enriching...`);
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, chartName);
hideLoadingOverlay();
openBeatportChartAsDownloadModal(data.tracks, chartName, null);
openBeatportChartAsDownloadModal(enrichedTracks, chartName, null);
} catch (error) {
console.error('❌ Error extracting DJ chart tracks:', error);
@ -26476,12 +26518,12 @@ function setupFeaturedChartItemHandlers() {
try {
showToast(`Loading ${chartName}...`, 'info');
showLoadingOverlay(`Loading ${chartName}...`);
showLoadingOverlay(`Scraping ${chartName}...`);
const response = await fetch('/api/beatport/chart/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chart_url: chartUrl, chart_name: chartName, limit: 100 })
body: JSON.stringify({ chart_url: chartUrl, chart_name: chartName, limit: 100, enrich: false })
});
if (!response.ok) {
@ -26493,9 +26535,11 @@ function setupFeaturedChartItemHandlers() {
throw new Error('No tracks found in chart');
}
console.log(`✅ Extracted ${data.tracks.length} tracks from Featured chart: ${chartName}`);
console.log(`✅ Extracted ${data.tracks.length} raw tracks from Featured chart, enriching...`);
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, chartName);
hideLoadingOverlay();
openBeatportChartAsDownloadModal(data.tracks, chartName, null);
openBeatportChartAsDownloadModal(enrichedTracks, chartName, null);
} catch (error) {
console.error('❌ Error extracting Featured chart tracks:', error);
@ -26521,12 +26565,12 @@ function setupNewChartItemHandlers(genreSlug, genreId, genreName) {
try {
showToast(`Loading ${chartName}...`, 'info');
showLoadingOverlay(`Loading ${chartName}...`);
showLoadingOverlay(`Scraping ${chartName}...`);
const response = await fetch('/api/beatport/chart/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chart_url: chartUrl, chart_name: chartName, limit: 100 })
body: JSON.stringify({ chart_url: chartUrl, chart_name: chartName, limit: 100, enrich: false })
});
if (!response.ok) {
@ -26538,9 +26582,11 @@ function setupNewChartItemHandlers(genreSlug, genreId, genreName) {
throw new Error('No tracks found in chart');
}
console.log(`✅ Extracted ${data.tracks.length} tracks from ${fullChartName}`);
console.log(`✅ Extracted ${data.tracks.length} raw tracks from ${fullChartName}, enriching...`);
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, fullChartName);
hideLoadingOverlay();
openBeatportChartAsDownloadModal(data.tracks, fullChartName, null);
openBeatportChartAsDownloadModal(enrichedTracks, fullChartName, null);
} catch (error) {
console.error(`❌ Error loading chart: ${error.message}`);
@ -26706,12 +26752,12 @@ function setupGenreChartItemHandlers(genreSlug, genreId, genreName) {
try {
showToast(`Loading ${chartName}...`, 'info');
showLoadingOverlay(`Loading ${chartName}...`);
showLoadingOverlay(`Scraping ${chartName}...`);
const response = await fetch('/api/beatport/chart/extract', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chart_url: chartUrl, chart_name: chartName, limit: 100 })
body: JSON.stringify({ chart_url: chartUrl, chart_name: chartName, limit: 100, enrich: false })
});
if (!response.ok) {
@ -26723,9 +26769,11 @@ function setupGenreChartItemHandlers(genreSlug, genreId, genreName) {
throw new Error('No tracks found in chart');
}
console.log(`✅ Extracted ${data.tracks.length} tracks from ${fullChartName}`);
console.log(`✅ Extracted ${data.tracks.length} raw tracks from ${fullChartName}, enriching...`);
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, fullChartName);
hideLoadingOverlay();
openBeatportChartAsDownloadModal(data.tracks, fullChartName, null);
openBeatportChartAsDownloadModal(enrichedTracks, fullChartName, null);
} catch (error) {
console.error(`❌ Error loading chart: ${error.message}`);
@ -43592,18 +43640,6 @@ let _beatportModalOpening = false;
async function _enrichTracksWithProgress(tracks, chartName) {
const enrichmentId = `enrich_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
// Listen for WebSocket progress updates to drive the loading overlay
const progressHandler = (data) => {
if (data.enrichment_id !== enrichmentId) return;
const overlayText = document.querySelector('#loading-overlay .loading-message');
if (overlayText) {
overlayText.textContent = `Fetching track metadata... (${data.completed}/${data.total}) ${data.current_track || ''}`;
}
};
if (typeof socket !== 'undefined' && socket) {
socket.on('beatport:enrich_progress', progressHandler);
}
try {
const resp = await fetch('/api/beatport/enrich-tracks', {
method: 'POST',
@ -43612,18 +43648,44 @@ async function _enrichTracksWithProgress(tracks, chartName) {
});
const data = await resp.json();
// Synchronous path — all tracks were cached, results returned inline
if (data.success && data.tracks) {
return data.tracks;
}
// Async path — poll for progress until done
if (data.success && data.async) {
while (true) {
await new Promise(r => setTimeout(r, 800));
try {
const progressResp = await fetch(`/api/beatport/enrich-progress/${enrichmentId}?_=${Date.now()}`);
const progress = await progressResp.json();
if (!progress.success) break;
// Update loading overlay with live progress
const overlayText = document.querySelector('#loading-overlay .loading-message');
if (overlayText) {
overlayText.textContent = `Fetching track metadata... (${progress.completed}/${progress.total}) ${progress.current_track || ''}`;
}
if (progress.done) {
if (progress.tracks) {
return progress.tracks;
}
console.warn('⚠️ Async enrichment failed:', progress.error);
return tracks;
}
} catch (pollErr) {
console.warn('⚠️ Progress poll error:', pollErr);
}
}
}
console.warn('⚠️ Enrichment failed, returning original tracks');
return tracks;
} catch (e) {
console.warn('⚠️ Failed to enrich tracks:', e);
return tracks;
} finally {
if (typeof socket !== 'undefined' && socket) {
socket.off('beatport:enrich_progress', progressHandler);
}
}
}
@ -43717,7 +43779,7 @@ async function handleBeatportChartCardClick(cardElement, chart) {
try {
const chartName = `${chart.name} - ${chart.creator}`;
showToast(`Loading ${chart.name}...`, 'info');
showLoadingOverlay(`Getting tracks from ${chart.name}...`);
showLoadingOverlay(`Scraping ${chart.name}...`);
const response = await fetch('/api/beatport/chart/extract', {
method: 'POST',
@ -43725,7 +43787,8 @@ async function handleBeatportChartCardClick(cardElement, chart) {
body: JSON.stringify({
chart_url: chart.url,
chart_name: `Featured Chart: ${chart.name}`,
limit: 100
limit: 100,
enrich: false
})
});
@ -43735,9 +43798,11 @@ async function handleBeatportChartCardClick(cardElement, chart) {
throw new Error('No tracks found in this chart');
}
console.log(`✅ Fetched ${data.tracks.length} tracks from ${chart.name}`);
console.log(`✅ Fetched ${data.tracks.length} raw tracks from ${chart.name}, enriching...`);
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, chartName);
hideLoadingOverlay();
openBeatportChartAsDownloadModal(data.tracks, chartName, chart.image);
openBeatportChartAsDownloadModal(enrichedTracks, chartName, chart.image);
} catch (error) {
console.error(`❌ Error handling chart click for ${chart.name}:`, error);
@ -43760,7 +43825,7 @@ async function handleBeatportDJChartCardClick(cardElement, chart) {
try {
const chartName = `${chart.name} - ${chart.creator}`;
showToast(`Loading ${chart.name}...`, 'info');
showLoadingOverlay(`Getting tracks from ${chart.name}...`);
showLoadingOverlay(`Scraping ${chart.name}...`);
const response = await fetch('/api/beatport/chart/extract', {
method: 'POST',
@ -43768,7 +43833,8 @@ async function handleBeatportDJChartCardClick(cardElement, chart) {
body: JSON.stringify({
chart_url: chart.url,
chart_name: `DJ Chart: ${chart.name}`,
limit: 100
limit: 100,
enrich: false
})
});
@ -43778,9 +43844,11 @@ async function handleBeatportDJChartCardClick(cardElement, chart) {
throw new Error('No tracks found in this DJ chart');
}
console.log(`✅ Fetched ${data.tracks.length} tracks from ${chart.name}`);
console.log(`✅ Fetched ${data.tracks.length} raw tracks from ${chart.name}, enriching...`);
const enrichedTracks = await _enrichTracksWithProgress(data.tracks, chartName);
hideLoadingOverlay();
openBeatportChartAsDownloadModal(data.tracks, chartName, chart.image);
openBeatportChartAsDownloadModal(enrichedTracks, chartName, chart.image);
} catch (error) {
console.error(`❌ Error handling DJ chart click for ${chart.name}:`, error);

View file

@ -40180,6 +40180,46 @@ tr.tag-diff-same {
border-color: rgba(239, 68, 68, 0.35);
}
.mcache-clear-dropdown {
position: relative;
display: inline-block;
}
.mcache-clear-dropdown-menu {
display: none;
position: absolute;
top: 100%;
right: 0;
margin-top: 4px;
background: #1e1e2e;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 4px;
min-width: 180px;
z-index: 1000;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.mcache-clear-dropdown-menu button {
display: flex;
align-items: center;
width: 100%;
padding: 8px 12px;
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
cursor: pointer;
border-radius: 6px;
transition: background 0.15s;
white-space: nowrap;
}
.mcache-clear-dropdown-menu button:hover {
background: rgba(255, 255, 255, 0.06);
color: #fff;
}
.mcache-modal-close {
background: none;
border: none;
@ -40442,6 +40482,16 @@ tr.tag-diff-same {
color: #fc3c44;
}
.mcache-source-badge.deezer {
background: rgba(160, 55, 255, 0.15);
color: #a037ff;
}
.mcache-source-badge.beatport {
background: rgba(0, 210, 120, 0.15);
color: #00d278;
}
.mcache-card-cache-info {
font-size: 10px;
color: rgba(255, 255, 255, 0.25);