include 'add to watchlist' button to each artist in library.

This commit is contained in:
Broque Thomas 2026-03-02 10:04:10 -08:00
parent b558dff138
commit df33adf6a7
6 changed files with 328 additions and 1 deletions

View file

@ -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,

View file

@ -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"""

View file

@ -1902,6 +1902,10 @@
<button class="watchlist-filter-btn active" data-filter="all">All</button>
<button class="watchlist-filter-btn" data-filter="watched">Watched</button>
<button class="watchlist-filter-btn" data-filter="unwatched">Unwatched</button>
<button class="library-watchlist-all-btn hidden" id="library-watchlist-all-btn" onclick="addAllUnwatchedToWatchlist(this)">
<span class="watchlist-all-icon">👁️</span>
<span class="watchlist-all-text">Watch All Unwatched</span>
</button>
</div>
<!-- Alphabet Selector -->

View file

@ -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;

View file

@ -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 = '<span class="watchlist-icon">👁️</span><span class="watchlist-text">Watch</span>';
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
// ===============================================

View file

@ -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 */
/* ============================= */