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:
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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, '"')}">
|
||||||
|
${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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue