Merge pull request #488 from Nezreka/chore/drop-redundant-spotify-library-section

Drop redundant standalone "Your Spotify Library" section on Discover
This commit is contained in:
BoulderBadgeDad 2026-05-03 20:55:23 -07:00 committed by GitHub
commit 8b41670717
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1 additions and 390 deletions

View file

@ -2913,48 +2913,6 @@
<div class="spotify-library-pagination" id="your-albums-pagination" style="display: none;"></div>
</div>
<!-- Spotify Library Section -->
<div class="discover-section" id="spotify-library-section" style="display: none;">
<div class="discover-section-header">
<div>
<h2 class="discover-section-title">Your Spotify Library</h2>
<p class="discover-section-subtitle" id="spotify-library-subtitle">Your saved albums on Spotify</p>
</div>
<div class="spotify-library-header-actions">
<button class="spotify-library-action-btn spotify-library-refresh-btn" onclick="refreshSpotifyLibraryCache()" title="Refresh from Spotify">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></svg>
Refresh
</button>
<button class="spotify-library-action-btn spotify-library-download-btn" id="spotify-library-download-missing-btn" onclick="downloadMissingSpotifyLibraryAlbums()" style="display: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
Download Missing
</button>
</div>
</div>
<div class="spotify-library-filters" id="spotify-library-filters" style="display: none;">
<input type="text" class="spotify-library-search" id="spotify-library-search"
placeholder="Search by artist or album..." oninput="debouncedSpotifyLibrarySearch()">
<select id="spotify-library-status-filter" class="spotify-library-select" onchange="loadSpotifyLibraryAlbums()">
<option value="all">All Albums</option>
<option value="missing">Missing</option>
<option value="owned">Owned</option>
</select>
<select id="spotify-library-sort" class="spotify-library-select" onchange="loadSpotifyLibraryAlbums()">
<option value="date_saved">Date Saved</option>
<option value="artist_name">Artist</option>
<option value="album_name">Album</option>
<option value="release_date">Release Date</option>
</select>
</div>
<div class="spotify-library-grid" id="spotify-library-grid">
<div class="discover-loading">
<div class="loading-spinner"></div>
<p>Loading your Spotify library...</p>
</div>
</div>
<div class="spotify-library-pagination" id="spotify-library-pagination" style="display: none;"></div>
</div>
<!-- Recent Releases Section -->
<div class="discover-section">
<div class="discover-section-header">

View file

@ -33,7 +33,6 @@ async function loadDiscoverPage() {
loadDiscoverHero(),
loadYourArtists(),
loadYourAlbums(),
loadSpotifyLibrarySection(),
loadDiscoverRecentReleases(),
loadSeasonalContent(), // Seasonal discovery
loadPersonalizedRecentlyAdded(), // NEW: Recently added from library
@ -1292,341 +1291,6 @@ async function downloadMissingYourAlbums() {
}
}
// ===============================
// SPOTIFY LIBRARY SECTION
// ===============================
let spotifyLibraryAlbums = [];
let spotifyLibraryPage = 0;
let spotifyLibraryTotal = 0;
const SPOTIFY_LIBRARY_PAGE_SIZE = 48;
let _spotifyLibrarySearchTimeout = null;
function debouncedSpotifyLibrarySearch() {
clearTimeout(_spotifyLibrarySearchTimeout);
_spotifyLibrarySearchTimeout = setTimeout(() => {
spotifyLibraryPage = 0;
loadSpotifyLibraryAlbums();
}, 400);
}
async function loadSpotifyLibrarySection() {
try {
const section = document.getElementById('spotify-library-section');
if (!section) return;
const response = await fetch(`/api/discover/spotify-library?offset=0&limit=${SPOTIFY_LIBRARY_PAGE_SIZE}`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (!data.success || !data.albums || data.albums.length === 0) {
section.style.display = 'none';
return;
}
section.style.display = '';
spotifyLibraryAlbums = data.albums;
spotifyLibraryTotal = data.total;
spotifyLibraryPage = 0;
// Update subtitle with stats
const subtitle = document.getElementById('spotify-library-subtitle');
if (subtitle && data.stats) {
const s = data.stats;
subtitle.textContent = `${s.total} albums \u00B7 ${s.owned} owned \u00B7 ${s.missing} missing`;
}
// Show download missing button if there are missing albums
const dlBtn = document.getElementById('spotify-library-download-missing-btn');
if (dlBtn && data.stats && data.stats.missing > 0) {
dlBtn.style.display = '';
}
// Show filters
const filters = document.getElementById('spotify-library-filters');
if (filters) filters.style.display = '';
renderSpotifyLibraryGrid(data.albums);
renderSpotifyLibraryPagination(data.total, 0);
} catch (error) {
console.error('Error loading Spotify library section:', error);
const section = document.getElementById('spotify-library-section');
if (section) section.style.display = 'none';
}
}
async function loadSpotifyLibraryAlbums() {
const grid = document.getElementById('spotify-library-grid');
if (!grid) return;
grid.innerHTML = '<div class="discover-loading"><div class="loading-spinner"></div><p>Loading...</p></div>';
try {
const search = (document.getElementById('spotify-library-search')?.value || '').trim();
const status = document.getElementById('spotify-library-status-filter')?.value || 'all';
const sort = document.getElementById('spotify-library-sort')?.value || 'date_saved';
const offset = spotifyLibraryPage * SPOTIFY_LIBRARY_PAGE_SIZE;
const params = new URLSearchParams({
offset, limit: SPOTIFY_LIBRARY_PAGE_SIZE, sort, sort_dir: 'desc', status
});
if (search) params.set('search', search);
const response = await fetch(`/api/discover/spotify-library?${params}`);
const data = await response.json();
if (!data.success) throw new Error(data.error);
spotifyLibraryAlbums = data.albums;
spotifyLibraryTotal = data.total;
// Update subtitle
const subtitle = document.getElementById('spotify-library-subtitle');
if (subtitle && data.stats) {
const s = data.stats;
subtitle.textContent = `${s.total} albums \u00B7 ${s.owned} owned \u00B7 ${s.missing} missing`;
}
renderSpotifyLibraryGrid(data.albums);
renderSpotifyLibraryPagination(data.total, offset);
} catch (error) {
console.error('Error loading Spotify library albums:', error);
grid.innerHTML = '<div class="spotify-library-empty"><p>Failed to load albums</p></div>';
}
}
function renderSpotifyLibraryGrid(albums) {
const grid = document.getElementById('spotify-library-grid');
if (!grid) return;
if (!albums || albums.length === 0) {
grid.innerHTML = '<div class="spotify-library-empty"><p>No albums found</p></div>';
return;
}
let html = '';
albums.forEach((album, index) => {
const coverUrl = album.image_url || '/static/placeholder-album.png';
const year = album.release_date ? album.release_date.substring(0, 4) : '';
const badgeClass = album.in_library ? 'owned' : 'missing';
const badgeIcon = album.in_library ? '\u2713' : '\u2193';
const trackInfo = album.total_tracks ? `${album.total_tracks} tracks` : '';
const meta = [year, trackInfo].filter(Boolean).join(' \u00B7 ');
html += `
<div class="spotify-library-card" onclick="openSpotifyLibraryAlbumDownload(${index})" title="${album.album_name} — ${album.artist_name}">
<div class="spotify-library-card-img">
<img src="${coverUrl}" alt="${album.album_name}" loading="lazy">
<div class="spotify-library-card-badge ${badgeClass}">${badgeIcon}</div>
</div>
<div class="spotify-library-card-info">
<p class="spotify-library-card-title">${album.album_name}</p>
<p class="spotify-library-card-artist">${album.artist_name}</p>
<p class="spotify-library-card-meta">${meta}</p>
</div>
</div>
`;
});
grid.innerHTML = html;
}
function renderSpotifyLibraryPagination(total, offset) {
const container = document.getElementById('spotify-library-pagination');
if (!container) return;
if (total <= SPOTIFY_LIBRARY_PAGE_SIZE) {
container.style.display = 'none';
return;
}
container.style.display = '';
const totalPages = Math.ceil(total / SPOTIFY_LIBRARY_PAGE_SIZE);
const currentPage = Math.floor(offset / SPOTIFY_LIBRARY_PAGE_SIZE) + 1;
const showEnd = Math.min(offset + SPOTIFY_LIBRARY_PAGE_SIZE, total);
container.innerHTML = `
<button class="spotify-library-page-btn" onclick="spotifyLibraryPrevPage()" ${currentPage <= 1 ? 'disabled' : ''}>&larr; Previous</button>
<span class="spotify-library-page-info">${offset + 1}\u2013${showEnd} of ${total}</span>
<button class="spotify-library-page-btn" onclick="spotifyLibraryNextPage()" ${currentPage >= totalPages ? 'disabled' : ''}>Next &rarr;</button>
`;
}
function spotifyLibraryPrevPage() {
if (spotifyLibraryPage > 0) {
spotifyLibraryPage--;
loadSpotifyLibraryAlbums();
}
}
function spotifyLibraryNextPage() {
const totalPages = Math.ceil(spotifyLibraryTotal / SPOTIFY_LIBRARY_PAGE_SIZE);
if (spotifyLibraryPage < totalPages - 1) {
spotifyLibraryPage++;
loadSpotifyLibraryAlbums();
}
}
async function openSpotifyLibraryAlbumDownload(index) {
const album = spotifyLibraryAlbums[index];
if (!album) {
showToast('Album data not found', 'error');
return;
}
console.log(`\u{1F4E5} Opening download modal for Spotify library album: ${album.album_name}`);
showLoadingOverlay(`Loading tracks for ${album.album_name}...`);
try {
const _params = new URLSearchParams({ name: album.album_name || '', artist: album.artist_name || '' });
const response = await fetch(`/api/discover/album/spotify/${album.spotify_album_id}?${_params}`);
if (!response.ok) throw new Error('Failed to fetch album tracks');
const albumData = await response.json();
if (!albumData.tracks || albumData.tracks.length === 0) {
throw new Error('No tracks found in album');
}
const spotifyTracks = albumData.tracks.map(track => {
let artists = track.artists || albumData.artists || [{ name: album.artist_name }];
if (Array.isArray(artists)) {
artists = artists.map(a => a.name || a);
}
return {
id: track.id,
name: track.name,
artists: artists,
album: {
id: albumData.id,
name: albumData.name,
album_type: albumData.album_type || 'album',
total_tracks: albumData.total_tracks || 0,
release_date: albumData.release_date || '',
images: albumData.images || []
},
duration_ms: track.duration_ms || 0,
track_number: track.track_number || 0
};
});
const virtualPlaylistId = `spotify_library_${album.spotify_album_id}`;
const artistContext = {
id: album.artist_id,
name: album.artist_name,
source: 'spotify'
};
const albumContext = {
id: albumData.id,
name: albumData.name,
album_type: albumData.album_type || 'album',
total_tracks: albumData.total_tracks || 0,
release_date: albumData.release_date || '',
images: albumData.images || []
};
await openDownloadMissingModalForYouTube(virtualPlaylistId, albumData.name, spotifyTracks, artistContext, albumContext);
hideLoadingOverlay();
} catch (error) {
console.error('Error opening Spotify library album download:', error);
showToast(`Failed to load album: ${error.message}`, 'error');
hideLoadingOverlay();
}
}
async function refreshSpotifyLibraryCache() {
try {
showToast('Refreshing Spotify library...', 'info');
const response = await fetch('/api/discover/spotify-library/refresh', { method: 'POST' });
const data = await response.json();
if (data.success) {
showToast('Spotify library refresh started — will update shortly', 'success');
// Reload after a delay to let the sync run
setTimeout(() => loadSpotifyLibrarySection(), 10000);
} else {
showToast(`Error: ${data.error}`, 'error');
}
} catch (error) {
showToast(`Error: ${error.message}`, 'error');
}
}
async function downloadMissingSpotifyLibraryAlbums() {
// Fetch all missing albums (no pagination limit)
try {
const response = await fetch('/api/discover/spotify-library?status=missing&limit=500&offset=0');
const data = await response.json();
if (!data.success || !data.albums || data.albums.length === 0) {
showToast('No missing albums to download', 'info');
return;
}
const missing = data.albums.filter(a => !a.in_library);
if (missing.length === 0) {
showToast('All albums are already in your library!', 'success');
return;
}
if (!confirm(`Download ${missing.length} missing album${missing.length > 1 ? 's' : ''} from your Spotify library?`)) {
return;
}
showToast(`Starting download for ${missing.length} albums...`, 'info');
// Download one at a time to avoid overwhelming the system
for (let i = 0; i < missing.length; i++) {
const album = missing[i];
try {
showToast(`Queuing ${i + 1}/${missing.length}: ${album.album_name}`, 'info');
const _params = new URLSearchParams({ name: album.album_name || '', artist: album.artist_name || '' });
const response = await fetch(`/api/discover/album/spotify/${album.spotify_album_id}?${_params}`);
if (!response.ok) continue;
const albumData = await response.json();
if (!albumData.tracks || albumData.tracks.length === 0) continue;
const spotifyTracks = albumData.tracks.map(track => {
let artists = track.artists || albumData.artists || [{ name: album.artist_name }];
if (Array.isArray(artists)) artists = artists.map(a => a.name || a);
return {
id: track.id,
name: track.name,
artists: artists,
album: {
id: albumData.id,
name: albumData.name,
album_type: albumData.album_type || 'album',
total_tracks: albumData.total_tracks || 0,
release_date: albumData.release_date || '',
images: albumData.images || []
},
duration_ms: track.duration_ms || 0,
track_number: track.track_number || 0
};
});
const virtualPlaylistId = `spotify_library_${album.spotify_album_id}`;
await openDownloadMissingModalForYouTube(virtualPlaylistId, albumData.name, spotifyTracks, {
id: album.artist_id, name: album.artist_name, source: 'spotify'
}, {
id: albumData.id, name: albumData.name, album_type: albumData.album_type || 'album',
total_tracks: albumData.total_tracks || 0, release_date: albumData.release_date || '',
images: albumData.images || []
});
} catch (err) {
console.error(`Error downloading album ${album.album_name}:`, err);
}
}
} catch (error) {
console.error('Error downloading missing Spotify library albums:', error);
showToast(`Error: ${error.message}`, 'error');
}
}
async function loadDiscoverReleaseRadar() {
try {

View file

@ -1230,17 +1230,6 @@ const HELPER_CONTENT = {
description: 'Click to browse tracks in this genre from your discovery pool.',
},
// Spotify Library
'#spotify-library-section': {
title: 'Your Spotify Library',
description: 'Albums saved in your Spotify account. Browse, search, and download albums you\'ve saved on Spotify but don\'t have locally.',
tips: [
'Search and filter by status (All/Missing/Owned)',
'Sort by date saved, artist, album name, or release date',
'"Download Missing" downloads all albums not in your library'
],
},
// Playlist Sync/Download buttons (generic — matches all discover playlist sections)
'.discover-section-actions .action-button.primary': {
title: 'Sync to Media Server',
@ -2485,7 +2474,6 @@ const HELPER_TOURS = {
{ page: 'discover', selector: '#discover-hero-view-all', title: 'View All Recommendations', description: 'Opens a modal with all recommended artists at once. "Watch All" adds every recommended artist to your watchlist in one click.' },
// Content sections (top to bottom)
{ page: 'discover', selector: '#spotify-library-section', title: 'Your Spotify Library', description: 'If Spotify is connected, this shows all your saved albums. Filter by Missing/Owned, sort by date, and click "Download Missing" to grab everything you don\'t have yet. Only visible with Spotify connected.' },
{ page: 'discover', selector: '#recent-releases-carousel', title: 'Recent Releases', description: 'New music from artists in your watchlist. Album cards show cover art — click any to open the download modal. Updates automatically when watchlist scans find new releases.' },
{ page: 'discover', selector: '#seasonal-albums-section', title: 'Seasonal Content', description: 'Season-aware sections that appear automatically — Christmas albums in December, summer vibes in July. Includes curated albums and a Seasonal Mix playlist you can sync to your server.' },
@ -3444,6 +3432,7 @@ const WHATS_NEW = {
'2.4.2': [
// --- post-2.4.1 dev work — entries hidden by _getLatestWhatsNewVersion until the build version bumps ---
{ date: 'Unreleased — 2.4.2 dev cycle' },
{ title: 'Drop Redundant "Your Spotify Library" Section on Discover', desc: 'discover page used to show two near-identical sections: "Your Albums" (cross-source aggregator across spotify/deezer/etc) AND "Your Spotify Library" (spotify-only). same UI, same grid, same filter / sort / download-missing controls — the spotify-only one was a strict subset of what your albums already covers. removed it. spotify saved albums still surface via the your albums section with spotify as one of its configured sources (gear button → configure sources). backend collection / storage is unchanged — the watchlist scanner still populates the spotify_library_albums cache for your albums to read.', page: 'discover' },
{ title: 'Library Disk Usage on Stats Page', desc: 'discord request (samuel [KC]): show how much disk space the library takes. new card on stats → system statistics shows total bytes + per-format breakdown (FLAC vs MP3 vs M4A bars). data comes from `tracks.file_size` populated during deep scan from whatever the media server already returns (plex MediaPart.size, jellyfin MediaSources[].Size, navidrome song.size, soulsync standalone os.path.getsize) — zero filesystem walk overhead. existing libraries see "Run a Deep Scan to populate" until the next deep scan fills in sizes; partial coverage shown as "X tracks measured (+Y pending)". migration is additive (NULL on legacy rows) so upgrading users have nothing to do.', page: 'stats' },
{ title: 'Fix: ReplayGain Wrote Same +52 dB Gain to Every Track', desc: 'noticed every downloaded track came out with `replaygain_track_gain: +52.00 dB` regardless of actual loudness. cause: parser used `re.search` which returned the FIRST `I:` (integrated loudness) reading from ffmpeg\'s ebur128 output. that\'s the per-window measurement at t=0.5s — almost always ~-70 LUFS because tracks start with silence/encoder padding. -18 (RG2 reference) - (-70) = +52 dB on every track. fix: parser now anchors to the `Summary:` block at the end of ffmpeg\'s output and reads the actual integrated loudness from there, not the silent-intro partial. defensive fallback uses the LAST per-window reading if Summary is missing (still better than the first). gains now reflect real per-track loudness.', page: 'downloads' },
{ title: 'Fix: Tracks Showed Completed When File Was Quarantined', desc: 'caught downloading kendrick mr morale: three tracks (rich interlude, savior interlude, savior) showed ✅ completed in the modal but were missing on disk. two layered bugs. (1) the post-process verification wrapper had a fallback that assumed success when no `_final_processed_path` was in context — but integrity-rejected files (which get quarantined instead of moved) leave that path unset, so the wrapper marked them complete. now wrapper explicitly checks `_integrity_failure_msg` and `_race_guard_failed` markers before the assume-success fallback. failed integrity = task marked failed, batch tracker notified with success=false. (2) acoustid skip-logic was too lenient — when fingerprint confidence was very high and either title OR artist matched a bit, it skipped verification with reason "likely same song in different language/script." that fired for english-vs-english by the same artist with the word "interlude" in both — same artist + 0.55 title sim = skip = wrong file accepted. tightened: skip now requires non-ASCII chars present (real language/script case) AND artist match, OR very high title similarity (≥0.80) AND artist match. english-vs-english with very different titles by same artist no longer skipped — verification correctly returns FAIL and the wrong file gets quarantined.', page: 'downloads' },