commit c02150b753a0a8179988a63b628b3b3e8842c17d
parent 6ddcfd5220c4744fea7e35a93629a921a5aaf46d
Author: Hunter
Date:   Sun, 17 May 2026 17:27:53 -0400

declare album art in manifest with cache-busting hash

Diffstat:
Mbuild.py | 17+++++++++++++++--
Mresources/script.js | 41+++++++++++++++++++++++------------------
2 files changed, 38 insertions(+), 20 deletions(-)

diff --git a/build.py b/build.py @@ -115,6 +115,17 @@ def build_pwa(app_name=None, base_path=None): # Get background color from styles.css background_color = get_background_color() + # mix/album_art.jpg overrides the default + custom_album_art = SCRIPT_DIR / "mix" / "album_art.jpg" + album_art_file = custom_album_art if custom_album_art.exists() else SCRIPT_DIR / "resources" / "album_art.jpg" + album_art_path = "mix/album_art.jpg" if custom_album_art.exists() else "resources/album_art.jpg" + # append a content hash so the URL changes when the image changes + if album_art_file.exists(): + import hashlib + art_hash = hashlib.md5(album_art_file.read_bytes()).hexdigest()[:8] + album_art_path = f"{album_art_path}?v={art_hash}" + print(f" Album art: {album_art_path}") + # Generate manifest.json manifest = { "id": base_path, @@ -127,6 +138,7 @@ def build_pwa(app_name=None, base_path=None): "background_color": background_color, "theme_color": background_color, "cache_name": cache_name, # Custom field for script.js to use + "album_art": album_art_path, # Custom field for script.js to use "icons": [ { "src": f"{base_path}resources/icon.png", @@ -150,7 +162,7 @@ def build_pwa(app_name=None, base_path=None): "resources/script.js", "mix/tracks.json", "resources/icon.png", - "resources/album_art.jpg", + album_art_path, "resources/play.svg", "resources/pause.svg", "resources/prev.svg", @@ -160,7 +172,8 @@ def build_pwa(app_name=None, base_path=None): ] AUDIO_EXTS = {".mp3", ".m4a", ".ogg", ".flac", ".wav"} - SKIP_NAMES = {"tracks.json", "readme.md"} + # album_art.jpg added explicitly above with a hash; skip to avoid a bare dupe + SKIP_NAMES = {"tracks.json", "readme.md", "album_art.jpg"} for path in sorted((SCRIPT_DIR / "mix").iterdir()): if not path.is_file(): continue diff --git a/resources/script.js b/resources/script.js @@ -75,7 +75,7 @@ let isPreloadingPriority = false; let totalBytesLoaded = 0; // Track total filesize of all preloaded tracks let cachedTracks = new Set(); // Track which tracks are cached for offline use let CACHE_NAME = 'my-mixapp'; // Default fallback -let hasCustomAlbumArt = false; +let albumArtPath = 'resources/album_art.jpg'; // Overridden by manifest.album_art const staticFiles = [ './', 'index.html', @@ -83,7 +83,6 @@ const staticFiles = [ 'resources/script.js', 'mix/tracks.json', 'resources/icon.png', - 'resources/album_art.jpg', 'resources/play.svg', 'resources/pause.svg', 'resources/prev.svg', @@ -131,17 +130,22 @@ fetch('manifest.json') if (manifest.name) { document.title = manifest.name; } + + // build.py resolves this to the custom mix art when present + if (manifest.album_art) { + albumArtPath = manifest.album_art; + } } + staticFiles.push(albumArtPath); // Probe for optional files and add them to staticFiles - const optionalFiles = ['mix/album_art.jpg', 'mix/custom.css', 'mix/custom.js']; + const optionalFiles = ['mix/custom.css', 'mix/custom.js']; return Promise.all([ ...optionalFiles.map(f => fetch(f, { method: 'HEAD' }) .then(r => { if (r.ok) { staticFiles.push(f); - if (f === 'mix/album_art.jpg') hasCustomAlbumArt = true; } }) .catch(() => {}) @@ -183,6 +187,20 @@ fetch('manifest.json') updateCurrentTrackDisplay('Unable to load tracks. Please check your connection.'); }); +// Set MediaMetadata for the given track. albumArtPath is resolved at build time +// (custom mix art if present, else default) and read from the manifest on startup. +function updateMediaSessionMetadata(track) { + if (!('mediaSession' in navigator) || !track) return; + const albumArtUrl = new URL(albumArtPath, document.baseURI).href; + navigator.mediaSession.metadata = new MediaMetadata({ + title: track.title, + artist: track.artist, + artwork: [ + { src: albumArtUrl, sizes: 'any', type: 'image/jpeg' } + ] + }); +} + // Audio event listeners audio.addEventListener('play', () => { isPlaying = true; @@ -207,20 +225,7 @@ audio.addEventListener('play', () => { // Update media session metadata if ('mediaSession' in navigator) { - // Convert relative path to absolute URL for media session - // Use document.baseURI to correctly resolve paths in subdirectories - const albumArtPath = hasCustomAlbumArt ? 'mix/album_art.jpg' : 'resources/album_art.jpg'; - const albumArtUrl = new URL(albumArtPath, document.baseURI).href; - navigator.mediaSession.metadata = new MediaMetadata({ - title: track.title, - artist: track.artist, - artwork: [ - { src: albumArtUrl, sizes: '860x860', type: 'image/jpeg' }, - { src: albumArtUrl, sizes: '512x512', type: 'image/jpeg' }, - { src: albumArtUrl, sizes: '256x256', type: 'image/jpeg' }, - { src: albumArtUrl, sizes: '128x128', type: 'image/jpeg' } - ] - }); + updateMediaSessionMetadata(track); // Set action handlers after playback starts (required for iOS) // Explicitly set seek handlers to null so iOS shows next/prev instead