include 'add to watchlist' button to each artist in library.
This commit is contained in:
parent
b558dff138
commit
df33adf6a7
6 changed files with 328 additions and 1 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ===============================================
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
/* ============================= */
|
||||
|
|
|
|||
Loading…
Reference in a new issue