search bar for watchlist

pull/80/head
Broque Thomas 6 months ago
parent 8798522854
commit c46e2c527e

@ -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,

@ -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,

@ -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}")

@ -19748,10 +19748,29 @@ async function showWatchlistModal() {
Update Similar Artists
</button>
</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 => `
<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">
<span class="watchlist-artist-name">${escapeHtml(artist.artist_name)}</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
*/

@ -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 */

Loading…
Cancel
Save