From e65b6bab672ce61f8137ae8a0267b6271c4cb58e Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:29:48 -0700 Subject: [PATCH] Add metadata source filter to library and fix Discogs enrichment Library page: new dropdown filter to show artists matched or unmatched to any metadata source (Spotify, MusicBrainz, Deezer, Discogs, etc). Select "No Discogs" to find artists needing manual Discogs matching. Filter applied as WHERE clause on the source ID columns. Discogs enrichment: added to valid_services whitelist, _enrichment_locks, and _run_single_enrichment handler. The Enrich button was returning an error when Discogs was selected from the dropdown. --- database/music_database.py | 26 +++++++++++++++++++++++++- web_server.py | 4 +++- webui/index.html | 31 +++++++++++++++++++++++++++++++ webui/static/script.js | 17 ++++++++++++++++- webui/static/style.css | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 3 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index 5abcd165..3d426574 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -7905,7 +7905,7 @@ class MusicDatabase: 'server_source': server_source } - def get_library_artists(self, search_query: str = "", letter: str = "", page: int = 1, limit: int = 50, watchlist_filter: str = "all", profile_id: int = 1) -> Dict[str, Any]: + def get_library_artists(self, search_query: str = "", letter: str = "", page: int = 1, limit: int = 50, watchlist_filter: str = "all", profile_id: int = 1, source_filter: str = "") -> Dict[str, Any]: """ Get artists for the library page with search, filtering, and pagination @@ -7915,6 +7915,7 @@ class MusicDatabase: page: Page number (1-based) limit: Number of results per page watchlist_filter: Filter by watchlist status ("all", "watched", "unwatched") + source_filter: Filter by metadata source match (e.g. "spotify", "!spotify" for unmatched) Returns: Dict containing artists list, pagination info, and total count @@ -7940,6 +7941,29 @@ class MusicDatabase: where_conditions.append("UPPER(SUBSTR(name, 1, 1)) = UPPER(?)") params.append(letter) + # Metadata source filter — match or exclude by enrichment source + if source_filter: + _source_columns = { + 'spotify': 'a.spotify_artist_id', + 'musicbrainz': 'a.musicbrainz_id', + 'deezer': 'a.deezer_id', + 'discogs': 'a.discogs_id', + 'audiodb': 'a.audiodb_id', + 'itunes': 'a.itunes_artist_id', + 'lastfm': 'a.lastfm_url', + 'genius': 'a.genius_url', + 'tidal': 'a.tidal_id', + 'qobuz': 'a.qobuz_id', + } + negate = source_filter.startswith('!') + key = source_filter.lstrip('!') + col = _source_columns.get(key) + if col: + if negate: + where_conditions.append(f"({col} IS NULL OR {col} = '')") + else: + where_conditions.append(f"({col} IS NOT NULL AND {col} != '')") + # Get active server for filtering from config.settings import config_manager active_server = config_manager.get_active_media_server() diff --git a/web_server.py b/web_server.py index 3f45a821..e2c3614b 100644 --- a/web_server.py +++ b/web_server.py @@ -9365,6 +9365,7 @@ def get_library_artists(): page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 75)) watchlist_filter = request.args.get('watchlist', 'all') + source_filter = request.args.get('source_filter', '') # Get database instance database = get_database() @@ -9376,7 +9377,8 @@ def get_library_artists(): page=page, limit=limit, watchlist_filter=watchlist_filter, - profile_id=get_current_profile_id() + profile_id=get_current_profile_id(), + source_filter=source_filter ) # Fix image URLs for all artists diff --git a/webui/index.html b/webui/index.html index 03434a4b..ebba27a7 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2633,6 +2633,37 @@ + +
+ +
+
diff --git a/webui/static/script.js b/webui/static/script.js index 2a2be056..bcde568f 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -41208,7 +41208,8 @@ const libraryPageState = { currentPage: 1, limit: 75, debounceTimer: null, - watchlistFilter: "all" + watchlistFilter: "all", + sourceFilter: "" }; function initializeLibraryPage() { @@ -41221,6 +41222,9 @@ function initializeLibraryPage() { // Initialize watchlist filter initializeWatchlistFilter(); + // Initialize metadata source filter + initializeSourceFilter(); + // Initialize alphabet selector initializeAlphabetSelector(); @@ -41302,6 +41306,16 @@ function initializeWatchlistFilter() { }); } +function initializeSourceFilter() { + const select = document.getElementById('library-source-filter'); + if (!select) return; + select.addEventListener('change', () => { + libraryPageState.sourceFilter = select.value; + libraryPageState.currentPage = 1; + loadLibraryArtists(); + }); +} + function initializeAlphabetSelector() { const alphabetButtons = document.querySelectorAll(".alphabet-btn"); @@ -41355,6 +41369,7 @@ async function loadLibraryArtists() { limit: libraryPageState.limit, watchlist: libraryPageState.watchlistFilter }); + if (libraryPageState.sourceFilter) params.set('source_filter', libraryPageState.sourceFilter); // Fetch artists from API const response = await fetch(`/api/library/artists?${params}`); diff --git a/webui/static/style.css b/webui/static/style.css index 8f57e2dc..4e17c6eb 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -21798,6 +21798,44 @@ body.helper-mode-active #dashboard-activity-feed:hover { padding: 8px 0; } +.library-source-filter { + display: flex; + justify-content: center; + padding: 0 0 4px; +} +.library-source-filter-select { + padding: 6px 30px 6px 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.05); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='rgba(255,255,255,0.35)' d='M2 4l4 4 4-4'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + color: rgba(255, 255, 255, 0.6); + font-size: 12px; + font-weight: 500; + font-family: inherit; + border-radius: 8px; + cursor: pointer; + transition: border-color 0.2s, color 0.2s; + min-width: 140px; +} +.library-source-filter-select:hover { + border-color: rgba(255, 255, 255, 0.2); + color: rgba(255, 255, 255, 0.8); +} +.library-source-filter-select:focus { + border-color: rgba(var(--accent-rgb), 0.4); + outline: none; +} +.library-source-filter-select option, +.library-source-filter-select optgroup { + background: #1a1a1e; + color: #fff; +} + .watchlist-filter-btn { padding: 6px 16px; border: 1px solid rgba(255, 255, 255, 0.1);