From 2ae6b620f2030e671b25d3721c9af225ff0651b5 Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Fri, 26 Dec 2025 11:38:21 +0100 Subject: [PATCH 1/4] Improve playlist download stability by skipping failed tracks Added try-catch blocks in downloadPlaylistAsZip, downloadAlbumAsZip, and downloadDiscography to handle individual track download failures gracefully. Errors are now logged to the console, and the download process continues for the remaining tracks. --- js/downloads.js | 166 ++++++++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 77 deletions(-) diff --git a/js/downloads.js b/js/downloads.js index 46a0041..7df322d 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -244,37 +244,40 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana updateBulkDownloadProgress(notification, i, tracks.length, trackTitle); - const blob = await downloadTrackBlob(track, quality, api); - zip.file(`${folderName}/${filename}`, blob); - - try { - const meta = buildTrackMetadata(track, api); - const metaFilename = filename.replace(/\.[^.]+$/, '.json'); - zip.file(`${folderName}/${metaFilename}`, JSON.stringify(meta, null, 2)); - } catch (e) { - console.warn('Could not attach metadata for', trackTitle, e); - } - - try { - await addCoverToZipIfMissing(zip, folderName, albumCoverId || track.album?.cover, api); - } catch (e) { + const blob = await downloadTrackBlob(track, quality, api); + zip.file(`${folderName}/${filename}`, blob); - } - - if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { try { - const lyricsData = await lyricsManager.fetchLyrics(track.id); - if (lyricsData) { - const lrcContent = lyricsManager.generateLRCContent(lyricsData, track); - if (lrcContent) { - const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc'); - zip.file(`${folderName}/${lrcFilename}`, lrcContent); - } - } - } catch (error) { - console.log('Could not add lyrics for:', trackTitle); + const meta = buildTrackMetadata(track, api); + const metaFilename = filename.replace(/\.[^.]+$/, '.json'); + zip.file(`${folderName}/${metaFilename}`, JSON.stringify(meta, null, 2)); + } catch (e) { + console.warn('Could not attach metadata for', trackTitle, e); } + + try { + await addCoverToZipIfMissing(zip, folderName, albumCoverId || track.album?.cover, api); + } catch (e) { + + } + + if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { + try { + const lyricsData = await lyricsManager.fetchLyrics(track.id); + if (lyricsData) { + const lrcContent = lyricsManager.generateLRCContent(lyricsData, track); + if (lrcContent) { + const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc'); + zip.file(`${folderName}/${lrcFilename}`, lrcContent); + } + } + } catch (error) { + console.log('Could not add lyrics for:', trackTitle); + } + } + } catch (err) { + console.error(`Failed to download track ${trackTitle}:`, err); } } @@ -323,36 +326,40 @@ export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyri updateBulkDownloadProgress(notification, i, tracks.length, trackTitle); - const blob = await downloadTrackBlob(track, quality, api); - zip.file(`${folderName}/${filename}`, blob); - - // add metadata JSON try { - const meta = buildTrackMetadata(track, api); - const metaFilename = filename.replace(/\.[^.]+$/, '.json'); - zip.file(`${folderName}/${metaFilename}`, JSON.stringify(meta, null, 2)); - } catch (e) { - console.warn('Could not attach metadata for', trackTitle, e); - } + const blob = await downloadTrackBlob(track, quality, api); + zip.file(`${folderName}/${filename}`, blob); - // add cover per track/playlist (attempt once per track) - try { - await addCoverToZipIfMissing(zip, folderName, track.album?.cover, api); - } catch (e) {} - - if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { + // add metadata JSON try { - const lyricsData = await lyricsManager.fetchLyrics(track.id); - if (lyricsData) { - const lrcContent = lyricsManager.generateLRCContent(lyricsData, track); - if (lrcContent) { - const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc'); - zip.file(`${folderName}/${lrcFilename}`, lrcContent); - } - } - } catch (error) { - console.log('Could not add lyrics for:', trackTitle); + const meta = buildTrackMetadata(track, api); + const metaFilename = filename.replace(/\.[^.]+$/, '.json'); + zip.file(`${folderName}/${metaFilename}`, JSON.stringify(meta, null, 2)); + } catch (e) { + console.warn('Could not attach metadata for', trackTitle, e); } + + // add cover per track/playlist (attempt once per track) + try { + await addCoverToZipIfMissing(zip, folderName, track.album?.cover, api); + } catch (e) {} + + if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { + try { + const lyricsData = await lyricsManager.fetchLyrics(track.id); + if (lyricsData) { + const lrcContent = lyricsManager.generateLRCContent(lyricsData, track); + if (lrcContent) { + const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc'); + zip.file(`${folderName}/${lrcFilename}`, lrcContent); + } + } + } catch (error) { + console.log('Could not add lyrics for:', trackTitle); + } + } + } catch (err) { + console.error(`Failed to download track ${trackTitle}:`, err); } } @@ -410,34 +417,39 @@ export async function downloadDiscography(artist, api, quality, lyricsManager = for (const track of tracks) { const filename = buildTrackFilename(track, quality); - 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); - } + const blob = await downloadTrackBlob(track, quality, api); + zip.file(`${rootFolder}/${albumFolder}/${filename}`, blob); - try { - await addCoverToZipIfMissing(zip, `${rootFolder}/${albumFolder}`, track.album?.cover || album.cover, api); - } catch (e) {} - - if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { try { - const lyricsData = await lyricsManager.fetchLyrics(track.id); - if (lyricsData) { - const lrcContent = lyricsManager.generateLRCContent(lyricsData, track); - if (lrcContent) { - const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc'); - zip.file(`${rootFolder}/${albumFolder}/${lrcFilename}`, lrcContent); - } - } - } catch (error) { - console.log('Could not add lyrics for:', track.title); + 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); + } catch (e) {} + + if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) { + try { + const lyricsData = await lyricsManager.fetchLyrics(track.id); + if (lyricsData) { + const lrcContent = lyricsManager.generateLRCContent(lyricsData, track); + if (lrcContent) { + const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc'); + zip.file(`${rootFolder}/${albumFolder}/${lrcFilename}`, lrcContent); + } + } + } catch (error) { + console.log('Could not add lyrics for:', track.title); + } + } + } catch (err) { + console.error(`Failed to download track ${track.title} in album ${album.title}:`, err); } } } catch (error) { From 45380ea148ff52a08729f353d7f14a7eccc2ff71 Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Fri, 26 Dec 2025 12:16:04 +0100 Subject: [PATCH 2/4] Fix UIRenderer constructor to properly accept and assign 'player' instance Resolved TypeError where 'this.player' was undefined in UIRenderer, preventing 'isCurrentTrack' check from working correctly. Updated constructor to accept 'player' argument passed from app.js. --- js/app.js | 2 +- js/ui.js | 94 +++++++++++++++++++++++++++---------------------------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/js/app.js b/js/app.js index e086e50..35de81c 100644 --- a/js/app.js +++ b/js/app.js @@ -175,12 +175,12 @@ function hideOfflineNotification() { document.addEventListener('DOMContentLoaded', async () => { const api = new LosslessAPI(apiSettings); - const ui = new UIRenderer(api); const audioPlayer = document.getElementById('audio-player'); const currentQuality = localStorage.getItem('playback-quality') || 'LOSSLESS'; const player = new Player(audioPlayer, api, currentQuality); + const ui = new UIRenderer(api, player); const scrobbler = new LastFMScrobbler(); const lyricsManager = new LyricsManager(api); const lyricsPanel = createLyricsPanel(); diff --git a/js/ui.js b/js/ui.js index e39e7e6..fa23d2a 100644 --- a/js/ui.js +++ b/js/ui.js @@ -3,8 +3,9 @@ import { SVG_PLAY, SVG_DOWNLOAD, SVG_MENU, formatTime, createPlaceholder, trackD import { recentActivityManager, backgroundSettings, trackListSettings } from './storage.js'; export class UIRenderer { - constructor(api) { + constructor(api, player) { this.api = api; + this.player = player; this.currentTrack = null; this.searchAbortController = null; } @@ -72,6 +73,7 @@ export class UIRenderer { const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : ''; const trackArtists = getTrackArtists(track); const trackTitle = getTrackTitle(track); + const isCurrentTrack = this.player?.currentTrack?.id === track.id; let yearDisplay = ''; const releaseDate = track.album?.releaseDate || track.streamStartDate; @@ -112,7 +114,7 @@ export class UIRenderer { `; return ` -
+
${trackNumberHTML}
@@ -627,67 +629,65 @@ export class UIRenderer { } } -async renderPlaylistPage(playlistId) { - this.showPage('playlist'); + async renderPlaylistPage(playlistId) { + this.showPage('playlist'); - const imageEl = document.getElementById('playlist-detail-image'); - const titleEl = document.getElementById('playlist-detail-title'); - const metaEl = document.getElementById('playlist-detail-meta'); - const descEl = document.getElementById('playlist-detail-description'); - const tracklistContainer = document.getElementById('playlist-detail-tracklist'); + const imageEl = document.getElementById('playlist-detail-image'); + const titleEl = document.getElementById('playlist-detail-title'); + const metaEl = document.getElementById('playlist-detail-meta'); + const descEl = document.getElementById('playlist-detail-description'); + const tracklistContainer = document.getElementById('playlist-detail-tracklist'); const playBtn = document.getElementById('play-playlist-btn'); if (playBtn) playBtn.innerHTML = `${SVG_PLAY}Play`; const dlBtn = document.getElementById('download-playlist-btn'); if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}Download`; imageEl.src = ''; - imageEl.style.backgroundColor = 'var(--muted)'; - titleEl.innerHTML = '
'; - metaEl.innerHTML = '
'; - descEl.innerHTML = '
'; - tracklistContainer.innerHTML = ` -
- # - Title - Duration -
- ${this.createSkeletonTracks(10, true)} - `; - - try { - const { playlist, tracks } = await this.api.getPlaylist(playlistId); - - const imageId = playlist.squareImage || playlist.image; - imageEl.src = this.api.getCoverUrl(imageId, '1080'); - imageEl.style.backgroundColor = ''; - - titleEl.textContent = playlist.title; - - this.adjustTitleFontSize(titleEl, playlist.title); - - const totalDuration = calculateTotalDuration(tracks); - - metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`; - - descEl.textContent = playlist.description || ''; - + imageEl.style.backgroundColor = 'var(--muted)'; + titleEl.innerHTML = '
'; + metaEl.innerHTML = '
'; + descEl.innerHTML = '
'; tracklistContainer.innerHTML = `
# Title Duration
+ ${this.createSkeletonTracks(10, true)} `; - this.renderListWithTracks(tracklistContainer, tracks, true); - - recentActivityManager.addPlaylist(playlist); - - document.title = `${playlist.title || 'Artist Mix'} - Monochrome`; } catch (error) { - console.error("Failed to load playlist:", error); - tracklistContainer.innerHTML = createPlaceholder(`Could not load playlist details. ${error.message}`); + try { + const { playlist, tracks } = await this.api.getPlaylist(playlistId); + + const imageId = playlist.squareImage || playlist.image; + imageEl.src = this.api.getCoverUrl(imageId, '1080'); + imageEl.style.backgroundColor = ''; + + titleEl.textContent = playlist.title; + this.adjustTitleFontSize(titleEl, playlist.title); + + const totalDuration = calculateTotalDuration(tracks); + + metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`; + descEl.textContent = playlist.description || ''; + + tracklistContainer.innerHTML = ` +
+ # + Title + Duration +
+ `; + + this.renderListWithTracks(tracklistContainer, tracks, true); + recentActivityManager.addPlaylist(playlist); + + document.title = `${playlist.title || 'Artist Mix'} - Monochrome`; + } catch (error) { + console.error("Failed to load playlist:", error); + tracklistContainer.innerHTML = createPlaceholder(`Could not load playlist details. ${error.message}`); + } } -} async renderArtistPage(artistId) { this.showPage('artist'); From 8ccd81b0584b06518c743d73d8168d57ee7e7f8f Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Wed, 24 Dec 2025 00:49:06 +0100 Subject: [PATCH 3/4] Remove Plausible analytics script --- index.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/index.html b/index.html index 1478e8e..9a5b593 100644 --- a/index.html +++ b/index.html @@ -491,9 +491,6 @@
- - - From 45a31075a52fbc407db55ae6cc68fdb19eb75480 Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Fri, 26 Dec 2025 12:28:05 +0100 Subject: [PATCH 4/4] Improve vibrant color contrast in light mode Modified setVibrantColor in ui.js to darken bright colors when in light mode and lighten dark colors in dark mode, ensuring text and button readability. --- js/ui.js | 58 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/js/ui.js b/js/ui.js index fa23d2a..3b78783 100644 --- a/js/ui.js +++ b/js/ui.js @@ -268,24 +268,60 @@ export class UIRenderer { if (!color) return; const root = document.documentElement; + const theme = root.getAttribute('data-theme'); + const isLightMode = theme === 'light'; + + let hex = color.replace('#', ''); + // Handle shorthand hex + if (hex.length === 3) { + hex = hex.split('').map(char => char + char).join(''); + } + + let r = parseInt(hex.substr(0, 2), 16); + let g = parseInt(hex.substr(2, 2), 16); + let b = parseInt(hex.substr(4, 2), 16); + + // Calculate perceived brightness + let brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000; + + if (isLightMode) { + // In light mode, the background is white. + // We need the color (used for text/highlights) to be dark enough. + // If brightness is too high (> 150), darken it. + while (brightness > 150) { + r = Math.floor(r * 0.9); + g = Math.floor(g * 0.9); + b = Math.floor(b * 0.9); + brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000; + } + } else { + // In dark mode, the background is dark. + // We need the color to be light enough. + // If brightness is too low (< 80), lighten it. + while (brightness < 80) { + r = Math.min(255, Math.floor(r * 1.15)); + g = Math.min(255, Math.floor(g * 1.15)); + b = Math.min(255, Math.floor(b * 1.15)); + brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000; + // Break if we hit white or can't get brighter to avoid infinite loop + if (r >= 255 && g >= 255 && b >= 255) break; + } + } + + const adjustedColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; - // Calculate contrast text color - const hex = color.replace('#', ''); - const r = parseInt(hex.substr(0, 2), 16); - const g = parseInt(hex.substr(2, 2), 16); - const b = parseInt(hex.substr(4, 2), 16); - const brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000; + // Calculate contrast text color for buttons (text on top of the vibrant color) const foreground = brightness > 128 ? '#000000' : '#ffffff'; // Set global CSS variables - root.style.setProperty('--primary', color); + root.style.setProperty('--primary', adjustedColor); root.style.setProperty('--primary-foreground', foreground); - root.style.setProperty('--highlight', color); + root.style.setProperty('--highlight', adjustedColor); root.style.setProperty('--highlight-rgb', `${r}, ${g}, ${b}`); - root.style.setProperty('--active-highlight', color); - root.style.setProperty('--ring', color); + root.style.setProperty('--active-highlight', adjustedColor); + root.style.setProperty('--ring', adjustedColor); - // Calculate a safe hover color (darken if too light) + // Calculate a safe hover color let hoverColor; if (brightness > 200) { const dr = Math.floor(r * 0.85);