search bar for watchlist
This commit is contained in:
parent
8798522854
commit
c46e2c527e
5 changed files with 189 additions and 14 deletions
|
|
@ -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, '"')}">
|
||||
${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…
Reference in a new issue