From d349754a93933eef2427bc7ac645e653184ed97a Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:31:25 -0700 Subject: [PATCH] Artist Map: cache backfill, constellation effects, related artists, polish - Metadata cache backfill: batch-lookup all node names across all sources to fill missing IDs, images, and genres - Source-aware navigation: View Discography passes correct source to artist page so non-active-source artists load correctly - Constellation hover effect: 800ms delay, fade in/out animation, dim overlay with glowing connection lines to related artists - Click ripple animation on node selection - Related artists list in info modal with clickable navigation - Rich tooltip with artist photo, name, genres on hover - Removed node dragging (caused visual desync with offscreen buffer) - Performance: cached constellation lookups, lighter cache query (no raw_json), canvas.width for proper DPR overlay coverage --- web_server.py | 58 ++++++++ webui/static/script.js | 309 ++++++++++++++++++++++++++++++++++++----- webui/static/style.css | 26 ++++ 3 files changed, 356 insertions(+), 37 deletions(-) diff --git a/web_server.py b/web_server.py index 40196a9a..22ffa0cf 100644 --- a/web_server.py +++ b/web_server.py @@ -41178,6 +41178,64 @@ def get_artist_map_data(): node['type'] = 'watchlist' break + # ── Backfill from metadata cache: batch-lookup all node names across all sources ── + # Single query to get ALL cached artist entries matching ANY node name + try: + all_names = list(set(_norm(n['name']) for n in nodes if n.get('name'))) + if all_names: + # Build case-insensitive IN clause via temp matching + # Lightweight query — no raw_json (can be huge) + cursor.execute(""" + SELECT entity_id, source, name, image_url, genres, popularity + FROM metadata_cache_entities + WHERE entity_type = 'artist' + """) + cache_rows = cursor.fetchall() + + # Index cache by normalized name → {source: {id, image_url, genres}} + cache_by_name = {} + for cr in cache_rows: + cn = _norm(cr['name'] or '') + if cn not in cache_by_name: + cache_by_name[cn] = {} + source = cr['source'] + genres = [] + if cr['genres']: + try: + genres = json.loads(cr['genres']) if isinstance(cr['genres'], str) else [] + except Exception: + pass + cache_by_name[cn][source] = { + 'id': cr['entity_id'], + 'image_url': cr['image_url'] or '', + 'genres': genres, + } + + # Apply cache data to nodes + source_id_map = {'spotify': 'spotify_id', 'itunes': 'itunes_id', 'deezer': 'deezer_id', 'discogs': 'discogs_id'} + for n in nodes: + nn = _norm(n['name']) + cached = cache_by_name.get(nn) + if not cached: + continue + for source, field in source_id_map.items(): + if not n.get(field) and source in cached: + n[field] = cached[source]['id'] + # Backfill image if missing or local path + if not n.get('image_url') or not n['image_url'].startswith('http'): + for source in ('spotify', 'deezer', 'itunes'): + if source in cached and cached[source].get('image_url', '').startswith('http'): + n['image_url'] = cached[source]['image_url'] + break + # Backfill genres if missing + if not n.get('genres') or len(n.get('genres', [])) == 0: + for source in ('spotify', 'deezer', 'itunes', 'discogs'): + if source in cached and cached[source].get('genres'): + n['genres'] = cached[source]['genres'][:5] + break + except Exception as cache_err: + logger.debug(f"Artist map cache backfill error: {cache_err}") + return jsonify({ 'success': True, 'nodes': nodes, diff --git a/webui/static/script.js b/webui/static/script.js index 0f1d071f..b4d57f28 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -55009,6 +55009,7 @@ async function loadYourArtists() { // Store for modal access and render carousel cards window._yaArtists = {}; + window._yaActiveSource = data.active_source || 'spotify'; data.artists.forEach(a => { window._yaArtists[a.id] = a; }); carousel.innerHTML = data.artists.map(a => _renderYourArtistCard(a)).join(''); @@ -55194,6 +55195,36 @@ async function openYourArtistInfoModal(poolId) { } } + // Related artists from map connections + const related = pool._related || []; + if (related.length > 0) { + const relLabel = pool.on_watchlist ? 'Similar Artists' : 'Connected To'; + bodyHTML += `