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,
|
'image_url': artist.thumb_url,
|
||||||
'genres': artist.genres,
|
'genres': artist.genres,
|
||||||
'musicbrainz_id': row['musicbrainz_id'],
|
'musicbrainz_id': row['musicbrainz_id'],
|
||||||
|
'spotify_artist_id': row['spotify_artist_id'],
|
||||||
|
'itunes_artist_id': row['itunes_artist_id'],
|
||||||
'deezer_id': row['deezer_id'],
|
'deezer_id': row['deezer_id'],
|
||||||
'audiodb_id': row['audiodb_id'],
|
'audiodb_id': row['audiodb_id'],
|
||||||
'album_count': row['album_count'] or 0,
|
'album_count': row['album_count'] or 0,
|
||||||
|
|
|
||||||
|
|
@ -21541,6 +21541,81 @@ def add_batch_to_watchlist():
|
||||||
print(f"Error batch adding to watchlist: {e}")
|
print(f"Error batch adding to watchlist: {e}")
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
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'])
|
@app.route('/api/watchlist/remove-batch', methods=['POST'])
|
||||||
def remove_batch_from_watchlist():
|
def remove_batch_from_watchlist():
|
||||||
"""Remove multiple artists from the 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 active" data-filter="all">All</button>
|
||||||
<button class="watchlist-filter-btn" data-filter="watched">Watched</button>
|
<button class="watchlist-filter-btn" data-filter="watched">Watched</button>
|
||||||
<button class="watchlist-filter-btn" data-filter="unwatched">Unwatched</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>
|
</div>
|
||||||
|
|
||||||
<!-- Alphabet Selector -->
|
<!-- Alphabet Selector -->
|
||||||
|
|
|
||||||
|
|
@ -870,6 +870,19 @@
|
||||||
max-width: 100%;
|
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 {
|
.alphabet-selector-inner {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
||||||
|
|
@ -27551,6 +27551,7 @@ function initializeLibrarySearch() {
|
||||||
|
|
||||||
function initializeWatchlistFilter() {
|
function initializeWatchlistFilter() {
|
||||||
const filterButtons = document.querySelectorAll(".watchlist-filter-btn");
|
const filterButtons = document.querySelectorAll(".watchlist-filter-btn");
|
||||||
|
const watchAllBtn = document.getElementById("library-watchlist-all-btn");
|
||||||
|
|
||||||
filterButtons.forEach(button => {
|
filterButtons.forEach(button => {
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
|
|
@ -27560,6 +27561,15 @@ function initializeWatchlistFilter() {
|
||||||
filterButtons.forEach(btn => btn.classList.remove("active"));
|
filterButtons.forEach(btn => btn.classList.remove("active"));
|
||||||
button.classList.add("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
|
// Update state and reload
|
||||||
libraryPageState.watchlistFilter = filter;
|
libraryPageState.watchlistFilter = filter;
|
||||||
libraryPageState.currentPage = 1;
|
libraryPageState.currentPage = 1;
|
||||||
|
|
@ -27710,7 +27720,8 @@ function createLibraryArtistCard(artist) {
|
||||||
badgeOffset += 32;
|
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) {
|
if (artist.is_watched) {
|
||||||
const watchIcon = document.createElement('div');
|
const watchIcon = document.createElement('div');
|
||||||
watchIcon.className = 'watchlist-card-icon';
|
watchIcon.className = 'watchlist-card-icon';
|
||||||
|
|
@ -27718,6 +27729,17 @@ function createLibraryArtistCard(artist) {
|
||||||
watchIcon.style.top = badgeOffset + 'px';
|
watchIcon.style.top = badgeOffset + 'px';
|
||||||
watchIcon.textContent = '👁️';
|
watchIcon.textContent = '👁️';
|
||||||
card.appendChild(watchIcon);
|
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
|
// 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
|
// Artist Detail Page Functions
|
||||||
// ===============================================
|
// ===============================================
|
||||||
|
|
|
||||||
|
|
@ -13509,6 +13509,50 @@ body {
|
||||||
background: rgba(var(--accent-rgb), 0.15);
|
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 {
|
.alphabet-selector {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
@ -25410,6 +25454,66 @@ body {
|
||||||
pointer-events: none;
|
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 */
|
/* IMPORT MODAL */
|
||||||
/* ============================= */
|
/* ============================= */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue