From 139b8530f4684b17531cfffcbcedcf92dea85318 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Tue, 17 Feb 2026 11:21:41 -0800 Subject: [PATCH] Add watchlist filter to library page --- database/music_database.py | 56 +++++++++++++++++++++++++++++++------- web_server.py | 4 ++- webui/index.html | 7 +++++ webui/static/script.js | 28 +++++++++++++++++-- webui/static/style.css | 33 ++++++++++++++++++++++ 5 files changed, 115 insertions(+), 13 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index ca0ac70..ad65166 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -4018,7 +4018,7 @@ class MusicDatabase: 'server_source': server_source } - def get_library_artists(self, search_query: str = "", letter: str = "", page: int = 1, limit: int = 50) -> Dict[str, Any]: + def get_library_artists(self, search_query: str = "", letter: str = "", page: int = 1, limit: int = 50, watchlist_filter: str = "all") -> Dict[str, Any]: """ Get artists for the library page with search, filtering, and pagination @@ -4027,6 +4027,7 @@ class MusicDatabase: letter: Filter by first letter (a-z, #, or "" for all) page: Page number (1-based) limit: Number of results per page + watchlist_filter: Filter by watchlist status ("all", "watched", "unwatched") Returns: Dict containing artists list, pagination info, and total count @@ -4062,7 +4063,39 @@ class MusicDatabase: where_clause = " AND ".join(where_conditions) if where_conditions else "1=1" - # Get total count (matching dashboard method) + # Pre-fetch watchlist data (small table, single fast query) + cursor.execute("SELECT spotify_artist_id, itunes_artist_id, LOWER(artist_name) as name_lower FROM watchlist_artists") + watchlist_rows = cursor.fetchall() + wl_spotify = {r['spotify_artist_id'] for r in watchlist_rows if r['spotify_artist_id']} + wl_itunes = {r['itunes_artist_id'] for r in watchlist_rows if r['itunes_artist_id']} + wl_names = {r['name_lower'] for r in watchlist_rows if r['name_lower']} + + # Apply watchlist filter as WHERE conditions using IN clauses + if watchlist_filter in ("watched", "unwatched"): + match_parts = [] + match_params = [] + if wl_spotify: + match_parts.append(f"(a.spotify_artist_id IS NOT NULL AND a.spotify_artist_id IN ({','.join('?' * len(wl_spotify))}))") + match_params.extend(wl_spotify) + if wl_itunes: + match_parts.append(f"(a.itunes_artist_id IS NOT NULL AND a.itunes_artist_id IN ({','.join('?' * len(wl_itunes))}))") + match_params.extend(wl_itunes) + if wl_names: + match_parts.append(f"LOWER(a.name) IN ({','.join('?' * len(wl_names))})") + match_params.extend(wl_names) + + if match_parts: + combined = ' OR '.join(match_parts) + if watchlist_filter == "watched": + where_clause += f" AND ({combined})" + else: + where_clause += f" AND NOT ({combined})" + params.extend(match_params) + elif watchlist_filter == "watched": + # Empty watchlist, no artists can match + where_clause += " AND 0" + + # Get total count count_query = f""" SELECT COUNT(*) as total_count FROM artists a @@ -4081,17 +4114,13 @@ class MusicDatabase: a.thumb_url, a.genres, a.musicbrainz_id, + a.spotify_artist_id, + a.itunes_artist_id, COUNT(DISTINCT al.id) as album_count, - COUNT(DISTINCT t.id) as track_count, - MAX(CASE WHEN wa.id IS NOT NULL THEN 1 ELSE 0 END) as is_watched + COUNT(DISTINCT t.id) as track_count FROM artists a LEFT JOIN albums al ON a.id = al.artist_id LEFT JOIN tracks t ON al.id = t.album_id - LEFT JOIN watchlist_artists wa ON ( - (a.spotify_artist_id IS NOT NULL AND a.spotify_artist_id = wa.spotify_artist_id) - OR (a.itunes_artist_id IS NOT NULL AND a.itunes_artist_id = wa.itunes_artist_id) - OR LOWER(a.name) = LOWER(wa.artist_name) - ) WHERE {where_clause} GROUP BY a.id, a.name, a.thumb_url, a.genres, a.musicbrainz_id ORDER BY a.name COLLATE NOCASE @@ -4124,6 +4153,13 @@ class MusicDatabase: genres=genres ) + # Determine watchlist status via set lookups + is_watched = ( + (row['spotify_artist_id'] and row['spotify_artist_id'] in wl_spotify) + or (row['itunes_artist_id'] and row['itunes_artist_id'] in wl_itunes) + or (row['name'] and row['name'].lower() in wl_names) + ) + # Add stats artist_data = { 'id': artist.id, @@ -4133,7 +4169,7 @@ class MusicDatabase: 'musicbrainz_id': row['musicbrainz_id'], 'album_count': row['album_count'] or 0, 'track_count': row['track_count'] or 0, - 'is_watched': bool(row['is_watched']) + 'is_watched': bool(is_watched) } artists.append(artist_data) diff --git a/web_server.py b/web_server.py index 38e871c..41a9804 100644 --- a/web_server.py +++ b/web_server.py @@ -4850,6 +4850,7 @@ def get_library_artists(): letter = request.args.get('letter', 'all') page = int(request.args.get('page', 1)) limit = int(request.args.get('limit', 75)) + watchlist_filter = request.args.get('watchlist', 'all') # Get database instance database = get_database() @@ -4859,7 +4860,8 @@ def get_library_artists(): search_query=search_query, letter=letter, page=page, - limit=limit + limit=limit, + watchlist_filter=watchlist_filter ) # Fix image URLs for all artists diff --git a/webui/index.html b/webui/index.html index 34efa27..c9a1fa4 100644 --- a/webui/index.html +++ b/webui/index.html @@ -1712,6 +1712,13 @@
🔍
+ +
+ + + +
+
diff --git a/webui/static/script.js b/webui/static/script.js index b9fe72a..6630aab 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -25299,7 +25299,8 @@ const libraryPageState = { currentLetter: "all", currentPage: 1, limit: 75, - debounceTimer: null + debounceTimer: null, + watchlistFilter: "all" }; function initializeLibraryPage() { @@ -25309,6 +25310,9 @@ function initializeLibraryPage() { // Initialize search functionality initializeLibrarySearch(); + // Initialize watchlist filter + initializeWatchlistFilter(); + // Initialize alphabet selector initializeAlphabetSelector(); @@ -25358,6 +25362,25 @@ function initializeLibrarySearch() { }); } +function initializeWatchlistFilter() { + const filterButtons = document.querySelectorAll(".watchlist-filter-btn"); + + filterButtons.forEach(button => { + button.addEventListener("click", () => { + const filter = button.getAttribute("data-filter"); + + // Update active state + filterButtons.forEach(btn => btn.classList.remove("active")); + button.classList.add("active"); + + // Update state and reload + libraryPageState.watchlistFilter = filter; + libraryPageState.currentPage = 1; + loadLibraryArtists(); + }); + }); +} + function initializeAlphabetSelector() { const alphabetButtons = document.querySelectorAll(".alphabet-btn"); @@ -25408,7 +25431,8 @@ async function loadLibraryArtists() { search: libraryPageState.currentSearch, letter: libraryPageState.currentLetter, page: libraryPageState.currentPage, - limit: libraryPageState.limit + limit: libraryPageState.limit, + watchlist: libraryPageState.watchlistFilter }); // Fetch artists from API diff --git a/webui/static/style.css b/webui/static/style.css index 520f731..8a690f3 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -12620,6 +12620,39 @@ body { } /* Alphabet Selector - Apple Style Design */ +/* Watchlist Filter */ +.watchlist-filter { + display: flex; + gap: 8px; + justify-content: center; + padding: 8px 0; +} + +.watchlist-filter-btn { + padding: 6px 16px; + border: 1px solid rgba(255, 255, 255, 0.1); + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.5); + font-size: 13px; + font-weight: 600; + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif; + cursor: pointer; + border-radius: 20px; + transition: all 0.2s ease-out; +} + +.watchlist-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); +} + +.watchlist-filter-btn.active { + color: #fff; + border-color: rgba(29, 185, 84, 0.5); + background: rgba(29, 185, 84, 0.15); +} + .alphabet-selector { overflow-x: auto; -webkit-overflow-scrolling: touch;