From c46e2c527e3d5b4fdb402d575899d7fa99ba514e Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Sun, 23 Nov 2025 20:23:30 -0800 Subject: [PATCH] search bar for watchlist --- core/watchlist_scanner.py | 26 +++++++++++++-- database/music_database.py | 52 +++++++++++++++++++++++++++--- web_server.py | 11 ++++--- webui/static/script.js | 48 +++++++++++++++++++++++++-- webui/static/style.css | 66 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 14 deletions(-) diff --git a/core/watchlist_scanner.py b/core/watchlist_scanner.py index e868fe9a..697dc443 100644 --- a/core/watchlist_scanner.py +++ b/core/watchlist_scanner.py @@ -240,10 +240,32 @@ class WatchlistScanner: """ try: logger.info(f"Scanning artist: {watchlist_artist.artist_name}") - + + # Update artist image from Spotify (cached for performance) + try: + artist_data = self.spotify_client.get_artist(watchlist_artist.spotify_artist_id) + if artist_data and 'images' in artist_data and artist_data['images']: + # Get medium-sized image (usually the second one, or first if only one) + image_url = None + if len(artist_data['images']) > 1: + image_url = artist_data['images'][1]['url'] + else: + image_url = artist_data['images'][0]['url'] + + # Update in database + if image_url: + self.database.update_watchlist_artist_image(watchlist_artist.spotify_artist_id, image_url) + logger.info(f"Updated artist image for {watchlist_artist.artist_name}") + else: + logger.warning(f"No image URL found for {watchlist_artist.artist_name}") + else: + logger.warning(f"No images in Spotify data for {watchlist_artist.artist_name}") + except Exception as img_error: + logger.warning(f"Could not update artist image for {watchlist_artist.artist_name}: {img_error}") + # Get artist discography from Spotify albums = self.get_artist_discography(watchlist_artist.spotify_artist_id, watchlist_artist.last_scan_timestamp) - + if albums is None: return ScanResult( artist_name=watchlist_artist.artist_name, diff --git a/database/music_database.py b/database/music_database.py index 3c35fb4d..0267f3be 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -85,6 +85,7 @@ class WatchlistArtist: last_scan_timestamp: Optional[datetime] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None + image_url: Optional[str] = None @dataclass class SimilarArtist: @@ -258,6 +259,9 @@ class MusicDatabase: # Add discovery feature tables (migration) self._add_discovery_tables(cursor) + # Add image_url column to watchlist_artists (migration) + self._add_watchlist_artist_image_column(cursor) + conn.commit() logger.info("Database initialized successfully") @@ -573,6 +577,20 @@ class MusicDatabase: logger.error(f"Error creating discovery tables: {e}") # Don't raise - this is a migration, database can still function + def _add_watchlist_artist_image_column(self, cursor): + """Add image_url column to watchlist_artists table""" + try: + cursor.execute("PRAGMA table_info(watchlist_artists)") + columns = [column[1] for column in cursor.fetchall()] + + if 'image_url' not in columns: + cursor.execute("ALTER TABLE watchlist_artists ADD COLUMN image_url TEXT") + logger.info("Added image_url column to watchlist_artists table") + + except Exception as e: + logger.error(f"Error adding image_url column to watchlist_artists: {e}") + # Don't raise - this is a migration, database can still function + def close(self): """Close database connection (no-op since we create connections per operation)""" # Each operation creates and closes its own connection, so nothing to do here @@ -2544,9 +2562,9 @@ class MusicDatabase: cursor = conn.cursor() cursor.execute(""" - SELECT id, spotify_artist_id, artist_name, date_added, - last_scan_timestamp, created_at, updated_at - FROM watchlist_artists + SELECT id, spotify_artist_id, artist_name, date_added, + last_scan_timestamp, created_at, updated_at, image_url + FROM watchlist_artists ORDER BY date_added DESC """) @@ -2554,6 +2572,12 @@ class MusicDatabase: watchlist_artists = [] for row in rows: + # Try to get image_url, fallback to None if column doesn't exist yet (migration) + try: + image_url = row['image_url'] + except (KeyError, IndexError): + image_url = None + watchlist_artists.append(WatchlistArtist( id=row['id'], spotify_artist_id=row['spotify_artist_id'], @@ -2561,7 +2585,8 @@ class MusicDatabase: date_added=datetime.fromisoformat(row['date_added']), last_scan_timestamp=datetime.fromisoformat(row['last_scan_timestamp']) if row['last_scan_timestamp'] else None, created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None, - updated_at=datetime.fromisoformat(row['updated_at']) if row['updated_at'] else None + updated_at=datetime.fromisoformat(row['updated_at']) if row['updated_at'] else None, + image_url=image_url )) return watchlist_artists @@ -2585,6 +2610,25 @@ class MusicDatabase: logger.error(f"Error getting watchlist count: {e}") return 0 + def update_watchlist_artist_image(self, spotify_artist_id: str, image_url: str) -> bool: + """Update the image URL for a watchlist artist""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + + cursor.execute(""" + UPDATE watchlist_artists + SET image_url = ?, updated_at = CURRENT_TIMESTAMP + WHERE spotify_artist_id = ? + """, (image_url, spotify_artist_id)) + + conn.commit() + return cursor.rowcount > 0 + + except Exception as e: + logger.error(f"Error updating watchlist artist image: {e}") + return False + # === Discovery Feature Methods === def add_or_update_similar_artist(self, source_artist_id: str, similar_artist_spotify_id: str, diff --git a/web_server.py b/web_server.py index ab94454e..a33e0597 100644 --- a/web_server.py +++ b/web_server.py @@ -14358,12 +14358,12 @@ def get_watchlist_count(): @app.route('/api/watchlist/artists', methods=['GET']) def get_watchlist_artists(): - """Get all artists in the watchlist""" + """Get all artists in the watchlist with cached images""" try: database = get_database() watchlist_artists = database.get_watchlist_artists() - - # Convert to JSON serializable format + + # Convert to JSON serializable format (images are cached from watchlist scans) artists_data = [] for artist in watchlist_artists: artists_data.append({ @@ -14373,9 +14373,10 @@ def get_watchlist_artists(): "date_added": artist.date_added.isoformat() if artist.date_added else None, "last_scan_timestamp": artist.last_scan_timestamp.isoformat() if artist.last_scan_timestamp else None, "created_at": artist.created_at.isoformat() if artist.created_at else None, - "updated_at": artist.updated_at.isoformat() if artist.updated_at else None + "updated_at": artist.updated_at.isoformat() if artist.updated_at else None, + "image_url": artist.image_url # Cached during watchlist scans }) - + return jsonify({"success": True, "artists": artists_data}) except Exception as e: print(f"Error getting watchlist artists: {e}") diff --git a/webui/static/script.js b/webui/static/script.js index 925bce8b..73cc485f 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -19748,10 +19748,29 @@ async function showWatchlistModal() { Update Similar Artists - -
+ + +
+ +
+ +
${artistsData.artists.map(artist => ` -
+
+ ${artist.image_url ? ` + ${escapeHtml(artist.artist_name)} + ` : ` +
+ 🎤 +
+ `}
${escapeHtml(artist.artist_name)} Added ${new Date(artist.date_added).toLocaleDateString()} @@ -19809,6 +19828,29 @@ function closeWatchlistModal() { } } +/** + * Filter watchlist artists based on search input + */ +function filterWatchlistArtists() { + const searchInput = document.getElementById('watchlist-search-input'); + const artistsList = document.getElementById('watchlist-artists-list'); + + if (!searchInput || !artistsList) return; + + const searchTerm = searchInput.value.toLowerCase().trim(); + const artistItems = artistsList.querySelectorAll('.watchlist-artist-item'); + + artistItems.forEach(item => { + const artistName = item.getAttribute('data-artist-name'); + + if (!searchTerm || artistName.includes(searchTerm)) { + item.style.display = 'flex'; + } else { + item.style.display = 'none'; + } + }); +} + /** * Start watchlist scan */ diff --git a/webui/static/style.css b/webui/static/style.css index 6acceb0a..18af6d3d 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -7538,6 +7538,72 @@ body { transform: translateY(-1px); } +/* Watchlist Search */ +.watchlist-search-container { + padding: 0 30px; +} + +.watchlist-search-input { + width: 100%; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 10px; + color: #ffffff; + font-size: 14px; + font-family: inherit; + transition: all 0.2s ease; +} + +.watchlist-search-input:focus { + outline: none; + background: rgba(255, 255, 255, 0.08); + border-color: rgba(29, 185, 84, 0.4); + box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.1); +} + +.watchlist-search-input::placeholder { + color: rgba(255, 255, 255, 0.4); +} + +/* Artist Image */ +.watchlist-artist-image { + width: 56px; + height: 56px; + border-radius: 50%; + object-fit: cover; + margin-right: 16px; + flex-shrink: 0; + border: 2px solid rgba(255, 255, 255, 0.1); + transition: all 0.2s ease; +} + +.watchlist-artist-item:hover .watchlist-artist-image { + border-color: rgba(29, 185, 84, 0.4); + transform: scale(1.05); +} + +.watchlist-artist-image-placeholder { + width: 56px; + height: 56px; + border-radius: 50%; + margin-right: 16px; + flex-shrink: 0; + background: linear-gradient(135deg, rgba(29, 185, 84, 0.2), rgba(29, 185, 84, 0.05)); + border: 2px solid rgba(29, 185, 84, 0.3); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + transition: all 0.2s ease; +} + +.watchlist-artist-item:hover .watchlist-artist-image-placeholder { + background: linear-gradient(135deg, rgba(29, 185, 84, 0.3), rgba(29, 185, 84, 0.1)); + border-color: rgba(29, 185, 84, 0.5); + transform: scale(1.05); +} + /* ===== WISHLIST OVERVIEW MODAL STYLES ===== */ /* Category Grid */