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:
parent
a7c7d9e6ed
commit
2656927f79
6 changed files with 345 additions and 132 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
217
web_server.py
217
web_server.py
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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()">×</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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue