declare album art in manifest with cache-busting hash
Hunter
Sunday, May 17th, 2026 at 5:27 PM
115 # Get background color from styles.css
116 background_color = get_background_color()
117
118 + # mix/album_art.jpg overrides the default
119 + custom_album_art = SCRIPT_DIR / "mix" / "album_art.jpg"
120 + album_art_file = custom_album_art if custom_album_art.exists() else SCRIPT_DIR / "resources" / "album_art.jpg"
121 + album_art_path = "mix/album_art.jpg" if custom_album_art.exists() else "resources/album_art.jpg"
122 + # append a content hash so the URL changes when the image changes
123 + if album_art_file.exists():
124 + import hashlib
125 + art_hash = hashlib.md5(album_art_file.read_bytes()).hexdigest()[:8]
126 + album_art_path = f"{album_art_path}?v={art_hash}"
127 + print(f" Album art: {album_art_path}")
128 +
129 # Generate manifest.json
130 manifest = {
131 "id": base_path,
138 "background_color": background_color,
139 "theme_color": background_color,
140 "cache_name": cache_name, # Custom field for script.js to use
141 + "album_art": album_art_path, # Custom field for script.js to use
142 "icons": [
143 {
144 "src": f"{base_path}resources/icon.png",
162 "resources/script.js",
163 "mix/tracks.json",
164 "resources/icon.png",
- "resources/album_art.jpg",
165 + album_art_path,
166 "resources/play.svg",
167 "resources/pause.svg",
168 "resources/prev.svg",
172 ]
173
174 AUDIO_EXTS = {".mp3", ".m4a", ".ogg", ".flac", ".wav"}
- SKIP_NAMES = {"tracks.json", "readme.md"}
175 + # album_art.jpg added explicitly above with a hash; skip to avoid a bare dupe
176 + SKIP_NAMES = {"tracks.json", "readme.md", "album_art.jpg"}
177 for path in sorted((SCRIPT_DIR / "mix").iterdir()):
178 if not path.is_file():
179 continue
75 let totalBytesLoaded = 0; // Track total filesize of all preloaded tracks
76 let cachedTracks = new Set(); // Track which tracks are cached for offline use
77 let CACHE_NAME = 'my-mixapp'; // Default fallback
-let hasCustomAlbumArt = false;
78 +let albumArtPath = 'resources/album_art.jpg'; // Overridden by manifest.album_art
79 const staticFiles = [
80 './',
81 'index.html',
83 'resources/script.js',
84 'mix/tracks.json',
85 'resources/icon.png',
- 'resources/album_art.jpg',
86 'resources/play.svg',
87 'resources/pause.svg',
88 'resources/prev.svg',
130 if (manifest.name) {
131 document.title = manifest.name;
132 }
133 +
134 + // build.py resolves this to the custom mix art when present
135 + if (manifest.album_art) {
136 + albumArtPath = manifest.album_art;
137 + }
138 }
139 + staticFiles.push(albumArtPath);
140
141 // Probe for optional files and add them to staticFiles
- const optionalFiles = ['mix/album_art.jpg', 'mix/custom.css', 'mix/custom.js'];
142 + const optionalFiles = ['mix/custom.css', 'mix/custom.js'];
143 return Promise.all([
144 ...optionalFiles.map(f =>
145 fetch(f, { method: 'HEAD' })
146 .then(r => {
147 if (r.ok) {
148 staticFiles.push(f);
- if (f === 'mix/album_art.jpg') hasCustomAlbumArt = true;
149 }
150 })
151 .catch(() => {})
187 updateCurrentTrackDisplay('Unable to load tracks. Please check your connection.');
188 });
189
190 +// Set MediaMetadata for the given track. albumArtPath is resolved at build time
191 +// (custom mix art if present, else default) and read from the manifest on startup.
192 +function updateMediaSessionMetadata(track) {
193 + if (!('mediaSession' in navigator) || !track) return;
194 + const albumArtUrl = new URL(albumArtPath, document.baseURI).href;
195 + navigator.mediaSession.metadata = new MediaMetadata({
196 + title: track.title,
197 + artist: track.artist,
198 + artwork: [
199 + { src: albumArtUrl, sizes: 'any', type: 'image/jpeg' }
200 + ]
201 + });
202 +}
203 +
204 // Audio event listeners
205 audio.addEventListener('play', () => {
206 isPlaying = true;
225
226 // Update media session metadata
227 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' }
- ]
- });
228 + updateMediaSessionMetadata(track);
229
230 // Set action handlers after playback starts (required for iOS)
231 // Explicitly set seek handlers to null so iOS shows next/prev instead