From da2b42b59a2b0c4f25d5f8ec2afe3407e9ed1eb5 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 15 Mar 2026 19:15:46 -0700 Subject: [PATCH] Add Redownload button to enhanced library view & fix album download mode - Redownload button on each album in enhanced view (admin only) - Uses same flow as artist page: fetches API tracklist, opens Download Missing modal with force-download option - Register dashboard bubbles for library redownload and issue downloads - Add library_redownload_ prefix to album download whitelist so it uses 1 worker with source reuse and sends full album context (release_date for year in folder name) --- webui/static/script.js | 114 ++++++++++++++++++++++++++++++++++++++++- webui/static/style.css | 21 ++++++++ 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/webui/static/script.js b/webui/static/script.js index 9db71f63..621cfbad 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -11874,7 +11874,7 @@ async function startMissingTracksProcess(playlistId) { // If this is an artist album download, use album name and include full context // Match 'artist_album_', 'enhanced_search_album_', 'enhanced_search_track_', 'discover_album_', and 'seasonal_album_' prefixes - if (playlistId.startsWith('artist_album_') || playlistId.startsWith('enhanced_search_album_') || playlistId.startsWith('enhanced_search_track_') || playlistId.startsWith('discover_album_') || playlistId.startsWith('seasonal_album_') || playlistId.startsWith('spotify_library_') || playlistId.startsWith('issue_download_')) { + if (playlistId.startsWith('artist_album_') || playlistId.startsWith('enhanced_search_album_') || playlistId.startsWith('enhanced_search_track_') || playlistId.startsWith('discover_album_') || playlistId.startsWith('seasonal_album_') || playlistId.startsWith('spotify_library_') || playlistId.startsWith('issue_download_') || playlistId.startsWith('library_redownload_')) { requestBody.playlist_name = process.album?.name || process.playlist.name; requestBody.is_album_download = true; requestBody.album_context = process.album; // Full Spotify album object @@ -37085,6 +37085,17 @@ function renderExpandedAlbumHeader(album) { reorganizeBtn.onclick = (e) => { e.stopPropagation(); showReorganizeModal(album.id); }; enrichRow.appendChild(reorganizeBtn); + const redownloadBtn = document.createElement('button'); + redownloadBtn.className = 'enhanced-redownload-album-btn'; + redownloadBtn.innerHTML = '↻ Redownload'; + redownloadBtn.title = 'Redownload this album (opens Download Missing modal with force-download)'; + redownloadBtn.onclick = (e) => { + e.stopPropagation(); + const aName = artistDetailPageState.enhancedData ? artistDetailPageState.enhancedData.artist.name : ''; + redownloadLibraryAlbum(album, aName, redownloadBtn); + }; + enrichRow.appendChild(redownloadBtn); + const deleteAlbumBtn = document.createElement('button'); deleteAlbumBtn.className = 'enhanced-delete-album-btn'; deleteAlbumBtn.textContent = 'Delete Album'; @@ -55261,7 +55272,7 @@ async function issueDownloadAlbum(spotifyAlbumId, artistName, albumName) { })); const playlistName = `[${artistName}] ${albumData.name}`; - const artistObject = { id: null, name: artistName }; + const artistObject = { id: `issue_${artistName}`, name: artistName, image_url: '' }; const fullAlbumObject = { name: albumData.name, id: albumData.id, @@ -55276,6 +55287,10 @@ async function issueDownloadAlbum(spotifyAlbumId, artistName, albumName) { virtualPlaylistId, playlistName, enrichedTracks, fullAlbumObject, artistObject, true ); + // Register download bubble so it appears on the dashboard + const albumType = fullAlbumObject.album_type || 'album'; + registerArtistDownload(artistObject, fullAlbumObject, virtualPlaylistId, albumType); + } catch (error) { console.error('Issue download error:', error); showToast(`Error: ${error.message}`, 'error'); @@ -55284,6 +55299,101 @@ async function issueDownloadAlbum(spotifyAlbumId, artistName, albumName) { } } +// --- Redownload Library Album (Enhanced View) --- +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}`); + } + + // Fallback: search by name if no ID or direct fetch failed + 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 || [], + release_date: albumData.release_date, + total_tracks: albumData.total_tracks, + artists: albumData.artists || [{ name: artistName || '' }] + }; + + await openDownloadMissingModalForArtistAlbum( + virtualPlaylistId, playlistName, enrichedTracks, fullAlbumObject, artistObject, true + ); + + // Register download bubble so it appears on the dashboard + 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; } + } +} + // --- Issue Action: Add to Wishlist --- async function issueAddToWishlist(spotifyAlbumId, artistName, albumName) { const btn = document.getElementById('issue-action-wishlist'); diff --git a/webui/static/style.css b/webui/static/style.css index e4d6ff8f..e9579d5d 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -35928,6 +35928,27 @@ textarea.enhanced-meta-field-input { border-color: rgba(100, 149, 237, 0.35); } +.enhanced-redownload-album-btn { + padding: 6px 14px; + border-radius: 8px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + background: rgba(34, 197, 94, 0.06); + border: 1px solid rgba(34, 197, 94, 0.15); + color: rgba(34, 197, 94, 0.6); + transition: all 0.15s ease; +} +.enhanced-redownload-album-btn:hover { + background: rgba(34, 197, 94, 0.12); + color: rgba(34, 197, 94, 0.9); + border-color: rgba(34, 197, 94, 0.35); +} +.enhanced-redownload-album-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + /* ── Reorganize Modal ── */ .reorganize-modal { max-width: 900px;