diff --git a/webui/index.html b/webui/index.html
index fed35624..29aec63a 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -2667,6 +2667,27 @@
+
+
diff --git a/webui/static/artists.js b/webui/static/artists.js
index 7d70c875..0bd23d63 100644
--- a/webui/static/artists.js
+++ b/webui/static/artists.js
@@ -742,325 +742,6 @@ function displayArtistDiscography(discography) {
/**
* Load similar artists from MusicMap
*/
-async function loadSimilarArtists(artistName) {
- if (!artistName) {
- console.warn('â ī¸ No artist name provided for similar artists');
- return;
- }
-
- console.log(`đ Loading similar artists for: ${artistName}`);
-
- // Get DOM elements
- const section = document.getElementById('similar-artists-section');
- const loadingEl = document.getElementById('similar-artists-loading');
- const errorEl = document.getElementById('similar-artists-error');
- const container = document.getElementById('similar-artists-bubbles-container');
-
- if (!section || !loadingEl || !errorEl || !container) {
- console.warn('â ī¸ Similar artists section elements not found');
- return;
- }
-
- // Show loading state
- loadingEl.classList.remove('hidden');
- errorEl.classList.add('hidden');
- container.innerHTML = '';
- section.style.display = 'block';
-
- try {
- // Create new abort controller for this similar artists stream
- similarArtistsController = new AbortController();
-
- // Use streaming endpoint for real-time bubble creation
- const url = `/api/artist/similar/${encodeURIComponent(artistName)}/stream`;
- console.log(`đĄ Streaming from: ${url}`);
-
- const response = await fetch(url, {
- signal: similarArtistsController.signal
- });
-
- if (!response.ok) {
- throw new Error(`Failed to fetch similar artists: ${response.status}`);
- }
-
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
- let artistCount = 0;
-
- // Read the stream
- while (true) {
- const { done, value } = await reader.read();
-
- if (done) {
- console.log('â
Stream complete');
- break;
- }
-
- // Decode the chunk and add to buffer
- buffer += decoder.decode(value, { stream: true });
-
- // Process complete messages (separated by \n\n)
- const messages = buffer.split('\n\n');
- buffer = messages.pop() || ''; // Keep incomplete message in buffer
-
- for (const message of messages) {
- if (!message.trim() || !message.startsWith('data: ')) continue;
-
- try {
- const jsonData = JSON.parse(message.substring(6)); // Remove 'data: ' prefix
-
- if (jsonData.error) {
- throw new Error(jsonData.error);
- }
-
- if (jsonData.artist) {
- // Hide loading on first artist
- if (artistCount === 0) {
- loadingEl.classList.add('hidden');
- }
-
- // Create and append bubble immediately
- const bubble = createSimilarArtistBubble(jsonData.artist);
- container.appendChild(bubble);
- artistCount++;
-
- console.log(`â
Added bubble for: ${jsonData.artist.name} (${artistCount})`);
- }
-
- if (jsonData.complete) {
- console.log(`đ Streaming complete: ${jsonData.total} artists`);
-
- if (artistCount === 0) {
- loadingEl.classList.add('hidden');
- container.innerHTML = `
-
-
đĩ
-
No similar artists found
-
- `;
- } else {
- // Lazy load images for similar artists that don't have them
- lazyLoadSimilarArtistImages(container);
- }
- }
- } catch (parseError) {
- console.error('â Error parsing stream message:', parseError);
- }
- }
- }
-
- // Clear the controller when done
- similarArtistsController = null;
-
- } catch (error) {
- // Don't show error if it was aborted (user navigated away)
- if (error.name === 'AbortError') {
- console.log('âšī¸ Similar artists stream aborted (user navigated to new artist)');
- loadingEl.classList.add('hidden');
- return;
- }
-
- console.error('â Error loading similar artists:', error);
-
- // Hide loading, show error
- loadingEl.classList.add('hidden');
- errorEl.classList.remove('hidden');
-
- // Also show error message in container
- container.innerHTML = `
-
-
â ī¸
-
${error.message}
-
- `;
- } finally {
- // Always clear the controller
- similarArtistsController = null;
- }
-}
-
-/**
- * Lazy load images for similar artist bubbles that don't have images
- */
-async function lazyLoadSimilarArtistImages(container) {
- if (!container) return;
-
- const bubblesNeedingImages = container.querySelectorAll('.similar-artist-bubble[data-needs-image="true"]');
-
- if (bubblesNeedingImages.length === 0) {
- console.log('â
All similar artist bubbles have images');
- return;
- }
-
- console.log(`đŧī¸ Lazy loading images for ${bubblesNeedingImages.length} similar artists`);
-
- // Load images in parallel batches
- const batchSize = 5;
- const bubbles = Array.from(bubblesNeedingImages);
-
- for (let i = 0; i < bubbles.length; i += batchSize) {
- const batch = bubbles.slice(i, i + batchSize);
-
- await Promise.all(batch.map(async (bubble) => {
- const artistId = bubble.getAttribute('data-artist-id');
- const artistSource = bubble.getAttribute('data-artist-source') || '';
- const artistPlugin = bubble.getAttribute('data-artist-plugin') || '';
- if (!artistId) return;
-
- try {
- const params = new URLSearchParams();
- if (artistSource) params.set('source', artistSource);
- if (artistPlugin) params.set('plugin', artistPlugin);
-
- const imageUrl = params.toString()
- ? `/api/artist/${encodeURIComponent(artistId)}/image?${params.toString()}`
- : `/api/artist/${encodeURIComponent(artistId)}/image`;
-
- const response = await fetch(imageUrl);
- const data = await response.json();
-
- if (data.success && data.image_url) {
- const imageContainer = bubble.querySelector('.similar-artist-bubble-image');
- if (imageContainer) {
- const artistName = bubble.querySelector('.similar-artist-bubble-name')?.textContent || 'Artist';
- imageContainer.innerHTML = `

`;
- bubble.setAttribute('data-needs-image', 'false');
- console.log(`â
Loaded image for similar artist ${artistId}`);
- }
- }
- } catch (error) {
- console.warn(`â ī¸ Failed to load image for similar artist ${artistId}:`, error);
- }
- }));
- }
-
- console.log('â
Finished lazy loading similar artist images');
-}
-
-/**
- * Display similar artist bubble cards progressively (one at a time with delay)
- */
-function displaySimilarArtistsProgressively(artists) {
- const container = document.getElementById('similar-artists-bubbles-container');
-
- if (!container) {
- console.warn('â ī¸ Similar artists container not found');
- return;
- }
-
- // Clear container
- container.innerHTML = '';
-
- // Add each bubble with a delay to simulate progressive loading
- artists.forEach((artist, index) => {
- setTimeout(() => {
- const bubble = createSimilarArtistBubble(artist);
- container.appendChild(bubble);
- }, index * 100); // 100ms delay between each bubble
- });
-
- console.log(`â
Displaying ${artists.length} similar artist bubbles progressively`);
-}
-
-/**
- * Display similar artist bubble cards (all at once - legacy)
- */
-function displaySimilarArtists(artists) {
- const container = document.getElementById('similar-artists-bubbles-container');
-
- if (!container) {
- console.warn('â ī¸ Similar artists container not found');
- return;
- }
-
- // Clear container
- container.innerHTML = '';
-
- // Create bubble cards with staggered animation
- artists.forEach((artist, index) => {
- const bubble = createSimilarArtistBubble(artist);
-
- // Add staggered animation delay (50ms per bubble)
- bubble.style.animationDelay = `${index * 0.05}s`;
-
- container.appendChild(bubble);
- });
-
- console.log(`â
Displayed ${artists.length} similar artist bubbles`);
-}
-
-/**
- * Create a similar artist bubble card element
- */
-function createSimilarArtistBubble(artist) {
- // Create bubble container
- const bubble = document.createElement('div');
- bubble.className = 'similar-artist-bubble';
- bubble.setAttribute('data-artist-id', artist.id);
- bubble.setAttribute('data-artist-source', artist.source || '');
- if (artist.plugin) {
- bubble.setAttribute('data-artist-plugin', artist.plugin);
- }
-
- // Track if image needs lazy loading
- const hasImage = artist.image_url && artist.image_url.trim() !== '';
- bubble.setAttribute('data-needs-image', hasImage ? 'false' : 'true');
-
- // Create image container
- const imageContainer = document.createElement('div');
- imageContainer.className = 'similar-artist-bubble-image';
-
- if (hasImage) {
- const img = document.createElement('img');
- img.src = artist.image_url;
- img.alt = artist.name;
-
- // Handle image load error
- img.onerror = () => {
- console.log(`Failed to load image for ${artist.name}`);
- imageContainer.innerHTML = `
đĩ
`;
- bubble.setAttribute('data-needs-image', 'true');
- };
-
- imageContainer.appendChild(img);
- } else {
- // No image - show fallback (will be lazy loaded)
- imageContainer.innerHTML = `
đĩ
`;
- }
-
- // Create name element
- const name = document.createElement('div');
- name.className = 'similar-artist-bubble-name';
- name.textContent = artist.name;
- name.title = artist.name; // Tooltip for full name
-
- // Optional: Create genres element (hidden by default in CSS)
- const genres = document.createElement('div');
- genres.className = 'similar-artist-bubble-genres';
- if (artist.genres && artist.genres.length > 0) {
- genres.textContent = artist.genres.slice(0, 2).join(', ');
- }
-
- // Assemble bubble
- bubble.appendChild(imageContainer);
- bubble.appendChild(name);
- if (artist.genres && artist.genres.length > 0) {
- bubble.appendChild(genres);
- }
-
- // Add click handler to navigate to artist detail page
- bubble.addEventListener('click', () => {
- console.log(`đĩ Clicked similar artist: ${artist.name} (ID: ${artist.id})`);
- // Navigate to this artist's detail page (same as clicking from search results)
- selectArtistForDetail(
- artist,
- artist.source ? { source: artist.source, plugin: artist.plugin } : {}
- );
- });
-
- return bubble;
-}
/**
* Restore cached completion data without re-scanning the database
diff --git a/webui/static/library.js b/webui/static/library.js
index 33e58955..147d52b1 100644
--- a/webui/static/library.js
+++ b/webui/static/library.js
@@ -949,6 +949,11 @@ function populateArtistDetailPage(data) {
console.log(`đ EPs:`, discography.eps);
console.log(`đ Singles:`, discography.singles);
+ // Tag the body so CSS can hide library-only UI for source artists (e.g.
+ // the Enhanced view toggle, the Status filter, completion bars). Set
+ // BEFORE rendering so any layout-dependent code sees the right state.
+ document.body.dataset.artistSource = (artist && artist.server_source) ? 'library' : 'source';
+
// Update hero section with image, name, and stats
updateArtistHeroSection(artist, discography);
@@ -966,6 +971,13 @@ function populateArtistDetailPage(data) {
if (libraryWatchlistBtn && data.spotify_artist && data.spotify_artist.spotify_artist_id) {
initializeLibraryWatchlistButton(data.spotify_artist.spotify_artist_id, data.spotify_artist.spotify_artist_name);
}
+
+ // Load Similar Artists section (works for both library + source artists via
+ // MusicMap name lookup). Fire-and-forget â the function handles its own
+ // loading state and errors.
+ if (artist && artist.name && typeof loadSimilarArtists === 'function') {
+ loadSimilarArtists(artist.name);
+ }
}
function updateArtistDetailImage(imageUrl, artistName) {
diff --git a/webui/static/shared-helpers.js b/webui/static/shared-helpers.js
index d45737f5..d2da2042 100644
--- a/webui/static/shared-helpers.js
+++ b/webui/static/shared-helpers.js
@@ -2759,3 +2759,366 @@ function renderEnrichmentCards(enrichment) {
}
// ===============================
+
+
+// ----------------------------------------------------------------------------
+// Similar Artists â fetch + render via MusicMap. Source-agnostic (artist name
+// based), works for both library and metadata-source artists. Targets DOM IDs
+// #similar-artists-loading, #similar-artists-error, #similar-artists-bubbles-
+// container that live on the artist-detail page.
+// ----------------------------------------------------------------------------
+
+// Similar artists lives on two pages (the inline Artists page and the standalone
+// artist-detail page), each with its own set of IDs so they don't collide in the
+// DOM. This resolver picks the set whose `.page` ancestor is currently active.
+function _resolveSimilarArtistsTargets() {
+ const candidates = [
+ // standalone artist-detail page (scoped ids)
+ { section: 'ad-similar-artists-section', loading: 'ad-similar-artists-loading', error: 'ad-similar-artists-error', bubbles: 'ad-similar-artists-bubbles-container' },
+ // legacy inline Artists page (base ids)
+ { section: 'similar-artists-section', loading: 'similar-artists-loading', error: 'similar-artists-error', bubbles: 'similar-artists-bubbles-container' },
+ ];
+ // Prefer the set whose parent .page is currently active.
+ for (const c of candidates) {
+ const el = document.getElementById(c.section);
+ if (el && el.closest('.page.active')) {
+ return {
+ section: el,
+ loadingEl: document.getElementById(c.loading),
+ errorEl: document.getElementById(c.error),
+ container: document.getElementById(c.bubbles),
+ };
+ }
+ }
+ // Fallback: return the first set that exists at all.
+ for (const c of candidates) {
+ const el = document.getElementById(c.section);
+ if (el) {
+ return {
+ section: el,
+ loadingEl: document.getElementById(c.loading),
+ errorEl: document.getElementById(c.error),
+ container: document.getElementById(c.bubbles),
+ };
+ }
+ }
+ return null;
+}
+
+async function loadSimilarArtists(artistName) {
+ if (!artistName) {
+ console.warn('â ī¸ No artist name provided for similar artists');
+ return;
+ }
+
+ console.log(`đ Loading similar artists for: ${artistName}`);
+
+ const targets = _resolveSimilarArtistsTargets();
+ if (!targets) {
+ console.warn('â ī¸ Similar artists section elements not found on any active page');
+ return;
+ }
+ const { section, loadingEl, errorEl, container } = targets;
+
+ // Show loading state
+ loadingEl.classList.remove('hidden');
+ errorEl.classList.add('hidden');
+ container.innerHTML = '';
+ section.style.display = 'block';
+
+ try {
+ // Create new abort controller for this similar artists stream
+ similarArtistsController = new AbortController();
+
+ // Use streaming endpoint for real-time bubble creation
+ const url = `/api/artist/similar/${encodeURIComponent(artistName)}/stream`;
+ console.log(`đĄ Streaming from: ${url}`);
+
+ const response = await fetch(url, {
+ signal: similarArtistsController.signal
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch similar artists: ${response.status}`);
+ }
+
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+ let artistCount = 0;
+
+ // Read the stream
+ while (true) {
+ const { done, value } = await reader.read();
+
+ if (done) {
+ console.log('â
Stream complete');
+ break;
+ }
+
+ // Decode the chunk and add to buffer
+ buffer += decoder.decode(value, { stream: true });
+
+ // Process complete messages (separated by \n\n)
+ const messages = buffer.split('\n\n');
+ buffer = messages.pop() || ''; // Keep incomplete message in buffer
+
+ for (const message of messages) {
+ if (!message.trim() || !message.startsWith('data: ')) continue;
+
+ try {
+ const jsonData = JSON.parse(message.substring(6)); // Remove 'data: ' prefix
+
+ if (jsonData.error) {
+ throw new Error(jsonData.error);
+ }
+
+ if (jsonData.artist) {
+ // Hide loading on first artist
+ if (artistCount === 0) {
+ loadingEl.classList.add('hidden');
+ }
+
+ // Create and append bubble immediately
+ const bubble = createSimilarArtistBubble(jsonData.artist);
+ container.appendChild(bubble);
+ artistCount++;
+
+ console.log(`â
Added bubble for: ${jsonData.artist.name} (${artistCount})`);
+ }
+
+ if (jsonData.complete) {
+ console.log(`đ Streaming complete: ${jsonData.total} artists`);
+
+ if (artistCount === 0) {
+ loadingEl.classList.add('hidden');
+ container.innerHTML = `
+
+
đĩ
+
No similar artists found
+
+ `;
+ } else {
+ // Lazy load images for similar artists that don't have them
+ lazyLoadSimilarArtistImages(container);
+ }
+ }
+ } catch (parseError) {
+ console.error('â Error parsing stream message:', parseError);
+ }
+ }
+ }
+
+ // Clear the controller when done
+ similarArtistsController = null;
+
+ } catch (error) {
+ // Don't show error if it was aborted (user navigated away)
+ if (error.name === 'AbortError') {
+ console.log('âšī¸ Similar artists stream aborted (user navigated to new artist)');
+ loadingEl.classList.add('hidden');
+ return;
+ }
+
+ console.error('â Error loading similar artists:', error);
+
+ // Hide loading, show error
+ loadingEl.classList.add('hidden');
+ errorEl.classList.remove('hidden');
+
+ // Also show error message in container
+ container.innerHTML = `
+
+
â ī¸
+
${error.message}
+
+ `;
+ } finally {
+ // Always clear the controller
+ similarArtistsController = null;
+ }
+}
+
+/**
+ * Lazy load images for similar artist bubbles that don't have images
+ */
+async function lazyLoadSimilarArtistImages(container) {
+ if (!container) return;
+
+ const bubblesNeedingImages = container.querySelectorAll('.similar-artist-bubble[data-needs-image="true"]');
+
+ if (bubblesNeedingImages.length === 0) {
+ console.log('â
All similar artist bubbles have images');
+ return;
+ }
+
+ console.log(`đŧī¸ Lazy loading images for ${bubblesNeedingImages.length} similar artists`);
+
+ // Load images in parallel batches
+ const batchSize = 5;
+ const bubbles = Array.from(bubblesNeedingImages);
+
+ for (let i = 0; i < bubbles.length; i += batchSize) {
+ const batch = bubbles.slice(i, i + batchSize);
+
+ await Promise.all(batch.map(async (bubble) => {
+ const artistId = bubble.getAttribute('data-artist-id');
+ const artistSource = bubble.getAttribute('data-artist-source') || '';
+ const artistPlugin = bubble.getAttribute('data-artist-plugin') || '';
+ if (!artistId) return;
+
+ try {
+ const params = new URLSearchParams();
+ if (artistSource) params.set('source', artistSource);
+ if (artistPlugin) params.set('plugin', artistPlugin);
+
+ const imageUrl = params.toString()
+ ? `/api/artist/${encodeURIComponent(artistId)}/image?${params.toString()}`
+ : `/api/artist/${encodeURIComponent(artistId)}/image`;
+
+ const response = await fetch(imageUrl);
+ const data = await response.json();
+
+ if (data.success && data.image_url) {
+ const imageContainer = bubble.querySelector('.similar-artist-bubble-image');
+ if (imageContainer) {
+ const artistName = bubble.querySelector('.similar-artist-bubble-name')?.textContent || 'Artist';
+ imageContainer.innerHTML = `

`;
+ bubble.setAttribute('data-needs-image', 'false');
+ console.log(`â
Loaded image for similar artist ${artistId}`);
+ }
+ }
+ } catch (error) {
+ console.warn(`â ī¸ Failed to load image for similar artist ${artistId}:`, error);
+ }
+ }));
+ }
+
+ console.log('â
Finished lazy loading similar artist images');
+}
+
+/**
+ * Display similar artist bubble cards progressively (one at a time with delay)
+ */
+function displaySimilarArtistsProgressively(artists) {
+ const targets = _resolveSimilarArtistsTargets();
+ const container = targets && targets.container;
+
+ if (!container) {
+ console.warn('â ī¸ Similar artists container not found');
+ return;
+ }
+
+ // Clear container
+ container.innerHTML = '';
+
+ // Add each bubble with a delay to simulate progressive loading
+ artists.forEach((artist, index) => {
+ setTimeout(() => {
+ const bubble = createSimilarArtistBubble(artist);
+ container.appendChild(bubble);
+ }, index * 100); // 100ms delay between each bubble
+ });
+
+ console.log(`â
Displaying ${artists.length} similar artist bubbles progressively`);
+}
+
+/**
+ * Display similar artist bubble cards (all at once - legacy)
+ */
+function displaySimilarArtists(artists) {
+ const targets = _resolveSimilarArtistsTargets();
+ const container = targets && targets.container;
+
+ if (!container) {
+ console.warn('â ī¸ Similar artists container not found');
+ return;
+ }
+
+ // Clear container
+ container.innerHTML = '';
+
+ // Create bubble cards with staggered animation
+ artists.forEach((artist, index) => {
+ const bubble = createSimilarArtistBubble(artist);
+
+ // Add staggered animation delay (50ms per bubble)
+ bubble.style.animationDelay = `${index * 0.05}s`;
+
+ container.appendChild(bubble);
+ });
+
+ console.log(`â
Displayed ${artists.length} similar artist bubbles`);
+}
+
+/**
+ * Create a similar artist bubble card element
+ */
+function createSimilarArtistBubble(artist) {
+ // Create bubble container
+ const bubble = document.createElement('div');
+ bubble.className = 'similar-artist-bubble';
+ bubble.setAttribute('data-artist-id', artist.id);
+ bubble.setAttribute('data-artist-source', artist.source || '');
+ if (artist.plugin) {
+ bubble.setAttribute('data-artist-plugin', artist.plugin);
+ }
+
+ // Track if image needs lazy loading
+ const hasImage = artist.image_url && artist.image_url.trim() !== '';
+ bubble.setAttribute('data-needs-image', hasImage ? 'false' : 'true');
+
+ // Create image container
+ const imageContainer = document.createElement('div');
+ imageContainer.className = 'similar-artist-bubble-image';
+
+ if (hasImage) {
+ const img = document.createElement('img');
+ img.src = artist.image_url;
+ img.alt = artist.name;
+
+ // Handle image load error
+ img.onerror = () => {
+ console.log(`Failed to load image for ${artist.name}`);
+ imageContainer.innerHTML = `
đĩ
`;
+ bubble.setAttribute('data-needs-image', 'true');
+ };
+
+ imageContainer.appendChild(img);
+ } else {
+ // No image - show fallback (will be lazy loaded)
+ imageContainer.innerHTML = `
đĩ
`;
+ }
+
+ // Create name element
+ const name = document.createElement('div');
+ name.className = 'similar-artist-bubble-name';
+ name.textContent = artist.name;
+ name.title = artist.name; // Tooltip for full name
+
+ // Optional: Create genres element (hidden by default in CSS)
+ const genres = document.createElement('div');
+ genres.className = 'similar-artist-bubble-genres';
+ if (artist.genres && artist.genres.length > 0) {
+ genres.textContent = artist.genres.slice(0, 2).join(', ');
+ }
+
+ // Assemble bubble
+ bubble.appendChild(imageContainer);
+ bubble.appendChild(name);
+ if (artist.genres && artist.genres.length > 0) {
+ bubble.appendChild(genres);
+ }
+
+ // Add click handler to navigate to artist detail page
+ bubble.addEventListener('click', () => {
+ console.log(`đĩ Clicked similar artist: ${artist.name} (ID: ${artist.id})`);
+ // Navigate to this artist's detail page (same as clicking from search results)
+ selectArtistForDetail(
+ artist,
+ artist.source ? { source: artist.source, plugin: artist.plugin } : {}
+ );
+ });
+
+ return bubble;
+}
diff --git a/webui/static/style.css b/webui/static/style.css
index 7298bc44..6600bcec 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -59377,3 +59377,39 @@ body.reduce-effects *::after {
font-size: 18px;
}
}
+
+
+/* =========================================================================
+ Source-artist visibility rules
+ =========================================================================
+ When the artist-detail page is rendering a source artist (Spotify / Deezer /
+ iTunes / etc. â not in the local library), hide library-only UI. The body
+ data-artist-source attribute is set by library.js populateArtistDetailPage
+ before rendering.
+*/
+
+/* Library-only: status filter (owned/missing only makes sense when you own things) */
+body[data-artist-source="source"] #artist-detail-page .filter-group:has(.filter-label:only-child),
+body[data-artist-source="source"] #artist-detail-page .discography-filter-btn[data-filter="ownership"] {
+ display: none !important;
+}
+
+/* Library-only: Enhanced view toggle + container (per-track editing requires owned tracks) */
+body[data-artist-source="source"] #artist-detail-page .enhanced-view-toggle-btn,
+body[data-artist-source="source"] #artist-detail-page #enhanced-view-container {
+ display: none !important;
+}
+
+/* Library-only: completion bars, enrichment coverage, Top Tracks sidebar, Radio / Enhance Quality buttons */
+body[data-artist-source="source"] #artist-detail-page .collection-overview,
+body[data-artist-source="source"] #artist-detail-page .artist-enrichment-coverage,
+body[data-artist-source="source"] #artist-detail-page #artist-hero-sidebar,
+body[data-artist-source="source"] #artist-detail-page #library-artist-radio-btn,
+body[data-artist-source="source"] #artist-detail-page #library-artist-enhance-btn {
+ display: none !important;
+}
+
+/* Library-only: section-stats "0 owned / 0 missing" counts don't apply */
+body[data-artist-source="source"] #artist-detail-page .section-stats {
+ display: none !important;
+}