diff --git a/webui/static/helper.js b/webui/static/helper.js index c1f6f3cf..69faa24a 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3415,6 +3415,7 @@ function closeHelperSearch() { const WHATS_NEW = { '2.6.2': [ { date: 'May 24, 2026 — 2.6.2 release' }, + { title: 'Fix: Redownload Album button on the enhanced artist view was dead', desc: 'the Redownload button on the enhanced artist-page album row was throwing a silent ReferenceError on click — no popup, no toast, no log line, button just did nothing. the underlying function was lost when script.js got split into 17 domain modules and nothing caught it for a while. restored. closes #699.' }, { title: 'iTunes / Apple Music link import', desc: 'new iTunes Link tab on the Sync page, between Deezer Link and YouTube. paste an Apple Music album, track, or playlist URL and SoulSync pulls the tracklist, runs it through the same discovery → sync → download flow as the other link tabs. handles the new Apple Music SPA token shape — token gets scraped from the JS bundle on first use and cached for 6 hours, with a 401 retry that refetches if Apple rotates mid-session.' }, { title: 'Auto-Sync: bulk schedule by source + custom interval columns', desc: 'each source group in the sidebar gets a Bulk button — opens a popover that lets you schedule every playlist in that group at a chosen interval in one click (1h / 2h / 4h / 8h / 12h / 16h / 24h / 48h / 72h / weekly) or "Custom interval…" for an arbitrary number of hours. also includes "Unschedule all" to clear a source\'s schedules. on the board itself, a schedule with a non-standard interval (e.g. 6h or 36h, created via Automations page or the custom prompt) now renders as its own dashed-border column instead of disappearing because it didn\'t match a hardcoded bucket.' }, { title: 'Auto-Sync manager: filter, failure indicators, history filters + load more', desc: 'a handful of UX additions on the Playlist Auto-Sync modal. sidebar gets a "Filter playlists…" search input so you can narrow down a long mirrored-playlist list. scheduled cards on the board now show a red ! badge when the last three pipeline runs all failed (yellow ⚠ if at least one of the last few failed) so chronically broken schedules surface visually instead of getting buried in the run-history tab. run history tab title shows a red error count when there are failed runs in the loaded window. the history tab itself gains All / Errors / Completed filter pills, a "Load more" footer that pulls another 50 entries, and a "Run pipeline again" button inside the expanded detail panel so you can re-trigger a specific playlist without leaving the modal. also dropped the "Discovered: completed" pill — `tracks_discovered` was a status string, not a count, and the same data is already in the before/after stats grid above.' }, diff --git a/webui/static/library.js b/webui/static/library.js index 7a4cae2a..4e5467bf 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -5299,6 +5299,99 @@ function _pollRedownloadProgress(taskId, overlay) { }, 300000); } +async function redownloadLibraryAlbum(album, artistName, btn) { + const albumName = album.title || ''; + const spotifyAlbumId = album.spotify_album_id || ''; + + if (!spotifyAlbumId && !albumName) { + showToast('No album ID or name available for redownload', 'warning'); + return; + } + + const origText = btn ? btn.innerHTML : ''; + try { + if (btn) { btn.disabled = true; btn.textContent = 'Loading...'; } + + let response; + if (spotifyAlbumId) { + const params = new URLSearchParams({ name: albumName, artist: artistName || '' }); + response = await fetch(`/api/spotify/album/${encodeURIComponent(spotifyAlbumId)}?${params}`); + } + + if (!response || !response.ok) { + const query = `${artistName || ''} ${albumName}`.trim(); + const searchResp = await fetch('/api/enhanced-search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }) + }); + if (!searchResp.ok) throw new Error('Album search failed'); + const searchData = await searchResp.json(); + const found = searchData.spotify_albums?.[0] || searchData.itunes_albums?.[0]; + if (!found || !found.id) { + showToast(`Could not find "${albumName}" by ${artistName || 'unknown'}`, 'warning'); + return; + } + const params = new URLSearchParams({ name: found.name || albumName, artist: found.artist || artistName || '' }); + response = await fetch(`/api/spotify/album/${encodeURIComponent(found.id)}?${params}`); + } + + if (!response.ok) throw new Error(`Failed to load album: ${response.status}`); + + const albumData = await response.json(); + if (!albumData || !albumData.tracks || albumData.tracks.length === 0) { + showToast(`No tracks found for "${albumName}"`, 'warning'); + return; + } + + const resolvedId = albumData.id || spotifyAlbumId || album.id; + const virtualPlaylistId = `library_redownload_${resolvedId}`; + const playlistName = `[${artistName || 'Unknown'}] ${albumData.name}`; + + const enrichedTracks = albumData.tracks.map(track => ({ + ...track, + album: { + name: albumData.name, + id: albumData.id, + album_type: albumData.album_type || 'album', + images: albumData.images || [], + release_date: albumData.release_date, + total_tracks: albumData.total_tracks + } + })); + + const enhancedArtist = artistDetailPageState.enhancedData?.artist; + const artistObject = { + id: artistDetailPageState.currentArtistId || `library_${artistName || album.id}`, + name: artistName || '', + image_url: enhancedArtist?.thumb_url || '' + }; + const fullAlbumObject = { + name: albumData.name, + id: albumData.id, + album_type: albumData.album_type || 'album', + images: albumData.images || [], + image_url: albumData.images?.[0]?.url || null, + release_date: albumData.release_date, + total_tracks: albumData.total_tracks, + artists: albumData.artists || [{ name: artistName || '' }] + }; + + await openDownloadMissingModalForArtistAlbum( + virtualPlaylistId, playlistName, enrichedTracks, fullAlbumObject, artistObject, true + ); + + const albumType = fullAlbumObject.album_type || 'album'; + registerArtistDownload(artistObject, fullAlbumObject, virtualPlaylistId, albumType); + + } catch (error) { + console.error('Redownload album error:', error); + showToast(`Error: ${error.message}`, 'error'); + } finally { + if (btn) { btn.disabled = false; btn.innerHTML = origText; } + } +} + async function deleteLibraryAlbum(albumId) { const choice = await _showAlbumDeleteDialog(); if (!choice) return;