diff --git a/web_server.py b/web_server.py index 7b746b06..c3360965 100644 --- a/web_server.py +++ b/web_server.py @@ -12993,7 +12993,7 @@ def _run_post_processing_worker(task_id, batch_id): return download_dir = docker_resolve_path(config_manager.get('soulseek.download_path', './downloads')) - transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './transfer')) + transfer_dir = docker_resolve_path(config_manager.get('soulseek.transfer_path', './Transfer')) # Try to get context for generating the correct final filename task_basename = extract_filename(task_filename) diff --git a/webui/static/script.js b/webui/static/script.js index 66358a5e..8eff3fff 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -91,6 +91,31 @@ let artistsSearchController = null; let artistCompletionController = null; // Track ongoing completion check to cancel when navigating away let similarArtistsController = null; // Track ongoing similar artists stream to cancel when navigating away +// --- Lazy Background Image Observer --- +// Watches elements with data-bg-src, applies background-image when visible, unobserves after. +const lazyBgObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const el = entry.target; + const src = el.dataset.bgSrc; + if (src) { + el.style.backgroundImage = `url('${src}')`; + delete el.dataset.bgSrc; + } + lazyBgObserver.unobserve(el); + } + }); +}, { rootMargin: '200px' }); + +/** + * Observe all elements with data-bg-src within a container for lazy background loading. + */ +function observeLazyBackgrounds(container) { + if (!container) return; + const elements = container.querySelectorAll('[data-bg-src]'); + elements.forEach(el => lazyBgObserver.observe(el)); +} + // --- MusicBrainz Integration Constants --- const MUSICBRAINZ_LOGO_URL = 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/9e/MusicBrainz_Logo_%282016%29.svg/500px-MusicBrainz_Logo_%282016%29.svg.png'; @@ -6340,6 +6365,11 @@ function generateMosaicBackground(coverUrls) { `; } + // Cap covers per row to 15 for GPU performance (avoids hundreds of tiles) + if (coverUrls.length > 15) { + coverUrls = coverUrls.slice(0, 15); + } + const rows = 4; let mosaicHTML = '
'; @@ -6359,8 +6389,8 @@ function generateMosaicBackground(coverUrls) { mosaicHTML += `
`; mosaicHTML += `
`; - // Generate tiles - duplicate 3 times for smooth infinite scroll - for (let duplicate = 0; duplicate < 3; duplicate++) { + // Generate tiles - duplicate 2 times for smooth infinite scroll + for (let duplicate = 0; duplicate < 2; duplicate++) { for (let i = 0; i < shuffledCovers.length; i++) { const coverUrl = shuffledCovers[i]; mosaicHTML += ` @@ -20075,6 +20105,7 @@ function displayArtistsResults(query, results) { // Create artist cards container.innerHTML = results.map(result => createArtistCardHTML(result)).join(''); + observeLazyBackgrounds(container); // Add event listeners to cards container.querySelectorAll('.artist-card').forEach((card, index) => { @@ -20191,10 +20222,10 @@ function createArtistCardHTML(artist) { artist.genres.slice(0, 3).join(', ') : 'Various genres'; const popularity = artist.popularity || 0; - // Create a fallback gradient if no image is available - const backgroundStyle = imageUrl ? - `background-image: url('${imageUrl}');` : - `background: linear-gradient(135deg, rgba(29, 185, 84, 0.3) 0%, rgba(24, 156, 71, 0.2) 100%);`; + // Use data-bg-src for lazy background loading via IntersectionObserver + const backgroundAttr = imageUrl ? + `data-bg-src="${imageUrl}"` : + `style="background: linear-gradient(135deg, rgba(29, 185, 84, 0.3) 0%, rgba(24, 156, 71, 0.2) 100%);"`; // Format popularity as a percentage for better UX const popularityText = popularity > 0 ? `${popularity}% Popular` : 'Popularity Unknown'; @@ -20215,7 +20246,7 @@ function createArtistCardHTML(artist) { return `
${mbIconHTML} -
+
${escapeHtml(artist.name)}
@@ -20369,6 +20400,7 @@ function displayArtistDiscography(discography) { if (albumsContainer) { if (discography.albums?.length > 0) { albumsContainer.innerHTML = discography.albums.map(album => createAlbumCardHTML(album)).join(''); + observeLazyBackgrounds(albumsContainer); // Add dynamic glow effects and click handlers to album cards albumsContainer.querySelectorAll('.album-card').forEach((card, index) => { @@ -20398,6 +20430,7 @@ function displayArtistDiscography(discography) { if (singlesContainer) { if (discography.singles?.length > 0) { singlesContainer.innerHTML = discography.singles.map(single => createAlbumCardHTML(single)).join(''); + observeLazyBackgrounds(singlesContainer); // Add dynamic glow effects and click handlers to singles cards singlesContainer.querySelectorAll('.album-card').forEach((card, index) => { @@ -21082,14 +21115,14 @@ function createAlbumCardHTML(album) { const type = album.album_type === 'album' ? 'Album' : album.album_type === 'single' ? 'Single' : 'EP'; - // Create a fallback gradient if no image is available - const backgroundStyle = imageUrl ? - `background-image: url('${imageUrl}');` : - `background: linear-gradient(135deg, rgba(29, 185, 84, 0.2) 0%, rgba(24, 156, 71, 0.1) 100%);`; + // Use data-bg-src for lazy background loading via IntersectionObserver + const backgroundAttr = imageUrl ? + `data-bg-src="${imageUrl}"` : + `style="background: linear-gradient(135deg, rgba(29, 185, 84, 0.2) 0%, rgba(24, 156, 71, 0.1) 100%);"`; return `
-
+
Checking...
@@ -24001,6 +24034,7 @@ async function showWatchlistModal() { ${escapeHtml(artist.artist_name)} ` : `
@@ -24153,7 +24187,8 @@ async function openWatchlistArtistConfigModal(artistId, artistName) { ${artist.image_url ? ` ${escapeHtml(artist.name)} + class="watchlist-artist-config-hero-image" + loading="lazy"> ` : ''}

${escapeHtml(artist.name)}

@@ -25380,6 +25415,7 @@ function createLibraryArtistCard(artist) { const img = document.createElement("img"); img.src = artist.image_url; img.alt = artist.name; + img.loading = 'lazy'; img.onerror = () => { console.log(`Failed to load image for ${artist.name}: ${artist.image_url}`); // Replace with fallback on error @@ -25951,6 +25987,7 @@ function createReleaseCard(release) { img.src = release.image_url; img.alt = release.title; img.className = "release-image"; + img.loading = 'lazy'; img.onerror = () => { imageContainer.innerHTML = `
💿
`; }; @@ -29152,7 +29189,7 @@ function restoreCachedImages(genres) { if (genreCard) { const imageElement = genreCard.querySelector('.genre-browser-card-image'); if (imageElement) { - imageElement.innerHTML = `${genre.name}`; + imageElement.innerHTML = `${genre.name}`; genreCard.classList.remove('genre-browser-card-fallback'); } } @@ -29202,7 +29239,7 @@ async function loadGenreBrowserImagesProgressively(genres) { const imageElement = genreCard.querySelector('.genre-browser-card-image'); if (imageElement) { // Replace the fallback emoji with the actual image - imageElement.innerHTML = `${genre.name}`; + imageElement.innerHTML = `${genre.name}`; genreCard.classList.remove('genre-browser-card-fallback'); console.log(`✅ Loaded and cached image for ${genre.name} in modal`); @@ -31020,7 +31057,7 @@ async function loadDiscoverRecentReleases() { html += `
- ${album.album_name} + ${album.album_name}

${album.album_name}

@@ -31075,7 +31112,7 @@ async function loadDiscoverReleaseRadar() {
${index + 1}
- ${track.album_name} + ${track.album_name}
${track.track_name}
@@ -31132,7 +31169,7 @@ async function loadDiscoverWeekly() {
${index + 1}
- ${track.album_name} + ${track.album_name}
${track.track_name}
@@ -31608,7 +31645,7 @@ async function loadDecadeTracks(decade) {
${index + 1}
- ${albumName} + ${albumName}
${trackName}
@@ -31977,7 +32014,7 @@ async function loadGenreTracks(genreName) {
${index + 1}
- ${albumName} + ${albumName}
${trackName}
@@ -32622,7 +32659,7 @@ function displayListenBrainzTracks(tracks, playlistId) {
${index + 1}
- ${albumName} + ${albumName}
${escapeHtml(track.track_name || 'Unknown Track')}
@@ -33036,7 +33073,7 @@ async function loadSeasonalAlbums(seasonData) { html += `
- ${album.album_name} + ${album.album_name}

${album.album_name}

@@ -33106,7 +33143,7 @@ async function loadSeasonalPlaylist(seasonData) {
${index + 1}
- ${track.album_name} + ${track.album_name}
${track.track_name}
@@ -33415,7 +33452,7 @@ async function loadPersonalizedDailyMixes() { html += `
- ${mix.name} + ${mix.name}
â–¶
@@ -33448,7 +33485,7 @@ function renderCompactPlaylist(container, tracks) {
${index + 1}
- ${track.album_name} + ${track.album_name}
${track.track_name}
@@ -33547,7 +33584,7 @@ async function searchBuildPlaylistArtists() { const imageUrl = artist.image_url || '/static/placeholder-album.png'; html += `
- ${artist.name} + ${artist.name} ${artist.name}
`; @@ -33611,7 +33648,7 @@ function renderBuildPlaylistSelectedArtists() { buildPlaylistSelectedArtists.forEach(artist => { html += `
- ${artist.name} + ${artist.name} ${artist.name}
@@ -35041,7 +35078,7 @@ async function loadImportSuggestions() { section.classList.remove('hidden'); grid.innerHTML = data.suggestions.map(a => `
- ${a.name} + ${a.name}
${a.name}
${a.artist}
@@ -35074,7 +35111,7 @@ async function searchImportAlbum() { } grid.innerHTML = data.albums.map(a => `
- ${a.name} + ${a.name}
${a.name}
${a.artist}
@@ -35126,7 +35163,7 @@ async function selectImportAlbum(albumId) { // Render hero const album = data.album; document.getElementById('import-album-hero').innerHTML = ` - ${album.name} + ${album.name}

${album.name}

${album.artist} · ${album.total_tracks} tracks · ${album.release_date ? album.release_date.substring(0,4) : ''}

diff --git a/webui/static/style.css b/webui/static/style.css index 0da2407e..c59199f1 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -161,8 +161,6 @@ body { background: linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.03) 100%); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); transform: translateX(4px); box-shadow: @@ -175,8 +173,6 @@ body { rgba(29, 185, 84, 0.16) 0%, rgba(29, 185, 84, 0.12) 50%, rgba(29, 185, 84, 0.08) 100%); - backdrop-filter: blur(20px) saturate(1.6); - -webkit-backdrop-filter: blur(20px) saturate(1.6); border: 1px solid rgba(29, 185, 84, 0.25); transform: translateX(6px); box-shadow: @@ -191,8 +187,6 @@ body { rgba(29, 185, 84, 0.22) 0%, rgba(29, 185, 84, 0.18) 50%, rgba(29, 185, 84, 0.12) 100%); - backdrop-filter: blur(20px) saturate(1.8); - -webkit-backdrop-filter: blur(20px) saturate(1.8); border: 1px solid rgba(29, 185, 84, 0.35); transform: translateX(8px); box-shadow: @@ -214,8 +208,6 @@ body { background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.04) 100%); - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.8); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); @@ -229,8 +221,6 @@ body { background: linear-gradient(135deg, rgba(29, 185, 84, 0.25) 0%, rgba(30, 215, 96, 0.20) 100%); - backdrop-filter: blur(10px) saturate(1.6); - -webkit-backdrop-filter: blur(10px) saturate(1.6); border: 1px solid rgba(29, 185, 84, 0.3); font-weight: 700; box-shadow: @@ -996,8 +986,6 @@ body { .stat-card { background: linear-gradient(135deg, rgba(29, 185, 84, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%); - backdrop-filter: blur(20px) saturate(1.5); - -webkit-backdrop-filter: blur(20px) saturate(1.5); border: 1px solid rgba(29, 185, 84, 0.15); border-radius: 16px; padding: 24px; @@ -1087,8 +1075,6 @@ body { border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 16px; padding: 20px; - backdrop-filter: blur(12px) saturate(1.1); - -webkit-backdrop-filter: blur(12px) saturate(1.1); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.05); position: relative; overflow: hidden; @@ -3507,7 +3493,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(12px) saturate(1.1); border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.08); border-top: 1px solid rgba(255, 255, 255, 0.12); @@ -3540,7 +3525,6 @@ body { background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(22, 22, 22, 1.0) 100%); - backdrop-filter: blur(16px) saturate(1.2); border-color: rgba(29, 185, 84, 0.3); border-top-color: rgba(29, 185, 84, 0.4); @@ -3612,7 +3596,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(12px) saturate(1.1); border-radius: 24px; border: 1px solid rgba(255, 255, 255, 0.08); border-top: 1px solid rgba(255, 255, 255, 0.12); @@ -3642,7 +3625,6 @@ body { background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(22, 22, 22, 1.0) 100%); - backdrop-filter: blur(16px) saturate(1.2); border-color: rgba(29, 185, 84, 0.3); border-top-color: rgba(29, 185, 84, 0.4); @@ -3746,7 +3728,6 @@ body { background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.01) 100%); - backdrop-filter: blur(8px); border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.05); padding: 12px; @@ -3767,7 +3748,6 @@ body { background: linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.02) 100%); - backdrop-filter: blur(10px); border-color: rgba(29, 185, 84, 0.2); /* Enhanced subtle depth */ @@ -4281,10 +4261,8 @@ body { /* Apple-style liquid glassmorphic foundation */ background: linear-gradient(135deg, - rgba(20, 20, 20, 0.55) 0%, - rgba(12, 12, 12, 0.65) 100%); - backdrop-filter: blur(40px) saturate(1.8); - -webkit-backdrop-filter: blur(40px) saturate(1.8); + rgba(20, 20, 20, 0.85) 0%, + rgba(12, 12, 12, 0.92) 100%); border-radius: 24px; border: 1px solid rgba(255, 255, 255, 0.08); border-top: 1px solid rgba(255, 255, 255, 0.12); @@ -4439,7 +4417,6 @@ body { background: linear-gradient(135deg, rgba(20, 20, 20, 0.95) 0%, rgba(12, 12, 12, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.12); border-top: 1px solid rgba(255, 255, 255, 0.18); @@ -4551,7 +4528,6 @@ body { background: linear-gradient(135deg, rgba(20, 20, 20, 0.95) 0%, rgba(12, 12, 12, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.12); border-top: 1px solid rgba(255, 255, 255, 0.18); @@ -4611,7 +4587,6 @@ body { background: linear-gradient(135deg, rgba(20, 20, 20, 0.95) 0%, rgba(12, 12, 12, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.12); border-top: 1px solid rgba(255, 255, 255, 0.18); @@ -5064,7 +5039,6 @@ body { .sync-main-panel, .sync-sidebar { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95), rgba(18, 18, 18, 0.98)); - backdrop-filter: blur(10px); border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.08); padding: 20px; @@ -5543,7 +5517,6 @@ body { border: none; border-radius: 50%; background: rgba(1, 255, 149, 0.2); - backdrop-filter: blur(10px); color: #01FF95; font-size: 24px; font-weight: 600; @@ -5654,7 +5627,6 @@ body { rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 50%, rgba(12, 12, 12, 0.99) 100%); - backdrop-filter: blur(24px) saturate(1.3); /* Enhanced borders matching modal hero */ border: 1px solid rgba(1, 255, 149, 0.3); @@ -5779,7 +5751,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); /* Enhanced borders matching modal */ border-radius: 20px; @@ -5818,7 +5789,6 @@ body { background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(22, 22, 22, 1.0) 100%); - backdrop-filter: blur(24px) saturate(1.3); transform: translateY(-6px) scale(1.02); border-color: rgba(1, 255, 149, 0.4); @@ -5911,7 +5881,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); /* Enhanced borders matching modal */ border-radius: 16px; @@ -5980,7 +5949,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); /* Enhanced borders matching modal */ border-radius: 16px; @@ -6022,7 +5990,6 @@ body { background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(22, 22, 22, 1.0) 100%); - backdrop-filter: blur(24px) saturate(1.3); transform: translateY(-4px) scale(1.02); border-color: rgba(1, 255, 149, 0.4); @@ -6300,7 +6267,6 @@ body { background: linear-gradient(135deg, rgba(20, 20, 20, 0.95) 0%, rgba(12, 12, 12, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); /* Enhanced borders */ border-radius: 20px; @@ -6344,7 +6310,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); /* Enhanced borders matching modal */ border-radius: 16px; @@ -6386,7 +6351,6 @@ body { background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(22, 22, 22, 1.0) 100%); - backdrop-filter: blur(24px) saturate(1.3); transform: translateY(-4px) scale(1.02); border-color: rgba(1, 255, 149, 0.4); @@ -6623,7 +6587,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); /* Enhanced borders matching modal */ border-radius: 16px; @@ -6662,7 +6625,6 @@ body { background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(22, 22, 22, 1.0) 100%); - backdrop-filter: blur(24px) saturate(1.3); transform: translateY(-4px) scale(1.02); border-color: rgba(138, 43, 226, 0.5); @@ -6855,7 +6817,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(20px) saturate(1.2); /* Enhanced borders matching modal */ border-radius: 16px; @@ -6894,7 +6855,6 @@ body { background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(22, 22, 22, 1.0) 100%); - backdrop-filter: blur(24px) saturate(1.3); transform: translateY(-6px) scale(1.02); border-color: rgba(138, 43, 226, 0.5); @@ -7229,7 +7189,6 @@ body { background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%); - backdrop-filter: blur(12px) saturate(1.1); border-radius: 20px; border: 1px solid rgba(255, 255, 255, 0.08); border-top: 1px solid rgba(255, 255, 255, 0.12); @@ -7253,7 +7212,6 @@ body { background: linear-gradient(135deg, rgba(30, 30, 30, 0.98) 0%, rgba(22, 22, 22, 1.0) 100%); - backdrop-filter: blur(16px) saturate(1.2); border-color: rgba(29, 185, 84, 0.3); border-top-color: rgba(29, 185, 84, 0.4); @@ -8316,6 +8274,7 @@ body { height: 100%; opacity: 0; animation: fadeInRow 1s ease-out forwards, infiniteScroll var(--speed, 30s) linear infinite; + will-change: transform; } .wishlist-mosaic-row.scroll-right { @@ -8400,14 +8359,14 @@ body { } /* Infinite scrolling animation */ -/* Content is duplicated 3x, animation moves by -33.333% (one set) for seamless loop */ +/* Content is duplicated 2x, animation moves by -50% (one set) for seamless loop */ @keyframes infiniteScroll { 0% { transform: translateX(0); } 100% { - transform: translateX(-33.333%); + transform: translateX(-50%); } }