Fix watchlist artist config and add image at insert when Itunes source.

This commit is contained in:
Broque Thomas 2026-01-25 09:24:28 -08:00
parent 47c45ddea7
commit 905e98016f
3 changed files with 116 additions and 13 deletions

View file

@ -291,6 +291,9 @@ class MusicDatabase:
# Add iTunes artist ID column to watchlist_artists (migration)
self._add_watchlist_itunes_id_column(cursor)
# Make spotify_artist_id nullable for iTunes-only artists (migration)
self._fix_watchlist_spotify_id_nullable(cursor)
conn.commit()
logger.info("Database initialized successfully")
@ -821,6 +824,67 @@ class MusicDatabase:
logger.error(f"Error adding itunes_artist_id column to watchlist_artists: {e}")
# Don't raise - this is a migration, database can still function
def _fix_watchlist_spotify_id_nullable(self, cursor):
"""
Make spotify_artist_id nullable in watchlist_artists table.
This allows adding iTunes-only artists without Spotify IDs.
Since SQLite doesn't support modifying column constraints directly,
we need to recreate the table if the constraint needs to be changed.
"""
try:
# Check if spotify_artist_id is currently NOT NULL
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='watchlist_artists'")
result = cursor.fetchone()
if result and 'spotify_artist_id TEXT UNIQUE NOT NULL' in result[0]:
logger.info("Migrating watchlist_artists table to make spotify_artist_id nullable...")
# Create new table with nullable spotify_artist_id
cursor.execute("""
CREATE TABLE watchlist_artists_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
spotify_artist_id TEXT UNIQUE,
artist_name TEXT NOT NULL,
date_added TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_scan_timestamp TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
image_url TEXT,
include_albums INTEGER DEFAULT 1,
include_eps INTEGER DEFAULT 1,
include_singles INTEGER DEFAULT 1,
include_live INTEGER DEFAULT 0,
include_remixes INTEGER DEFAULT 0,
include_acoustic INTEGER DEFAULT 0,
include_compilations INTEGER DEFAULT 0,
itunes_artist_id TEXT
)
""")
# Copy data from old table
cursor.execute("""
INSERT INTO watchlist_artists_new
SELECT * FROM watchlist_artists
""")
# Drop old table
cursor.execute("DROP TABLE watchlist_artists")
# Rename new table
cursor.execute("ALTER TABLE watchlist_artists_new RENAME TO watchlist_artists")
# Recreate indexes
cursor.execute("CREATE INDEX IF NOT EXISTS idx_watchlist_spotify_id ON watchlist_artists (spotify_artist_id)")
logger.info("Successfully migrated watchlist_artists table - spotify_artist_id is now nullable")
else:
logger.debug("watchlist_artists table already has nullable spotify_artist_id or custom schema")
except Exception as e:
logger.error(f"Error making spotify_artist_id nullable in 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

View file

@ -17423,7 +17423,8 @@ def get_watchlist_artists():
"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,
"image_url": artist.image_url # Cached during watchlist scans
"image_url": artist.image_url, # Cached during watchlist scans
"itunes_artist_id": artist.itunes_artist_id # For iTunes-only artists
})
return jsonify({"success": True, "artists": artists_data})
@ -17446,9 +17447,42 @@ def add_to_watchlist():
success = database.add_artist_to_watchlist(artist_id, artist_name)
if success:
# Fetch and cache artist image immediately from Spotify
# Detect ID type: iTunes IDs are purely numeric, Spotify IDs are alphanumeric
is_itunes_id = artist_id.isdigit()
# Fetch and cache artist image immediately
try:
if spotify_client and spotify_client.is_authenticated():
if is_itunes_id:
# For iTunes artists, fetch image from iTunes API
# We look up 'album' entity because 'artist' entity doesn't always have artwork
# We fallback to the first album's artwork as the artist image
try:
itunes_url = f"https://itunes.apple.com/lookup?id={artist_id}&entity=album&limit=5"
print(f"🔍 Fetching iTunes artist image: {itunes_url}")
resp = requests.get(itunes_url, timeout=5)
image_url = None
if resp.status_code == 200:
data = resp.json()
results = data.get('results', [])
# Iterate results to find one with artwork
for res in results:
if 'artworkUrl100' in res:
# Get highest res by replacing dimensions
# iTunes artwork URLs usually end in .../100x100bb.jpg
image_url = res['artworkUrl100'].replace('100x100', '600x600')
break
if image_url:
database.update_watchlist_artist_image(artist_id, image_url)
print(f"✅ Cached iTunes artist image for {artist_name}")
else:
print(f"⚠️ No artwork found for iTunes artist {artist_name}")
except Exception as itunes_error:
print(f"⚠️ Error fetching iTunes artwork: {itunes_error}")
elif spotify_client and spotify_client.is_authenticated():
# For Spotify artists, fetch from Spotify API
artist_data = spotify_client.get_artist(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)
@ -17974,21 +18008,26 @@ def watchlist_artist_config(artist_id):
cursor.execute("""
SELECT include_albums, include_eps, include_singles,
include_live, include_remixes, include_acoustic, include_compilations,
artist_name, image_url
artist_name, image_url, spotify_artist_id, itunes_artist_id
FROM watchlist_artists
WHERE spotify_artist_id = ?
""", (artist_id,))
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
""", (artist_id, artist_id))
result = cursor.fetchone()
conn.close()
if not result:
return jsonify({"success": False, "error": "Artist not found in watchlist"}), 404
# Determine if this is an iTunes or Spotify artist
is_itunes_artist = artist_id.isdigit()
spotify_id = result[9] # spotify_artist_id from query
itunes_id = result[10] # itunes_artist_id from query
# Get artist info from Spotify
# Get artist info from Spotify (only for Spotify artists)
artist_info = None
if spotify_client and spotify_client.is_authenticated():
if not is_itunes_artist and spotify_client and spotify_client.is_authenticated() and spotify_id:
try:
artist_data = spotify_client.sp.artist(artist_id)
artist_data = spotify_client.sp.artist(spotify_id)
if artist_data:
artist_info = {
'id': artist_data['id'],
@ -18053,10 +18092,10 @@ def watchlist_artist_config(artist_id):
SET include_albums = ?, include_eps = ?, include_singles = ?,
include_live = ?, include_remixes = ?, include_acoustic = ?, include_compilations = ?,
updated_at = CURRENT_TIMESTAMP
WHERE spotify_artist_id = ?
WHERE spotify_artist_id = ? OR itunes_artist_id = ?
""", (int(include_albums), int(include_eps), int(include_singles),
int(include_live), int(include_remixes), int(include_acoustic), int(include_compilations),
artist_id))
artist_id, artist_id))
conn.commit()
if cursor.rowcount == 0:

View file

@ -23669,7 +23669,7 @@ async function showWatchlistModal() {
${artistsData.artists.map(artist => `
<div class="watchlist-artist-item"
data-artist-name="${artist.artist_name.toLowerCase().replace(/"/g, '&quot;')}"
data-artist-id="${artist.spotify_artist_id}"
data-artist-id="${artist.spotify_artist_id || artist.itunes_artist_id}"
style="cursor: pointer;">
${artist.image_url ? `
<img src="${artist.image_url}"
@ -23689,7 +23689,7 @@ async function showWatchlistModal() {
` : ''}
</div>
<button class="playlist-modal-btn playlist-modal-btn-secondary watchlist-remove-btn"
data-artist-id="${artist.spotify_artist_id}"
data-artist-id="${artist.spotify_artist_id || artist.itunes_artist_id}"
data-artist-name="${escapeHtml(artist.artist_name)}"
onclick="event.stopPropagation();">
Remove