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)
pull/253/head
Broque Thomas 2 months ago
parent 0742cc45e6
commit da2b42b59a

@ -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');

@ -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;

Loading…
Cancel
Save