diff --git a/database/music_database.py b/database/music_database.py
index ef2dadda..06d3839c 100644
--- a/database/music_database.py
+++ b/database/music_database.py
@@ -4859,6 +4859,8 @@ class MusicDatabase:
'image_url': artist.thumb_url,
'genres': artist.genres,
'musicbrainz_id': row['musicbrainz_id'],
+ 'spotify_artist_id': row['spotify_artist_id'],
+ 'itunes_artist_id': row['itunes_artist_id'],
'deezer_id': row['deezer_id'],
'audiodb_id': row['audiodb_id'],
'album_count': row['album_count'] or 0,
diff --git a/web_server.py b/web_server.py
index f5347c66..b84bcab9 100644
--- a/web_server.py
+++ b/web_server.py
@@ -21541,6 +21541,81 @@ def add_batch_to_watchlist():
print(f"Error batch adding to watchlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
+@app.route('/api/library/watchlist-all-unwatched', methods=['POST'])
+def watchlist_all_unwatched_library_artists():
+ """Add all unwatched library artists (that have valid external IDs) to the watchlist"""
+ try:
+ database = get_database()
+ active_source = _get_active_discovery_source()
+
+ # Get ALL unwatched artists from library (no pagination)
+ result = database.get_library_artists(
+ search_query='',
+ letter='all',
+ page=1,
+ limit=99999,
+ watchlist_filter='unwatched'
+ )
+
+ unwatched_artists = result.get('artists', [])
+ added = 0
+ skipped_no_id = 0
+ skipped_already = 0
+
+ for artist in unwatched_artists:
+ # Determine the external ID to use based on active source
+ artist_id = None
+ if active_source == 'spotify' and artist.get('spotify_artist_id'):
+ artist_id = artist['spotify_artist_id']
+ elif artist.get('itunes_artist_id'):
+ artist_id = artist['itunes_artist_id']
+ elif artist.get('spotify_artist_id'):
+ # Fallback: use spotify if itunes not available
+ artist_id = artist['spotify_artist_id']
+
+ if not artist_id:
+ skipped_no_id += 1
+ continue
+
+ artist_name = artist.get('name', '')
+ if not artist_name:
+ continue
+
+ # Check if already watched (shouldn't be since we filtered, but safety check)
+ if database.is_artist_in_watchlist(artist_id):
+ skipped_already += 1
+ continue
+
+ success = database.add_artist_to_watchlist(artist_id, artist_name)
+ if success:
+ added += 1
+ # Use library thumb_url if available (no HTTP calls needed)
+ if artist.get('image_url'):
+ try:
+ database.update_watchlist_artist_image(artist_id, artist['image_url'])
+ except Exception:
+ pass
+
+ total_unwatched = len(unwatched_artists)
+ message_parts = [f"Added {added} artist{'s' if added != 1 else ''} to watchlist"]
+ if skipped_no_id > 0:
+ message_parts.append(f"{skipped_no_id} skipped (no matching ID yet)")
+
+ return jsonify({
+ "success": True,
+ "added": added,
+ "skipped_no_id": skipped_no_id,
+ "skipped_already": skipped_already,
+ "total_unwatched": total_unwatched,
+ "message": " — ".join(message_parts)
+ })
+
+ except Exception as e:
+ print(f"Error bulk watchlisting library artists: {e}")
+ import traceback
+ traceback.print_exc()
+ return jsonify({"success": False, "error": str(e)}), 500
+
@app.route('/api/watchlist/remove-batch', methods=['POST'])
def remove_batch_from_watchlist():
"""Remove multiple artists from the watchlist"""
diff --git a/webui/index.html b/webui/index.html
index 1fb88ee4..639214e5 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -1902,6 +1902,10 @@
+
diff --git a/webui/static/mobile.css b/webui/static/mobile.css
index d81707c9..8116056b 100644
--- a/webui/static/mobile.css
+++ b/webui/static/mobile.css
@@ -870,6 +870,19 @@
max-width: 100%;
}
+ .watchlist-filter {
+ flex-wrap: wrap;
+ }
+
+ .library-watchlist-all-btn {
+ font-size: 12px;
+ padding: 5px 12px;
+ }
+
+ .library-card-watchlist-btn {
+ opacity: 1;
+ }
+
.alphabet-selector-inner {
flex-wrap: wrap;
min-width: 0;
diff --git a/webui/static/script.js b/webui/static/script.js
index 86717e0c..a9f27900 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -27551,6 +27551,7 @@ function initializeLibrarySearch() {
function initializeWatchlistFilter() {
const filterButtons = document.querySelectorAll(".watchlist-filter-btn");
+ const watchAllBtn = document.getElementById("library-watchlist-all-btn");
filterButtons.forEach(button => {
button.addEventListener("click", () => {
@@ -27560,6 +27561,15 @@ function initializeWatchlistFilter() {
filterButtons.forEach(btn => btn.classList.remove("active"));
button.classList.add("active");
+ // Show/hide "Watch All Unwatched" button
+ if (watchAllBtn) {
+ if (filter === "unwatched") {
+ watchAllBtn.classList.remove("hidden");
+ } else {
+ watchAllBtn.classList.add("hidden");
+ }
+ }
+
// Update state and reload
libraryPageState.watchlistFilter = filter;
libraryPageState.currentPage = 1;
@@ -27710,7 +27720,8 @@ function createLibraryArtistCard(artist) {
badgeOffset += 32;
});
- // Add watchlist indicator if artist is on the watchlist
+ // Add watchlist button/indicator on the card
+ const hasExternalId = artist.itunes_artist_id || artist.spotify_artist_id;
if (artist.is_watched) {
const watchIcon = document.createElement('div');
watchIcon.className = 'watchlist-card-icon';
@@ -27718,6 +27729,17 @@ function createLibraryArtistCard(artist) {
watchIcon.style.top = badgeOffset + 'px';
watchIcon.textContent = '👁️';
card.appendChild(watchIcon);
+ } else if (hasExternalId) {
+ const watchBtn = document.createElement('button');
+ watchBtn.className = 'library-card-watchlist-btn';
+ watchBtn.style.top = badgeOffset + 'px';
+ watchBtn.title = 'Add to Watchlist';
+ watchBtn.innerHTML = '👁️Watch';
+ watchBtn.onclick = (e) => {
+ e.stopPropagation();
+ toggleLibraryCardWatchlist(watchBtn, artist);
+ };
+ card.appendChild(watchBtn);
}
// Create image element
@@ -27843,6 +27865,113 @@ function showLibraryEmpty(show) {
}
}
+async function addAllUnwatchedToWatchlist(btn) {
+ if (btn.classList.contains('adding') || btn.classList.contains('all-added')) return;
+
+ btn.classList.add('adding');
+ const textSpan = btn.querySelector('.watchlist-all-text');
+ const originalText = textSpan.textContent;
+ textSpan.textContent = 'Adding...';
+
+ try {
+ const response = await fetch('/api/library/watchlist-all-unwatched', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ });
+ const data = await response.json();
+
+ if (data.success) {
+ btn.classList.remove('adding');
+ btn.classList.add('all-added');
+
+ let resultText = `Added ${data.added}`;
+ if (data.skipped_no_id > 0) {
+ resultText += ` (${data.skipped_no_id} unmatched)`;
+ }
+ textSpan.textContent = resultText;
+
+ if (data.added > 0) {
+ showToast(data.message, 'success');
+ // Reload the library to reflect changes
+ setTimeout(() => {
+ loadLibraryArtists();
+ }, 1500);
+ } else if (data.skipped_no_id > 0) {
+ showToast(`No artists could be added — ${data.skipped_no_id} don't have matching IDs yet. Background workers will match them over time.`, 'warning');
+ } else {
+ showToast('No unwatched artists found', 'info');
+ }
+
+ // Reset button after a delay
+ setTimeout(() => {
+ btn.classList.remove('all-added');
+ textSpan.textContent = originalText;
+ }, 5000);
+ } else {
+ throw new Error(data.error || 'Failed to add artists');
+ }
+ } catch (error) {
+ console.error('Error bulk adding unwatched artists:', error);
+ btn.classList.remove('adding');
+ textSpan.textContent = originalText;
+ showToast('Failed to add artists to watchlist', 'error');
+ }
+}
+
+async function toggleLibraryCardWatchlist(btn, artist) {
+ if (btn.disabled) return;
+ btn.disabled = true;
+
+ const icon = btn.querySelector('.watchlist-icon');
+ const text = btn.querySelector('.watchlist-text');
+ const isWatching = btn.classList.contains('watching');
+
+ text.textContent = '...';
+
+ try {
+ // Determine external ID: prefer iTunes, fallback Spotify
+ const artistId = artist.itunes_artist_id || artist.spotify_artist_id;
+
+ if (isWatching) {
+ const response = await fetch('/api/watchlist/remove', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ artist_id: artistId })
+ });
+ const data = await response.json();
+ if (!data.success) throw new Error(data.error);
+
+ btn.classList.remove('watching');
+ btn.title = 'Add to Watchlist';
+ text.textContent = 'Watch';
+ showToast(`Removed ${artist.name} from watchlist`, 'success');
+ } else {
+ const response = await fetch('/api/watchlist/add', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ artist_id: artistId, artist_name: artist.name })
+ });
+ const data = await response.json();
+ if (!data.success) throw new Error(data.error);
+
+ btn.classList.add('watching');
+ btn.title = 'Remove from Watchlist';
+ text.textContent = 'Watching';
+ showToast(`Added ${artist.name} to watchlist`, 'success');
+ }
+
+ if (typeof updateWatchlistCount === 'function') {
+ updateWatchlistCount();
+ }
+ } catch (error) {
+ console.error('Error toggling library card watchlist:', error);
+ text.textContent = isWatching ? 'Watching' : 'Watch';
+ showToast(`Error: ${error.message}`, 'error');
+ } finally {
+ btn.disabled = false;
+ }
+}
+
// ===============================================
// Artist Detail Page Functions
// ===============================================
diff --git a/webui/static/style.css b/webui/static/style.css
index ee4f01c0..429b776c 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -13509,6 +13509,50 @@ body {
background: rgba(var(--accent-rgb), 0.15);
}
+.library-watchlist-all-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 16px;
+ border: 1px solid rgba(var(--accent-rgb), 0.4);
+ background: rgba(var(--accent-rgb), 0.12);
+ color: rgba(var(--accent-rgb), 1);
+ font-size: 13px;
+ font-weight: 600;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', system-ui, sans-serif;
+ cursor: pointer;
+ border-radius: 20px;
+ transition: all 0.2s ease-out;
+ margin-left: 4px;
+}
+
+.library-watchlist-all-btn:hover {
+ background: rgba(var(--accent-rgb), 0.25);
+ border-color: rgba(var(--accent-rgb), 0.6);
+ transform: scale(1.02);
+}
+
+.library-watchlist-all-btn.adding {
+ opacity: 0.7;
+ pointer-events: none;
+ cursor: wait;
+}
+
+.library-watchlist-all-btn.all-added {
+ background: rgba(var(--accent-rgb), 0.2);
+ border-color: rgba(var(--accent-rgb), 0.5);
+ color: #fff;
+ pointer-events: none;
+}
+
+.library-watchlist-all-btn .watchlist-all-icon {
+ font-size: 14px;
+}
+
+.library-watchlist-all-btn.hidden {
+ display: none;
+}
+
.alphabet-selector {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
@@ -25410,6 +25454,66 @@ body {
pointer-events: none;
}
+.library-card-watchlist-btn {
+ position: absolute;
+ right: 8px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 10px;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(4px);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 12px;
+ color: #e0e0e0;
+ font-size: 11px;
+ font-weight: 600;
+ font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
+ cursor: pointer;
+ z-index: 100;
+ transition: all 0.2s ease;
+ opacity: 0;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+}
+
+.library-artist-card:hover .library-card-watchlist-btn {
+ opacity: 1;
+}
+
+.library-card-watchlist-btn:hover {
+ background: rgba(var(--accent-rgb), 0.25);
+ border-color: rgba(var(--accent-rgb), 0.4);
+ color: #fff;
+ transform: scale(1.05);
+}
+
+.library-card-watchlist-btn.watching {
+ background: rgba(255, 193, 7, 0.2);
+ color: #ffc107;
+ border-color: rgba(255, 193, 7, 0.4);
+ opacity: 1;
+}
+
+.library-card-watchlist-btn.watching:hover {
+ background: rgba(255, 193, 7, 0.35);
+ border-color: rgba(255, 193, 7, 0.6);
+ color: #fff;
+}
+
+.library-card-watchlist-btn:disabled {
+ opacity: 0.5;
+ cursor: wait;
+ transform: none;
+}
+
+.library-card-watchlist-btn .watchlist-icon {
+ font-size: 12px;
+}
+
+.library-card-watchlist-btn .watchlist-text {
+ line-height: 1;
+}
+
/* ============================= */
/* IMPORT MODAL */
/* ============================= */