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)
|
enriched.append(rich)
|
||||||
if (i + 1) <= 3 or (i + 1) % 25 == 0:
|
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')}")
|
print(f" ✅ [{i+1}/{total}] {rich.get('artist', '?')} - {rich.get('title', '?')} | {rich.get('release_name', 'no release')}")
|
||||||
continue
|
else:
|
||||||
|
enriched.append(track)
|
||||||
enriched.append(track)
|
else:
|
||||||
|
enriched.append(track)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ⚠️ [{i+1}/{total}] Error enriching track: {e}")
|
print(f" ⚠️ [{i+1}/{total}] Error enriching track: {e}")
|
||||||
enriched.append(track)
|
enriched.append(track)
|
||||||
|
|
||||||
# Report progress
|
# Report progress (always runs — success, failure, or exception)
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
track_name = track.get('title', 'Unknown')
|
track_name = track.get('title', 'Unknown')
|
||||||
progress_callback(i + 1, total, track_name)
|
progress_callback(i + 1, total, track_name)
|
||||||
|
|
|
||||||
|
|
@ -612,6 +612,8 @@ class MetadataCache:
|
||||||
return self._extract_itunes_fields(entity_type, raw_data)
|
return self._extract_itunes_fields(entity_type, raw_data)
|
||||||
elif source == 'deezer':
|
elif source == 'deezer':
|
||||||
return self._extract_deezer_fields(entity_type, raw_data)
|
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', '')))}
|
return {'name': str(raw_data.get('name', raw_data.get('trackName', '')))}
|
||||||
|
|
||||||
def _extract_spotify_fields(self, entity_type: str, data: dict) -> dict:
|
def _extract_spotify_fields(self, entity_type: str, data: dict) -> dict:
|
||||||
|
|
@ -780,3 +782,27 @@ class MetadataCache:
|
||||||
fields['external_urls'] = json.dumps(urls)
|
fields['external_urls'] = json.dumps(urls)
|
||||||
|
|
||||||
return fields
|
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()
|
'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):
|
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}")
|
logger.error(f"Error browsing metadata cache: {e}")
|
||||||
return jsonify({"error": str(e)}), 500
|
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):
|
def metadata_cache_entity_detail(source, entity_type, entity_id):
|
||||||
"""Get detailed view of a single cached entity."""
|
"""Get detailed view of a single cached entity."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -35360,14 +35341,31 @@ def extract_beatport_chart_tracks():
|
||||||
"count": 0
|
"count": 0
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
|
enrich = data.get('enrich', True)
|
||||||
|
|
||||||
logger.info(f"🔍 API request to extract tracks from chart: {chart_name}")
|
logger.info(f"🔍 API request to extract tracks from chart: {chart_name}")
|
||||||
logger.info(f"🔗 Chart URL: {chart_url}")
|
logger.info(f"🔗 Chart URL: {chart_url}")
|
||||||
|
|
||||||
# Initialize the Beatport scraper
|
# Initialize the Beatport scraper
|
||||||
scraper = BeatportUnifiedScraper()
|
scraper = BeatportUnifiedScraper()
|
||||||
|
|
||||||
# Extract tracks from the specific chart URL
|
if enrich:
|
||||||
tracks = scraper.extract_tracks_from_chart(chart_url, chart_name, limit)
|
# 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}")
|
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()}")
|
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
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'])
|
@app.route('/api/beatport/enrich-tracks', methods=['POST'])
|
||||||
def enrich_beatport_tracks():
|
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:
|
try:
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
if not data:
|
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})")
|
logger.info(f"🎯 Enriching {len(tracks)} Beatport tracks with per-track metadata (id: {enrichment_id})")
|
||||||
|
|
||||||
# --- Check enrichment cache ---
|
# --- Check enrichment cache (fast, do before spawning background) ---
|
||||||
cached_results = {} # index -> enriched track data
|
cached_results = {}
|
||||||
uncached_tracks = [] # tracks that need scraping
|
uncached_tracks = []
|
||||||
uncached_indices = [] # original indices of uncached tracks
|
uncached_indices = []
|
||||||
|
|
||||||
|
from core.metadata_cache import get_metadata_cache
|
||||||
|
mcache = get_metadata_cache()
|
||||||
|
|
||||||
for i, track in enumerate(tracks):
|
for i, track in enumerate(tracks):
|
||||||
url = track.get('url') or track.get('track_url') or ''
|
url = track.get('url') or track.get('track_url') or ''
|
||||||
if url:
|
if url:
|
||||||
cached = get_cached_enrichment(url)
|
cached = mcache.get_entity('beatport', 'track', url)
|
||||||
if cached:
|
if cached:
|
||||||
cached_results[i] = cached
|
cached_results[i] = cached
|
||||||
continue
|
continue
|
||||||
|
|
@ -36449,77 +36454,125 @@ def enrich_beatport_tracks():
|
||||||
|
|
||||||
cache_hits = len(cached_results)
|
cache_hits = len(cached_results)
|
||||||
cache_misses = len(uncached_tracks)
|
cache_misses = len(uncached_tracks)
|
||||||
cache_size = len(_beatport_enrichment_cache)
|
logger.info(f"📦 Enrichment cache: {cache_hits} hits, {cache_misses} misses")
|
||||||
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]}")
|
|
||||||
|
|
||||||
# Emit instant progress for cached tracks
|
# All cached — return immediately (no background task needed)
|
||||||
if cache_hits > 0:
|
if cache_misses == 0:
|
||||||
socketio.emit('beatport:enrich_progress', {
|
merged = [None] * len(tracks)
|
||||||
'enrichment_id': enrichment_id,
|
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,
|
'completed': cache_hits,
|
||||||
'total': len(tracks),
|
'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 ---
|
def _run_enrichment():
|
||||||
newly_enriched = []
|
try:
|
||||||
if uncached_tracks:
|
def on_progress(completed, total, track_name):
|
||||||
def on_progress(completed, total, track_name):
|
new_completed = cache_hits + completed
|
||||||
socketio.emit('beatport:enrich_progress', {
|
with _enrichment_tasks_lock:
|
||||||
'enrichment_id': enrichment_id,
|
task = _enrichment_tasks.get(enrichment_id)
|
||||||
'completed': cache_hits + completed,
|
if task:
|
||||||
'total': len(tracks),
|
task['completed'] = new_completed
|
||||||
'current_track': track_name
|
task['current_track'] = track_name
|
||||||
})
|
else:
|
||||||
|
logger.warning(f"⚠️ on_progress: task {enrichment_id} not found in _enrichment_tasks!")
|
||||||
|
|
||||||
scraper = BeatportUnifiedScraper()
|
scraper = BeatportUnifiedScraper()
|
||||||
newly_enriched = scraper.enrich_chart_tracks(uncached_tracks, progress_callback=on_progress)
|
newly_enriched = scraper.enrich_chart_tracks(uncached_tracks, progress_callback=on_progress)
|
||||||
|
|
||||||
# --- Apply text cleaning and cache newly enriched tracks ---
|
# Clean and cache
|
||||||
def clean_track(track):
|
for track in newly_enriched:
|
||||||
if track.get('title'):
|
if track.get('title'):
|
||||||
track['title'] = clean_beatport_text(track['title'])
|
track['title'] = clean_beatport_text(track['title'])
|
||||||
if track.get('artist'):
|
if track.get('artist'):
|
||||||
track['artist'] = clean_beatport_text(track['artist'])
|
track['artist'] = clean_beatport_text(track['artist'])
|
||||||
if track.get('release_name'):
|
if track.get('release_name'):
|
||||||
track['release_name'] = clean_beatport_text(track['release_name'])
|
track['release_name'] = clean_beatport_text(track['release_name'])
|
||||||
if track.get('label'):
|
if track.get('label'):
|
||||||
track['label'] = clean_beatport_text(track['label'])
|
track['label'] = clean_beatport_text(track['label'])
|
||||||
return track
|
url = track.get('url') or track.get('track_url') or ''
|
||||||
|
if url:
|
||||||
|
mcache.store_entity('beatport', 'track', url, track)
|
||||||
|
|
||||||
for track in newly_enriched:
|
# Merge in original order
|
||||||
clean_track(track)
|
merged = [None] * len(tracks)
|
||||||
url = track.get('url') or track.get('track_url') or ''
|
for idx, d in cached_results.items():
|
||||||
if url:
|
merged[idx] = d
|
||||||
set_cached_enrichment(url, track)
|
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 ---
|
logger.info(f"✅ Enriched {len(merged)} tracks ({cache_hits} cached, {cache_misses} scraped)")
|
||||||
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]
|
|
||||||
|
|
||||||
# Fill any gaps with original track data (safety)
|
with _enrichment_tasks_lock:
|
||||||
for i in range(len(merged)):
|
task = _enrichment_tasks.get(enrichment_id)
|
||||||
if merged[i] is None:
|
if task:
|
||||||
merged[i] = tracks[i]
|
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:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error enriching tracks: {e}")
|
logger.error(f"❌ Error starting enrichment: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
logger.error(f"Full traceback: {traceback.format_exc()}")
|
logger.error(f"Full traceback: {traceback.format_exc()}")
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
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'])
|
@app.route('/api/beatport/homepage/featured-charts', methods=['GET'])
|
||||||
def get_beatport_homepage_featured_charts():
|
def get_beatport_homepage_featured_charts():
|
||||||
"""Get Beatport Featured Charts from homepage section"""
|
"""Get Beatport Featured Charts from homepage section"""
|
||||||
|
|
|
||||||
|
|
@ -5703,7 +5703,17 @@
|
||||||
<div class="mcache-modal-header">
|
<div class="mcache-modal-header">
|
||||||
<h3 class="mcache-modal-title">Metadata Cache Browser</h3>
|
<h3 class="mcache-modal-title">Metadata Cache Browser</h3>
|
||||||
<div class="mcache-modal-header-actions">
|
<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>
|
<button class="mcache-modal-close" onclick="closeMetadataCacheModal()">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -5720,6 +5730,10 @@
|
||||||
<span class="mcache-stat-pill-label">Deezer</span>
|
<span class="mcache-stat-pill-label">Deezer</span>
|
||||||
<span class="mcache-stat-pill-value" id="mcache-browse-deezer-count">0</span>
|
<span class="mcache-stat-pill-value" id="mcache-browse-deezer-count">0</span>
|
||||||
</div>
|
</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">
|
<div class="mcache-stat-pill">
|
||||||
<span class="mcache-stat-pill-label">Total Hits</span>
|
<span class="mcache-stat-pill-label">Total Hits</span>
|
||||||
<span class="mcache-stat-pill-value" id="mcache-browse-hits">0</span>
|
<span class="mcache-stat-pill-value" id="mcache-browse-hits">0</span>
|
||||||
|
|
@ -5741,6 +5755,7 @@
|
||||||
<option value="spotify">Spotify</option>
|
<option value="spotify">Spotify</option>
|
||||||
<option value="itunes">iTunes</option>
|
<option value="itunes">iTunes</option>
|
||||||
<option value="deezer">Deezer</option>
|
<option value="deezer">Deezer</option>
|
||||||
|
<option value="beatport">Beatport</option>
|
||||||
</select>
|
</select>
|
||||||
<select class="mcache-sort-filter" id="mcache-sort-filter" onchange="loadMetadataCacheBrowse()">
|
<select class="mcache-sort-filter" id="mcache-sort-filter" onchange="loadMetadataCacheBrowse()">
|
||||||
<option value="last_accessed_at">Recently Accessed</option>
|
<option value="last_accessed_at">Recently Accessed</option>
|
||||||
|
|
|
||||||
|
|
@ -18102,9 +18102,9 @@ async function loadMetadataCacheStats() {
|
||||||
const tracksEl = document.getElementById('mcache-stat-tracks');
|
const tracksEl = document.getElementById('mcache-stat-tracks');
|
||||||
const hitsEl = document.getElementById('mcache-stat-hits');
|
const hitsEl = document.getElementById('mcache-stat-hits');
|
||||||
|
|
||||||
if (artistsEl) artistsEl.textContent = (stats.artists?.spotify || 0) + (stats.artists?.itunes || 0) + (stats.artists?.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);
|
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);
|
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;
|
if (hitsEl) hitsEl.textContent = stats.total_hits || 0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silently fail — cache may not be initialized yet
|
// 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 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 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 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-spotify-count', spotifyTotal);
|
||||||
el('mcache-browse-itunes-count', itunesTotal);
|
el('mcache-browse-itunes-count', itunesTotal);
|
||||||
el('mcache-browse-deezer-count', deezerTotal);
|
el('mcache-browse-deezer-count', deezerTotal);
|
||||||
|
el('mcache-browse-beatport-count', beatportTotal);
|
||||||
el('mcache-browse-hits', stats.total_hits || 0);
|
el('mcache-browse-hits', stats.total_hits || 0);
|
||||||
el('mcache-browse-searches', stats.searches || 0);
|
el('mcache-browse-searches', stats.searches || 0);
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
|
|
@ -18959,7 +18961,7 @@ function renderMetadataCacheGrid(items, entityType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
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">
|
<div class="mcache-card-top">
|
||||||
${imageHtml}
|
${imageHtml}
|
||||||
<div class="mcache-card-info">
|
<div class="mcache-card-info">
|
||||||
|
|
@ -19126,8 +19128,26 @@ function closeMetadataCacheDetail() {
|
||||||
if (modal) modal.style.display = 'none';
|
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() {
|
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 {
|
try {
|
||||||
const response = await fetch('/api/metadata-cache/clear', { method: 'DELETE' });
|
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() {
|
function debouncedMetadataCacheSearch() {
|
||||||
if (_mcacheSearchTimeout) clearTimeout(_mcacheSearchTimeout);
|
if (_mcacheSearchTimeout) clearTimeout(_mcacheSearchTimeout);
|
||||||
_mcacheSearchTimeout = setTimeout(() => {
|
_mcacheSearchTimeout = setTimeout(() => {
|
||||||
|
|
@ -26434,12 +26474,12 @@ function setupDJChartItemHandlers() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showToast(`Loading ${chartName}...`, 'info');
|
showToast(`Loading ${chartName}...`, 'info');
|
||||||
showLoadingOverlay(`Loading ${chartName}...`);
|
showLoadingOverlay(`Scraping ${chartName}...`);
|
||||||
|
|
||||||
const response = await fetch('/api/beatport/chart/extract', {
|
const response = await fetch('/api/beatport/chart/extract', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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) {
|
if (!response.ok) {
|
||||||
|
|
@ -26451,9 +26491,11 @@ function setupDJChartItemHandlers() {
|
||||||
throw new Error('No tracks found in chart');
|
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();
|
hideLoadingOverlay();
|
||||||
openBeatportChartAsDownloadModal(data.tracks, chartName, null);
|
openBeatportChartAsDownloadModal(enrichedTracks, chartName, null);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error extracting DJ chart tracks:', error);
|
console.error('❌ Error extracting DJ chart tracks:', error);
|
||||||
|
|
@ -26476,12 +26518,12 @@ function setupFeaturedChartItemHandlers() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showToast(`Loading ${chartName}...`, 'info');
|
showToast(`Loading ${chartName}...`, 'info');
|
||||||
showLoadingOverlay(`Loading ${chartName}...`);
|
showLoadingOverlay(`Scraping ${chartName}...`);
|
||||||
|
|
||||||
const response = await fetch('/api/beatport/chart/extract', {
|
const response = await fetch('/api/beatport/chart/extract', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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) {
|
if (!response.ok) {
|
||||||
|
|
@ -26493,9 +26535,11 @@ function setupFeaturedChartItemHandlers() {
|
||||||
throw new Error('No tracks found in chart');
|
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();
|
hideLoadingOverlay();
|
||||||
openBeatportChartAsDownloadModal(data.tracks, chartName, null);
|
openBeatportChartAsDownloadModal(enrichedTracks, chartName, null);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error extracting Featured chart tracks:', error);
|
console.error('❌ Error extracting Featured chart tracks:', error);
|
||||||
|
|
@ -26521,12 +26565,12 @@ function setupNewChartItemHandlers(genreSlug, genreId, genreName) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showToast(`Loading ${chartName}...`, 'info');
|
showToast(`Loading ${chartName}...`, 'info');
|
||||||
showLoadingOverlay(`Loading ${chartName}...`);
|
showLoadingOverlay(`Scraping ${chartName}...`);
|
||||||
|
|
||||||
const response = await fetch('/api/beatport/chart/extract', {
|
const response = await fetch('/api/beatport/chart/extract', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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) {
|
if (!response.ok) {
|
||||||
|
|
@ -26538,9 +26582,11 @@ function setupNewChartItemHandlers(genreSlug, genreId, genreName) {
|
||||||
throw new Error('No tracks found in chart');
|
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();
|
hideLoadingOverlay();
|
||||||
openBeatportChartAsDownloadModal(data.tracks, fullChartName, null);
|
openBeatportChartAsDownloadModal(enrichedTracks, fullChartName, null);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error loading chart: ${error.message}`);
|
console.error(`❌ Error loading chart: ${error.message}`);
|
||||||
|
|
@ -26706,12 +26752,12 @@ function setupGenreChartItemHandlers(genreSlug, genreId, genreName) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showToast(`Loading ${chartName}...`, 'info');
|
showToast(`Loading ${chartName}...`, 'info');
|
||||||
showLoadingOverlay(`Loading ${chartName}...`);
|
showLoadingOverlay(`Scraping ${chartName}...`);
|
||||||
|
|
||||||
const response = await fetch('/api/beatport/chart/extract', {
|
const response = await fetch('/api/beatport/chart/extract', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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) {
|
if (!response.ok) {
|
||||||
|
|
@ -26723,9 +26769,11 @@ function setupGenreChartItemHandlers(genreSlug, genreId, genreName) {
|
||||||
throw new Error('No tracks found in chart');
|
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();
|
hideLoadingOverlay();
|
||||||
openBeatportChartAsDownloadModal(data.tracks, fullChartName, null);
|
openBeatportChartAsDownloadModal(enrichedTracks, fullChartName, null);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error loading chart: ${error.message}`);
|
console.error(`❌ Error loading chart: ${error.message}`);
|
||||||
|
|
@ -43592,18 +43640,6 @@ let _beatportModalOpening = false;
|
||||||
async function _enrichTracksWithProgress(tracks, chartName) {
|
async function _enrichTracksWithProgress(tracks, chartName) {
|
||||||
const enrichmentId = `enrich_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
|
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 {
|
try {
|
||||||
const resp = await fetch('/api/beatport/enrich-tracks', {
|
const resp = await fetch('/api/beatport/enrich-tracks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -43612,18 +43648,44 @@ async function _enrichTracksWithProgress(tracks, chartName) {
|
||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
|
// Synchronous path — all tracks were cached, results returned inline
|
||||||
if (data.success && data.tracks) {
|
if (data.success && data.tracks) {
|
||||||
return 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');
|
console.warn('⚠️ Enrichment failed, returning original tracks');
|
||||||
return tracks;
|
return tracks;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('⚠️ Failed to enrich tracks:', e);
|
console.warn('⚠️ Failed to enrich tracks:', e);
|
||||||
return tracks;
|
return tracks;
|
||||||
} finally {
|
|
||||||
if (typeof socket !== 'undefined' && socket) {
|
|
||||||
socket.off('beatport:enrich_progress', progressHandler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43717,7 +43779,7 @@ async function handleBeatportChartCardClick(cardElement, chart) {
|
||||||
try {
|
try {
|
||||||
const chartName = `${chart.name} - ${chart.creator}`;
|
const chartName = `${chart.name} - ${chart.creator}`;
|
||||||
showToast(`Loading ${chart.name}...`, 'info');
|
showToast(`Loading ${chart.name}...`, 'info');
|
||||||
showLoadingOverlay(`Getting tracks from ${chart.name}...`);
|
showLoadingOverlay(`Scraping ${chart.name}...`);
|
||||||
|
|
||||||
const response = await fetch('/api/beatport/chart/extract', {
|
const response = await fetch('/api/beatport/chart/extract', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -43725,7 +43787,8 @@ async function handleBeatportChartCardClick(cardElement, chart) {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chart_url: chart.url,
|
chart_url: chart.url,
|
||||||
chart_name: `Featured Chart: ${chart.name}`,
|
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');
|
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();
|
hideLoadingOverlay();
|
||||||
openBeatportChartAsDownloadModal(data.tracks, chartName, chart.image);
|
openBeatportChartAsDownloadModal(enrichedTracks, chartName, chart.image);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error handling chart click for ${chart.name}:`, error);
|
console.error(`❌ Error handling chart click for ${chart.name}:`, error);
|
||||||
|
|
@ -43760,7 +43825,7 @@ async function handleBeatportDJChartCardClick(cardElement, chart) {
|
||||||
try {
|
try {
|
||||||
const chartName = `${chart.name} - ${chart.creator}`;
|
const chartName = `${chart.name} - ${chart.creator}`;
|
||||||
showToast(`Loading ${chart.name}...`, 'info');
|
showToast(`Loading ${chart.name}...`, 'info');
|
||||||
showLoadingOverlay(`Getting tracks from ${chart.name}...`);
|
showLoadingOverlay(`Scraping ${chart.name}...`);
|
||||||
|
|
||||||
const response = await fetch('/api/beatport/chart/extract', {
|
const response = await fetch('/api/beatport/chart/extract', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -43768,7 +43833,8 @@ async function handleBeatportDJChartCardClick(cardElement, chart) {
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
chart_url: chart.url,
|
chart_url: chart.url,
|
||||||
chart_name: `DJ Chart: ${chart.name}`,
|
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');
|
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();
|
hideLoadingOverlay();
|
||||||
openBeatportChartAsDownloadModal(data.tracks, chartName, chart.image);
|
openBeatportChartAsDownloadModal(enrichedTracks, chartName, chart.image);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error handling DJ chart click for ${chart.name}:`, 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);
|
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 {
|
.mcache-modal-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
@ -40442,6 +40482,16 @@ tr.tag-diff-same {
|
||||||
color: #fc3c44;
|
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 {
|
.mcache-card-cache-info {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: rgba(255, 255, 255, 0.25);
|
color: rgba(255, 255, 255, 0.25);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue