allow mix/icon.png to override default icon

Hunter
Tuesday, October 13th, 2026 at 12:12 AM

build.py+15-5
index.html -1
mix/readme.md+3-2
readme.md+1-1
rip.py+18-1

build.py
  4  (PWA requirements) based on the contents of tracks.json
  5  """
  6  
  7 +import hashlib
  8  import json
  9  import re
 10  from pathlib import Path

122  	album_art_path = "mix/album_art.jpg" if custom_album_art.exists() else "resources/album_art.jpg"
123  	# append a content hash so the URL changes when the image changes
124  	if album_art_file.exists():
    -		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 +	# mix/icon.png overrides the default
130 +	custom_icon = SCRIPT_DIR / "mix" / "icon.png"
131 +	icon_file = custom_icon if custom_icon.exists() else SCRIPT_DIR / "resources" / "icon.png"
132 +	icon_path = "mix/icon.png" if custom_icon.exists() else "resources/icon.png"
133 +	# append a content hash so the URL changes when the image changes
134 +	if icon_file.exists():
135 +		icon_hash = hashlib.md5(icon_file.read_bytes()).hexdigest()[:8]
136 +		icon_path = f"{icon_path}?v={icon_hash}"
137 +	print(f"  Icon: {icon_path}")
138 +
139  	# Generate manifest.json
140  	manifest = {
141  		"id": base_path,

151  		"album_art": album_art_path,  # Custom field for script.js to use
152  		"icons": [
153  			{
    -				"src": f"{base_path}resources/icon.png",
154 +				"src": f"{base_path}{icon_path}",
155  				"sizes": "640x640",
156  				"type": "image/png",
157  				"purpose": "any maskable"

171  		"resources/styles.css",
172  		"resources/script.js",
173  		"mix/tracks.json",
    -		"resources/icon.png",
174 +		icon_path,
175  		album_art_path,
176  		"resources/play.svg",
177  		"resources/pause.svg",

182  	]
183  
184  	AUDIO_EXTS = {".mp3", ".m4a", ".ogg", ".flac", ".wav"}
    -	# album_art.jpg added explicitly above with a hash; skip to avoid a bare dupe
    -	SKIP_NAMES = {"tracks.json", "readme.md", "album_art.jpg"}
185 +	# album_art.jpg and icon.png added explicitly above with a hash; skip to avoid a bare dupe
186 +	SKIP_NAMES = {"tracks.json", "readme.md", "album_art.jpg", "icon.png"}
187  	for path in sorted((SCRIPT_DIR / "mix").iterdir()):
188  		if not path.is_file():
189  			continue
index.html
 7  	<meta name="theme-color" content="#080a0c">
 8  	<meta name="apple-mobile-web-app-capable" content="yes">
 9  	<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
   -	<link rel="apple-touch-icon" href="resources/icon.png">
10  	<link rel="manifest" href="manifest.json">
11  	<title>my mixapp</title>
12  	<link rel="stylesheet" href="resources/styles.css">
mix/readme.md
 7  ## optional files
 8  - `custom.css` - custom styles to override the default theme
 9  - `custom.js` - custom scripts for additional functionality
   -- `album_art.jpg` - cover art for your mix (overrides the default in `/resources`)
10 +- `album_art.jpg` - cover art for your mix (overrides the default in `/resources`)
11 +- `icon.png` - home screen icon for your mix (overrides the default in `/resources`)
readme.md
72  	(pictured: integration with iOS lockscreen controls)
73  
74  ## customization
   -add your own `custom.css`, `custom.js`, and/or `album_art.jpg` to `mix/` to customize your mixapp's appearance and behavior.
75 +add `custom.css`, `custom.js`, `album_art.jpg`, and/or `icon.png` to `mix/` to customize your mixapp's appearance and behavior.
76  
77  ## local network installation
78  
rip.py
 10    ./rip.py 3    rips only track 3
 11  
 12  If /mix holds only tracks from this CD, ripping the whole disc also
    -downloads its cover art to mix/album_art.jpg and runs build.py
 13 +downloads its cover art to mix/album_art.jpg, scales it to mix/icon.png
 14 +and runs build.py
 15  """
 16  
 17  import os

280  	print("✓ Saved album art to mix/album_art.jpg")
281  
282  
283 +def make_icon():
284 +	"""Scale mix/album_art.jpg to a 1000x1000 mix/icon.png using ffmpeg"""
285 +	icon = MIX_DIR / "icon.png"
286 +	result = subprocess.run(['ffmpeg', '-loglevel', 'error', '-y', '-i', str(MIX_DIR / "album_art.jpg"),
287 +	                         '-vf', 'scale=1000:1000', '-frames:v', '1', str(icon)],
288 +	                        capture_output=True, text=True)
289 +	if result.returncode != 0:
290 +		icon.unlink(missing_ok=True)
291 +		err_tail = "\n".join(result.stderr.strip().splitlines()[-5:])
292 +		print(f"  Could not create icon:\n{err_tail}")
293 +		return
294 +	print("✓ Saved icon to mix/icon.png")
295 +
296 +
297  def mix_tracks():
298  	"""Names of the audio files currently in /mix"""
299  	return {f.name for f in MIX_DIR.iterdir() if f.suffix.lower() in scan.SUPPORTED_EXTENSIONS}

698  	if only_this_cd and whole_cd_in_mix and album:
699  		if release_id and not (MIX_DIR / "album_art.jpg").exists():
700  			download_album_art(release_id, release_group_id)
701 +		if (MIX_DIR / "album_art.jpg").exists() and not (MIX_DIR / "icon.png").exists():
702 +			make_icon()
703  		build_mixapp(album)
704  		print("\nRun https_serve.py to install your mixapp.")
705  	elif success_count > 0: