allow mix/icon.png to override default icon
Hunter
Tuesday, October 13th, 2026 at 12:12 AM
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
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">
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`)
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
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: