From e84d187e765b80fcda6ec18a417ce5fa0121f346 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 3 May 2026 20:52:44 -0700 Subject: [PATCH] Drop redundant standalone "Your Spotify Library" section on Discover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discover page used to show two near-identical sections: - "Your Albums" — cross-source aggregator across Spotify / Deezer / etc with a gear button to configure sources, search, status filter, sort options, and a download-missing action. - "Your Spotify Library" — Spotify-only with the same grid UI, same refresh / download-missing buttons, same filter / sort controls. The Spotify-only section was a strict subset of what Your Albums already covers (Spotify is one of the configurable sources). User flagged the redundancy when scoping the upcoming Discogs integration and asked for the duplicate to be removed. Removal scope: - `webui/index.html` — drop the `#spotify-library-section` block (42 lines). - `webui/static/discover.js` — drop the dead JS (~335 lines): state vars `spotifyLibraryAlbums` / `spotifyLibraryPage` / etc, all the loaders / renderers / pagination / click handlers, and the `loadSpotifyLibrarySection()` call in `loadDiscoverPage`'s Promise.all. - `webui/static/helper.js` — drop the helper annotation entry at `#spotify-library-section` and the matching guided-tour entry. Backend untouched. The Spotify saved-albums cache (`spotify_library_albums` table + watchlist_scanner upsert/cleanup + `/api/discover/spotify-library` endpoint + the DAO methods) is shared infrastructure that Your Albums reads from when Spotify is one of its configured sources. Removing the UI section just removes the duplicate surface — Spotify saved albums still appear in Your Albums via the existing source dispatch. CSS class names (`.spotify-library-grid`, `.spotify-library-search`, `.spotify-library-pagination`) intentionally remain on the surviving Your Albums elements — they share the same visual styling and renaming would be churn for no benefit. Verified: full suite 1813 pass (no new tests — pure UI/dead-code removal). Backend endpoint behavior unchanged. WHATS_NEW entry under '2.4.2' dev cycle. --- webui/index.html | 42 ----- webui/static/discover.js | 336 --------------------------------------- webui/static/helper.js | 13 +- 3 files changed, 1 insertion(+), 390 deletions(-) diff --git a/webui/index.html b/webui/index.html index f0f8e908..bf7cb3ff 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2913,48 +2913,6 @@ - - -
diff --git a/webui/static/discover.js b/webui/static/discover.js index eb50d2ba..ebe426a1 100644 --- a/webui/static/discover.js +++ b/webui/static/discover.js @@ -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 = '

Loading...

'; - - 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 = '

Failed to load albums

'; - } -} - -function renderSpotifyLibraryGrid(albums) { - const grid = document.getElementById('spotify-library-grid'); - if (!grid) return; - - if (!albums || albums.length === 0) { - grid.innerHTML = '

No albums found

'; - 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 += ` -
-
- ${album.album_name} -
${badgeIcon}
-
-
-

${album.album_name}

-

${album.artist_name}

-

${meta}

-
-
- `; - }); - - 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 = ` - - ${offset + 1}\u2013${showEnd} of ${total} - - `; -} - -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 { diff --git a/webui/static/helper.js b/webui/static/helper.js index 7729ce9e..2dc6b0ca 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -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' },