diff --git a/webui/index.html b/webui/index.html
index 3f2b1624..52788f0f 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -2061,6 +2061,30 @@
+
+
+
+ Show
+
+
+
+
+
+
+ Include
+
+
+
+
+
+
+ Status
+
+
+
+
+
+
diff --git a/webui/static/script.js b/webui/static/script.js
index 8a1d32c6..4de18d1d 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -27085,6 +27085,13 @@ let artistDetailPageState = {
currentArtistName: null
};
+// Discography filter state
+let discographyFilterState = {
+ categories: { albums: true, eps: true, singles: true },
+ content: { live: true, compilations: true, featured: true },
+ ownership: 'all' // 'all', 'owned', 'missing'
+};
+
function navigateToArtistDetail(artistId, artistName) {
console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId})`);
@@ -27140,6 +27147,9 @@ function initializeArtistDetailPage() {
});
}
+ // Initialize discography filter buttons
+ initializeDiscographyFilters();
+
artistDetailPageState.isInitialized = true;
console.log("✅ Artist Detail page initialized successfully");
}
@@ -27147,6 +27157,9 @@ function initializeArtistDetailPage() {
async function loadArtistDetailData(artistId, artistName) {
console.log(`🔄 Loading artist detail data for: ${artistName} (ID: ${artistId})`);
+ // Reset discography filters to defaults
+ resetDiscographyFilters();
+
// Show loading state and hide all content
showArtistDetailLoading(true);
showArtistDetailError(false);
@@ -27491,6 +27504,9 @@ function populateDiscographySections(discography) {
// Populate singles
populateReleaseSection('singles', discography.singles);
+
+ // Apply any active filters after populating
+ applyDiscographyFilters();
}
function populateReleaseSection(sectionType, releases) {
@@ -27540,6 +27556,18 @@ function createReleaseCard(release) {
// Store mutable reference so stream updates propagate to click handler
card._releaseData = release;
+ // Tag card for content-type filtering
+ const titleLower = (release.title || '').toLowerCase();
+ const livePattern = /\b(live)\b|\(live[^)]*\)|\[live[^]]*\]/i;
+ const compilationPattern = /\b(greatest hits|best of|collection|anthology|essential)\b/i;
+ const featuredPattern = /\(?\bfeat\.?\s|\bft\.?\s|\bfeaturing\b/i;
+ const isLive = livePattern.test(release.title || '') || (release.album_type === 'compilation' && livePattern.test(release.title || ''));
+ const isCompilation = (release.album_type === 'compilation') || compilationPattern.test(release.title || '');
+ const isFeatured = featuredPattern.test(release.title || '');
+ card.setAttribute("data-is-live", isLive ? "true" : "false");
+ card.setAttribute("data-is-compilation", isCompilation ? "true" : "false");
+ card.setAttribute("data-is-featured", isFeatured ? "true" : "false");
+
// Add MusicBrainz icon if available
let mbIcon = null;
if (release.musicbrainz_release_id) {
@@ -27969,6 +27997,9 @@ function updateLibraryReleaseCard(data) {
formatsDiv.innerHTML = data.formats.map(f => `${f}`).join('');
card.appendChild(formatsDiv);
}
+
+ // Re-apply filters so newly resolved cards respect active filters
+ applyDiscographyFilters();
}
function updateCategoryStatsFromStream(category, ownedCount, missingCount) {
@@ -28066,6 +28097,125 @@ function recalculateSummaryStats(artistFormatSet) {
}
}
+// ===============================================
+// Discography Filter Functions
+// ===============================================
+
+function initializeDiscographyFilters() {
+ const container = document.getElementById('discography-filters');
+ if (!container) return;
+
+ container.addEventListener('click', (e) => {
+ const btn = e.target.closest('.discography-filter-btn');
+ if (!btn) return;
+
+ const filterType = btn.dataset.filter;
+ const value = btn.dataset.value;
+
+ if (filterType === 'category') {
+ // Multi-toggle: toggle this category on/off
+ btn.classList.toggle('active');
+ discographyFilterState.categories[value] = btn.classList.contains('active');
+ } else if (filterType === 'content') {
+ // Multi-toggle: toggle this content type on/off
+ btn.classList.toggle('active');
+ discographyFilterState.content[value] = btn.classList.contains('active');
+ } else if (filterType === 'ownership') {
+ // Single-select: deactivate siblings, activate this one
+ container.querySelectorAll('[data-filter="ownership"]').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ discographyFilterState.ownership = value;
+ }
+
+ applyDiscographyFilters();
+ });
+}
+
+function resetDiscographyFilters() {
+ discographyFilterState.categories = { albums: true, eps: true, singles: true };
+ discographyFilterState.content = { live: true, compilations: true, featured: true };
+ discographyFilterState.ownership = 'all';
+
+ // Reset button visual states
+ const container = document.getElementById('discography-filters');
+ if (!container) return;
+ container.querySelectorAll('.discography-filter-btn').forEach(btn => {
+ const filterType = btn.dataset.filter;
+ const value = btn.dataset.value;
+ if (filterType === 'ownership') {
+ btn.classList.toggle('active', value === 'all');
+ } else {
+ btn.classList.add('active');
+ }
+ });
+}
+
+function applyDiscographyFilters() {
+ const categories = ['albums', 'eps', 'singles'];
+
+ for (const cat of categories) {
+ const section = document.getElementById(`${cat}-section`);
+ if (!section) continue;
+
+ // Category toggle — hide entire section
+ if (!discographyFilterState.categories[cat]) {
+ section.style.display = 'none';
+ continue;
+ }
+ section.style.display = '';
+
+ // Filter individual cards within the section
+ const grid = document.getElementById(`${cat}-grid`);
+ if (!grid) continue;
+
+ let visibleOwned = 0;
+ let visibleMissing = 0;
+ let visibleCount = 0;
+
+ grid.querySelectorAll('.release-card').forEach(card => {
+ let hidden = false;
+
+ // Content filters
+ if (!discographyFilterState.content.live && card.getAttribute('data-is-live') === 'true') {
+ hidden = true;
+ }
+ if (!discographyFilterState.content.compilations && card.getAttribute('data-is-compilation') === 'true') {
+ hidden = true;
+ }
+ if (!discographyFilterState.content.featured && card.getAttribute('data-is-featured') === 'true') {
+ hidden = true;
+ }
+
+ // Ownership filter (only apply if card is not still checking)
+ if (!hidden && discographyFilterState.ownership !== 'all' && card._releaseData) {
+ const owned = card._releaseData.owned;
+ if (owned !== null) { // Don't hide cards still being checked
+ if (discographyFilterState.ownership === 'owned' && !owned) hidden = true;
+ if (discographyFilterState.ownership === 'missing' && owned) hidden = true;
+ }
+ }
+
+ card.style.display = hidden ? 'none' : '';
+
+ // Count visible cards for stats
+ if (!hidden && card._releaseData) {
+ visibleCount++;
+ if (card._releaseData.owned === true) visibleOwned++;
+ else if (card._releaseData.owned === false) visibleMissing++;
+ }
+ });
+
+ // Update section stats to reflect filtered view
+ const ownedEl = document.getElementById(`${cat}-owned-count`);
+ const missingEl = document.getElementById(`${cat}-missing-count`);
+ if (ownedEl) ownedEl.textContent = `${visibleOwned} owned`;
+ if (missingEl) missingEl.textContent = `${visibleMissing} missing`;
+
+ // Hide section entirely if all cards are hidden
+ section.style.display = visibleCount === 0 ? 'none' : '';
+ }
+}
+
// UI state management functions
function showArtistDetailLoading(show) {
const loadingElement = document.getElementById("artist-detail-loading");
diff --git a/webui/static/style.css b/webui/static/style.css
index 5df434d8..b7361a04 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -13566,6 +13566,62 @@ body {
letter-spacing: 0.5px;
}
+/* Discography Filters */
+.discography-filters {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 0 20px;
+ flex-wrap: wrap;
+}
+
+.filter-group {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.filter-label {
+ font-size: 11px;
+ font-weight: 600;
+ color: rgba(255, 255, 255, 0.35);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-right: 4px;
+}
+
+.filter-divider {
+ width: 1px;
+ height: 24px;
+ background: rgba(255, 255, 255, 0.1);
+ margin: 0 4px;
+}
+
+.discography-filter-btn {
+ padding: 5px 14px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.05);
+ color: rgba(255, 255, 255, 0.4);
+ font-size: 12px;
+ font-weight: 600;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif;
+ cursor: pointer;
+ border-radius: 16px;
+ transition: all 0.2s ease-out;
+}
+
+.discography-filter-btn:hover {
+ color: rgba(255, 255, 255, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.discography-filter-btn.active {
+ color: #fff;
+ border-color: rgba(29, 185, 84, 0.5);
+ background: rgba(29, 185, 84, 0.15);
+}
+
/* Discography Sections */
.discography-sections {
display: flex;