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