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);