diff --git a/database/music_database.py b/database/music_database.py index e06b232b..bf28fc7a 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -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 diff --git a/web_server.py b/web_server.py index 2ec13b97..d74e6a0e 100644 --- a/web_server.py +++ b/web_server.py @@ -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: diff --git a/webui/static/script.js b/webui/static/script.js index c6cf4a64..9db125a2 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -23669,7 +23669,7 @@ async function showWatchlistModal() { ${artistsData.artists.map(artist => `