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' },