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:
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