diff --git a/js/app.js b/js/app.js index 35de81c..dfafd45 100644 --- a/js/app.js +++ b/js/app.js @@ -8,7 +8,7 @@ import { LastFMScrobbler } from './lastfm.js'; import { LyricsManager, createLyricsPanel, showKaraokeView, showSyncedLyricsPanel, clearLyricsPanelSync } from './lyrics.js'; import { createRouter, updateTabTitle } from './router.js'; import { initializeSettings } from './settings.js'; -import { initializePlayerEvents, initializeTrackInteractions } from './events.js'; +import { initializePlayerEvents, initializeTrackInteractions, handleTrackAction } from './events.js'; import { initializeUIInteractions } from './ui-interactions.js'; import { downloadAlbumAsZip, downloadDiscography, downloadPlaylistAsZip } from './downloads.js'; import { debounce, SVG_PLAY } from './utils.js'; @@ -275,7 +275,7 @@ document.addEventListener('DOMContentLoaded', async () => { document.getElementById('download-current-btn')?.addEventListener('click', () => { if (player.currentTrack) { - downloadTrackWithMetadata(player.currentTrack, player.quality, api, lyricsManager); + handleTrackAction('download', player.currentTrack, player, api, lyricsManager); } }); diff --git a/js/downloads.js b/js/downloads.js index 7df322d..10ac3d4 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -4,6 +4,39 @@ import { lyricsSettings } from './storage.js'; const downloadTasks = new Map(); let downloadNotificationContainer = null; +const coverCache = new Map(); + +/** + * Fetches and caches cover art as a Blob + */ +async function getCoverBlob(api, coverId) { + if (!coverId) return null; + if (coverCache.has(coverId)) return coverCache.get(coverId); + + try { + const url = api.getCoverUrl(coverId, '1280'); + const response = await fetch(url); + if (response.ok) { + const blob = await response.blob(); + coverCache.set(coverId, blob); + return blob; + } + } catch (error) { + console.warn('Cover fetch failed:', error); + } + return null; +} + +/** + * Adds a cover blob to a JSZip instance + */ +function addCoverBlobToZip(zip, folderPath, blob) { + if (!blob) return; + const path = folderPath ? `${folderPath}/cover.jpg` : 'cover.jpg'; + if (!zip.file(path)) { + zip.file(path, blob); + } +} async function loadJSZip() { try { @@ -148,7 +181,7 @@ function removeDownloadTask(trackId) { taskEl.remove(); downloadTasks.delete(trackId); - if (downloadNotificationContainer && downloadNotificationContainer.children.length === 0) { + if (downloadNotificationContainer && downloadNotificationContainer.children.length === 0) { downloadNotificationContainer.remove(); downloadNotificationContainer = null; } @@ -177,55 +210,16 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null) { return blob; } -function buildTrackMetadata(track, api) { - const artists = []; - if (Array.isArray(track.artists) && track.artists.length) { - for (const a of track.artists) artists.push(a.name || a); - } else if (track.artist && track.artist.name) { - artists.push(track.artist.name); - } - - return { - id: track.id, - title: track.title || null, - artists, - album: track.album?.title || null, - albumArtist: track.album?.artist?.name || track.artist?.name || null, - trackNumber: track.trackNumber ?? null, - discNumber: track.discNumber ?? null, - durationMs: track.duration ?? null, - releaseDate: track.album?.releaseDate || null, - bitrate: track.audioQuality || null - }; -} - -async function addCoverToZipIfMissing(zip, folderPath, coverId, api) { - if (!coverId) return; - - const coverPath = folderPath ? `${folderPath}/cover.jpg` : 'cover.jpg'; - if (zip.file(coverPath)) return; - - try { - const url = api.getCoverUrl(coverId, '1000'); - const resp = await fetch(url); - if (!resp.ok) return; - const blob = await resp.blob(); - zip.file(coverPath, blob); - } catch (e) { - - console.warn('Could not fetch cover for zip:', e); - } -} - export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsManager = null) { const JSZip = await loadJSZip(); const zip = new JSZip(); - const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf'; - const releaseDate = album.releaseDate ? new Date(album.releaseDate) : null; + const coverBlob = await getCoverBlob(api, album.cover || album.album?.cover || album.coverId); + const releaseDateStr = album.releaseDate || (tracks[0]?.streamStartDate ? tracks[0].streamStartDate.split('T')[0] : ''); + const releaseDate = releaseDateStr ? new Date(releaseDateStr) : null; const year = (releaseDate && !isNaN(releaseDate.getTime())) ? releaseDate.getFullYear() : ''; - const folderName = formatTemplate(template, { + const folderName = formatTemplate(localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist}', { albumTitle: album.title, albumArtist: album.artist?.name, year: year @@ -234,8 +228,7 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana const notification = createBulkDownloadNotification('album', album.title, tracks.length); try { - - const albumCoverId = album.cover || album.album?.cover || album.coverId || null; + addCoverBlobToZip(zip, folderName, coverBlob); for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; @@ -281,7 +274,7 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana } } - updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...'); + updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...'); const zipBlob = await zip.generateAsync({ type: 'blob', @@ -305,18 +298,17 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana } } -export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyricsManager = null) { +export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyricsManager = null) { const JSZip = await loadJSZip(); const zip = new JSZip(); - const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf'; - const folderName = formatTemplate(template, { + const folderName = formatTemplate(localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist}', { albumTitle: playlist.title, albumArtist: 'Playlist', year: new Date().getFullYear() }); - const notification = createBulkDownloadNotification('playlist', playlist.title, tracks.length); + const notification = createBulkDownloadNotification('playlist', playlist.title, tracks.length); try { for (let i = 0; i < tracks.length; i++) { @@ -327,10 +319,11 @@ export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyri updateBulkDownloadProgress(notification, i, tracks.length, trackTitle); try { + const coverBlob = await getCoverBlob(api, track.album?.cover); const blob = await downloadTrackBlob(track, quality, api); zip.file(`${folderName}/${filename}`, blob); - - // add metadata JSON + + addCoverBlobToZip(zip, folderName, coverBlob); try { const meta = buildTrackMetadata(track, api); const metaFilename = filename.replace(/\.[^.]+$/, '.json'); @@ -363,7 +356,7 @@ export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyri } } - updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...'); + updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...'); const zipBlob = await zip.generateAsync({ type: 'blob', @@ -391,8 +384,7 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = const JSZip = await loadJSZip(); const zip = new JSZip(); - const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf'; - const rootFolder = `${sanitizeForFilename(artist.name)} discography - monochrome.tf`; + const rootFolder = `${sanitizeForFilename(artist.name)} discography`; const allReleases = [...(artist.albums || []), ...(artist.eps || [])]; const totalAlbums = allReleases.length; @@ -406,7 +398,10 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = try { const { album: fullAlbum, tracks } = await api.getAlbum(album.id); - const releaseDate = fullAlbum.releaseDate ? new Date(fullAlbum.releaseDate) : null; + const coverBlob = await getCoverBlob(api, fullAlbum.cover || album.cover); + + const releaseDateStr = fullAlbum.releaseDate || (tracks[0]?.streamStartDate ? tracks[0].streamStartDate.split('T')[0] : ''); + const releaseDate = releaseDateStr ? new Date(releaseDateStr) : null; const year = (releaseDate && !isNaN(releaseDate.getTime())) ? releaseDate.getFullYear() : ''; const albumFolder = formatTemplate(template, { @@ -415,6 +410,8 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = year: year }); + addCoverBlobToZip(zip, `${rootFolder}/${albumFolder}`, coverBlob); + for (const track of tracks) { const filename = buildTrackFilename(track, quality); @@ -422,13 +419,7 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = const blob = await downloadTrackBlob(track, quality, api); zip.file(`${rootFolder}/${albumFolder}/${filename}`, blob); - try { - const meta = buildTrackMetadata(track, api); - const metaFilename = filename.replace(/\.[^.]+$/, '.json'); - zip.file(`${rootFolder}/${albumFolder}/${metaFilename}`, JSON.stringify(meta, null, 2)); - } catch (e) { - console.warn('Could not attach metadata for', track.title, e); - } + try { await addCoverToZipIfMissing(zip, `${rootFolder}/${albumFolder}`, track.album?.cover || album.cover, api); @@ -487,7 +478,7 @@ function createBulkDownloadNotification(type, name, totalItems) { const notifEl = document.createElement('div'); notifEl.className = 'download-task bulk-download'; - const typeLabel = type === 'album' ? 'Album' : type === 'playlist' ? 'Playlist' : 'Discography'; + const typeLabel = type === 'album' ? 'Album' : type === 'playlist' ? 'Playlist' : 'Discography'; notifEl.innerHTML = `