add default album art; allow overriding

Hunter
Sunday, May 3rd, 2026 at 10:46 PM

build.py+1 
mix/readme.md+2-1
readme.md+1-1
resources/album_art.jpgnew
resources/script.js+10-2

build.py
141  		"resources/script.js",
142  		"mix/tracks.json",
143  		"resources/icon.png",
144 +		"resources/album_art.jpg",
145  		"resources/play.svg",
146  		"resources/pause.svg",
147  		"resources/prev.svg",
mix/readme.md
 5  supported formats: `.mp3`, `.m4a`, `.ogg`, `.flac`, `.wav`
 6  
 7  ## optional files
   -- `album_art.jpg` - cover art for your mix
 8  - `custom.css` - custom styles to override the default theme
 9  - `custom.js` - custom scripts for additional functionality
10 +- `album_art.jpg` - cover art for your mix (overrides the default in `/resources`)
readme.md
66  	(pictured: integration with iOS lockscreen controls)
67  
68  ## customization
   -add `custom.css`, `custom.js` and/or `album_art.jpg` to `/mix` to customize your mixapp's appearance and behavior. these files are automatically loaded if present.
69 +add your own `custom.css`, `custom.js`, and/or `album_art.jpg` to `/mix` to customize your mixapp's appearance and behavior.
70  
71  ## intellectual property notice
72  ensure you have the right to distribute any media files you include in public mixapps. personal archival backups are for your own use. sharing them with others, even as a gift, is not covered by fair use or backup exceptions.
resources/album_art.jpg
Binary files differ.
resources/script.js
 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
 78 +let hasCustomAlbumArt = false;
 79  const staticFiles = [
 80  	'./',
 81  	'index.html',

 83  	'resources/script.js',
 84  	'mix/tracks.json',
 85  	'resources/icon.png',
 86 +	'resources/album_art.jpg',
 87  	'resources/play.svg',
 88  	'resources/pause.svg',
 89  	'resources/prev.svg',

138  		return Promise.all([
139  			...optionalFiles.map(f =>
140  				fetch(f, { method: 'HEAD' })
    -					.then(r => { if (r.ok) staticFiles.push(f); })
141 +					.then(r => {
142 +						if (r.ok) {
143 +							staticFiles.push(f);
144 +							if (f === 'mix/album_art.jpg') hasCustomAlbumArt = true;
145 +						}
146 +					})
147  					.catch(() => {})
148  			),
149  			fetch('mix/tracks.json')

209  	if ('mediaSession' in navigator) {
210  		// Convert relative path to absolute URL for media session
211  		// Use document.baseURI to correctly resolve paths in subdirectories
    -		const albumArtUrl = new URL('mix/album_art.jpg', document.baseURI).href;
212 +		const albumArtPath = hasCustomAlbumArt ? 'mix/album_art.jpg' : 'resources/album_art.jpg';
213 +		const albumArtUrl = new URL(albumArtPath, document.baseURI).href;
214  		navigator.mediaSession.metadata = new MediaMetadata({
215  			title: track.title,
216  			artist: track.artist,