search bar for watchlist

This commit is contained in:
Broque Thomas 2025-11-23 20:23:30 -08:00
parent 8798522854
commit c46e2c527e
5 changed files with 189 additions and 14 deletions

View file

@ -240,10 +240,32 @@ class WatchlistScanner:
""" """
try: try:
logger.info(f"Scanning artist: {watchlist_artist.artist_name}") 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 # Get artist discography from Spotify
albums = self.get_artist_discography(watchlist_artist.spotify_artist_id, watchlist_artist.last_scan_timestamp) albums = self.get_artist_discography(watchlist_artist.spotify_artist_id, watchlist_artist.last_scan_timestamp)
if albums is None: if albums is None:
return ScanResult( return ScanResult(
artist_name=watchlist_artist.artist_name, artist_name=watchlist_artist.artist_name,

View file

@ -85,6 +85,7 @@ class WatchlistArtist:
last_scan_timestamp: Optional[datetime] = None last_scan_timestamp: Optional[datetime] = None
created_at: Optional[datetime] = None created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None
image_url: Optional[str] = None
@dataclass @dataclass
class SimilarArtist: class SimilarArtist:
@ -258,6 +259,9 @@ class MusicDatabase:
# Add discovery feature tables (migration) # Add discovery feature tables (migration)
self._add_discovery_tables(cursor) self._add_discovery_tables(cursor)
# Add image_url column to watchlist_artists (migration)
self._add_watchlist_artist_image_column(cursor)
conn.commit() conn.commit()
logger.info("Database initialized successfully") logger.info("Database initialized successfully")
@ -573,6 +577,20 @@ class MusicDatabase:
logger.error(f"Error creating discovery tables: {e}") logger.error(f"Error creating discovery tables: {e}")
# Don't raise - this is a migration, database can still function # 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): def close(self):
"""Close database connection (no-op since we create connections per operation)""" """Close database connection (no-op since we create connections per operation)"""
# Each operation creates and closes its own connection, so nothing to do here # Each operation creates and closes its own connection, so nothing to do here
@ -2544,9 +2562,9 @@ class MusicDatabase:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute("""
SELECT id, spotify_artist_id, artist_name, date_added, SELECT id, spotify_artist_id, artist_name, date_added,
last_scan_timestamp, created_at, updated_at last_scan_timestamp, created_at, updated_at, image_url
FROM watchlist_artists FROM watchlist_artists
ORDER BY date_added DESC ORDER BY date_added DESC
""") """)
@ -2554,6 +2572,12 @@ class MusicDatabase:
watchlist_artists = [] watchlist_artists = []
for row in rows: 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( watchlist_artists.append(WatchlistArtist(
id=row['id'], id=row['id'],
spotify_artist_id=row['spotify_artist_id'], spotify_artist_id=row['spotify_artist_id'],
@ -2561,7 +2585,8 @@ class MusicDatabase:
date_added=datetime.fromisoformat(row['date_added']), date_added=datetime.fromisoformat(row['date_added']),
last_scan_timestamp=datetime.fromisoformat(row['last_scan_timestamp']) if row['last_scan_timestamp'] else None, 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, 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 return watchlist_artists
@ -2585,6 +2610,25 @@ class MusicDatabase:
logger.error(f"Error getting watchlist count: {e}") logger.error(f"Error getting watchlist count: {e}")
return 0 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 === # === Discovery Feature Methods ===
def add_or_update_similar_artist(self, source_artist_id: str, similar_artist_spotify_id: str, def add_or_update_similar_artist(self, source_artist_id: str, similar_artist_spotify_id: str,

View file

@ -14358,12 +14358,12 @@ def get_watchlist_count():
@app.route('/api/watchlist/artists', methods=['GET']) @app.route('/api/watchlist/artists', methods=['GET'])
def get_watchlist_artists(): def get_watchlist_artists():
"""Get all artists in the watchlist""" """Get all artists in the watchlist with cached images"""
try: try:
database = get_database() database = get_database()
watchlist_artists = database.get_watchlist_artists() watchlist_artists = database.get_watchlist_artists()
# Convert to JSON serializable format # Convert to JSON serializable format (images are cached from watchlist scans)
artists_data = [] artists_data = []
for artist in watchlist_artists: for artist in watchlist_artists:
artists_data.append({ artists_data.append({
@ -14373,9 +14373,10 @@ def get_watchlist_artists():
"date_added": artist.date_added.isoformat() if artist.date_added else None, "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, "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, "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}) return jsonify({"success": True, "artists": artists_data})
except Exception as e: except Exception as e:
print(f"Error getting watchlist artists: {e}") print(f"Error getting watchlist artists: {e}")

View file

@ -19748,10 +19748,29 @@ async function showWatchlistModal() {
Update Similar Artists Update Similar Artists
</button> </button>
</div> </div>
<div class="watchlist-artists-list"> <!-- Search Bar -->
<div class="watchlist-search-container" style="margin-bottom: 16px;">
<input type="text"
id="watchlist-search-input"
class="watchlist-search-input"
placeholder="🔍 Search artists..."
oninput="filterWatchlistArtists()">
</div>
<div class="watchlist-artists-list" id="watchlist-artists-list">
${artistsData.artists.map(artist => ` ${artistsData.artists.map(artist => `
<div class="watchlist-artist-item"> <div class="watchlist-artist-item" data-artist-name="${artist.artist_name.toLowerCase().replace(/"/g, '&quot;')}">
${artist.image_url ? `
<img src="${artist.image_url}"
alt="${escapeHtml(artist.artist_name)}"
class="watchlist-artist-image"
onerror="this.style.display='none'">
` : `
<div class="watchlist-artist-image-placeholder">
🎤
</div>
`}
<div class="watchlist-artist-info"> <div class="watchlist-artist-info">
<span class="watchlist-artist-name">${escapeHtml(artist.artist_name)}</span> <span class="watchlist-artist-name">${escapeHtml(artist.artist_name)}</span>
<span class="watchlist-artist-date">Added ${new Date(artist.date_added).toLocaleDateString()}</span> <span class="watchlist-artist-date">Added ${new Date(artist.date_added).toLocaleDateString()}</span>
@ -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 * Start watchlist scan
*/ */

View file

@ -7538,6 +7538,72 @@ body {
transform: translateY(-1px); 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 ===== */ /* ===== WISHLIST OVERVIEW MODAL STYLES ===== */
/* Category Grid */ /* Category Grid */