build.py -> catalog.py; generate_manifests.py -> build.py
Hunter
Monday, April 6th, 2026 at 4:06 PM
1 #!/usr/bin/env python3
2 """
-Scans /mix directory and populates tracks.json with metadata
-Supports MP3, M4A, OGG, FLAC, and WAV formats
-Automatically manages a virtual environment for dependencies
3 +Creates manifest.json and service-worker.js
4 +(PWA requirements) based on the contents of tracks.json
5 """
6
-import os
-import sys
-import subprocess
7 import json
8 +import re
9 from pathlib import Path
10
-SCRIPT_DIR = Path(__file__).parent.absolute()
-VENV_DIR = SCRIPT_DIR / "venv"
-MIX_DIR = SCRIPT_DIR / "mix"
-OUTPUT_FILE = MIX_DIR / "tracks.json"
-
-
-def setup_venv():
- """Create and set up virtual environment if it doesn't exist"""
- # Determine the path to pip and python in the venv
- if sys.platform == "win32":
- pip_path = VENV_DIR / "Scripts" / "pip"
- python_path = VENV_DIR / "Scripts" / "python"
11 +def get_configuration():
12 + """Prompt user for configuration values"""
13 + print("=" * 60)
14 + print("PWA Configuration")
15 + print("=" * 60)
16 + print()
17 +
18 + # Get app name
19 + app_name = input("Enter a name for your mixapp: ").strip()
20 + if not app_name:
21 + print("Error: App name is required")
22 + exit(1)
23 +
24 + # Get base path with smart default
25 + default_path = app_name.lower().replace(" ", "_")
26 + print()
27 + print(f"Enter the deployment path (or press Return/Enter for default)")
28 + print(f"Default: /{default_path}/")
29 + base_path_input = input("Path: ").strip()
30 +
31 + if base_path_input:
32 + # User provided a path - ensure it has leading/trailing slashes
33 + base_path = base_path_input
34 + if not base_path.startswith("/"):
35 + base_path = "/" + base_path
36 + if not base_path.endswith("/"):
37 + base_path = base_path + "/"
38 else:
- pip_path = VENV_DIR / "bin" / "pip"
- python_path = VENV_DIR / "bin" / "python3"
-
- # Check if venv needs to be created or recreated
- if not VENV_DIR.exists() or not python_path.exists():
- if VENV_DIR.exists():
- print("Virtual environment incomplete, recreating...")
- import shutil
- shutil.rmtree(VENV_DIR)
- else:
- print("Creating virtual environment...")
-
- try:
- subprocess.check_call([sys.executable, "-m", "venv", str(VENV_DIR)])
- print("Virtual environment created successfully.")
- except subprocess.CalledProcessError as e:
- print(f"Error creating virtual environment: {e}")
- sys.exit(1)
-
- # Ensure pip is available (sometimes venv doesn't include it)
- if not pip_path.exists():
- print("Installing pip in virtual environment...")
- try:
- subprocess.check_call([str(python_path), "-m", "ensurepip", "--upgrade"])
- except subprocess.CalledProcessError as e:
- print(f"Error ensuring pip: {e}")
- sys.exit(1)
-
- check = subprocess.run(
- [str(python_path), "-c", "import mutagen"],
- capture_output=True
- )
- if check.returncode != 0:
- try:
- subprocess.check_call([str(python_path), "-m", "pip", "install", "-q", "mutagen"])
- except subprocess.CalledProcessError:
- print("Note: Could not install mutagen (offline?). Metadata will be derived from filenames.\n")
-
- return python_path
39 + # Use default
40 + base_path = f"/{default_path}/"
41 + print(f"Using default path: {base_path}")
42
43 + print()
44 + print(f"Configuration:")
45 + print(f" App Name: {app_name}")
46 + print(f" Base Path: {base_path}")
47 + print()
48
-def run_in_venv():
- """Re-run this script in the virtual environment"""
- python_path = setup_venv()
-
- # Re-run this script with the venv Python
- print("Running scanner in virtual environment...\n")
- subprocess.check_call([str(python_path), __file__, "--in-venv"])
- sys.exit(0)
49 + return app_name, base_path
50
51 +# File paths (no need to edit these)
52 +SCRIPT_DIR = Path(__file__).parent.absolute()
53 +TRACKS_JSON = SCRIPT_DIR / "mix" / "tracks.json"
54 +STYLES_CSS = SCRIPT_DIR / "resources" / "styles.css"
55
-def scan_tracks():
- """Main function to scan audio files and generate tracks.json"""
- # Import mutagen here (only after venv is active)
- try:
- from mutagen import File as MutagenFile
- has_mutagen = True
- except ImportError:
- MutagenFile = None
- has_mutagen = False
- print("Mutagen not available. Metadata will be derived from filenames.\n")
56
- # Supported audio formats
- SUPPORTED_EXTENSIONS = ('.mp3', '.m4a', '.ogg', '.flac', '.wav')
57 +def get_background_color():
58 + """Extract the --background CSS variable from styles.css"""
59 + if not STYLES_CSS.exists():
60 + print("Warning: styles.css not found. Using default color.")
61 + return "#080a0c"
62
- # Check if tracks directory exists, create if it doesn't
- if not MIX_DIR.exists():
- print(f"Creating {MIX_DIR.name} directory...")
- MIX_DIR.mkdir(parents=True, exist_ok=True)
- print(f"✓ {MIX_DIR.name} directory created.")
- print(f"\nAdd audio files to the {MIX_DIR.name} directory and run this script again.")
- print(f"Supported formats: {', '.join(SUPPORTED_EXTENSIONS)}")
- sys.exit(0)
63 + with open(STYLES_CSS, 'r', encoding='utf-8') as f:
64 + content = f.read()
65
- # Find all supported audio files
- audio_files = [f for f in MIX_DIR.iterdir() if f.suffix.lower() in SUPPORTED_EXTENSIONS]
66 + # Look for --background: <color>; pattern
67 + match = re.search(
68 + r'--background:\s*([#a-zA-Z0-9(),.\s]+?)\s*;',
69 + content
70 + )
71 + if match:
72 + color = match.group(1).strip()
73 + print(f"Found background color in styles.css: {color}")
74 + return color
75
- if not audio_files:
- print(f"No audio files found in {MIX_DIR}")
- print(f"\nPlease add audio files to the {MIX_DIR.name} directory and run this script again.")
- print(f"Supported formats: {', '.join(SUPPORTED_EXTENSIONS)}")
- sys.exit(0)
76 + print("Warning: --background not found in styles.css. Using default color.")
77 + return "#080a0c"
78
- print(f"Found {len(audio_files)} audio file(s). Extracting metadata...\n")
79
- tracks = []
80 +def _next_cache_name(app_name, manifest_path):
81 + """Determine the next cache name by incrementing the version in an existing manifest.
82
- for audio_file in sorted(audio_files):
83 + If manifest.json doesn't exist or has no versioned cache_name, returns "{app_name}-v1".
84 + If it already has e.g. "heaven-v1", returns "heaven-v1.1".
85 + If it already has e.g. "heaven-v1.3", returns "heaven-v1.4".
86 + """
87 + if manifest_path.exists():
88 try:
- title = None
- artist = None
-
- if has_mutagen:
- audio = MutagenFile(audio_file, easy=True)
- if audio and audio.tags:
- title = audio.tags.get('title', [None])[0]
- artist = audio.tags.get('artist', [None])[0]
-
- # Fallback to filename for title if not found
- if not title:
- title = audio_file.stem # filename without extension
-
- # Fallback to "Unknown Artist" if not found
- if not artist:
- artist = "Unknown Artist"
-
- track_info = {
- "title": title,
- "artist": artist,
- "filename": audio_file.name
89 + with open(manifest_path, 'r', encoding='utf-8') as f:
90 + existing = json.load(f)
91 + old_cache = existing.get("cache_name", "")
92 + # Match pattern like "appname-v1" or "appname-v1.3"
93 + match = re.match(r'^(.+)-v(\d+)(?:\.(\d+))?$', old_cache)
94 + if match:
95 + prefix = match.group(1)
96 + major = int(match.group(2))
97 + minor = int(match.group(3)) if match.group(3) is not None else 0
98 + # If app was renamed, start fresh with new name
99 + if prefix != app_name:
100 + return f"{app_name}-v1"
101 + return f"{app_name}-v{major}.{minor + 1}"
102 + except (json.JSONDecodeError, KeyError):
103 + pass
104 + return f"{app_name}-v1"
105 +
106 +
107 +def generate_pwa_manifests(app_name=None, base_path=None):
108 + """Generate PWA manifest files based on tracks.json
109 +
110 + Args:
111 + app_name: Name of the app. If None, will be prompted via get_configuration()
112 + base_path: Base path for the app. If None, will be prompted via get_configuration()
113 + """
114 + # Get configuration if not provided
115 + if app_name is None or base_path is None:
116 + app_name, base_path = get_configuration()
117 +
118 + # Derived values
119 + short_name = app_name
120 + cache_name = _next_cache_name(app_name, SCRIPT_DIR / "manifest.json")
121 + app_description = f"{app_name}"
122 +
123 + print("Generating PWA manifests...")
124 + print(f" Cache name: {cache_name}")
125 +
126 + # Load tracks.json
127 + if not TRACKS_JSON.exists():
128 + print("Error: tracks.json not found. Run catalog.py first.")
129 + return
130 +
131 + with open(TRACKS_JSON, 'r', encoding='utf-8') as f:
132 + tracks = json.load(f)
133 +
134 + # Get background color from styles.css
135 + background_color = get_background_color()
136 +
137 + # Generate manifest.json
138 + manifest = {
139 + "id": base_path,
140 + "name": app_name,
141 + "short_name": short_name,
142 + "description": app_description,
143 + "start_url": base_path,
144 + "scope": base_path,
145 + "display": "standalone",
146 + "background_color": background_color,
147 + "theme_color": background_color,
148 + "cache_name": cache_name, # Custom field for script.js to use
149 + "icons": [
150 + {
151 + "src": f"{base_path}resources/icon.png",
152 + "sizes": "640x640",
153 + "type": "image/png",
154 + "purpose": "any maskable"
155 }
-
- tracks.append(track_info)
- print(f"✓ {track_info['artist']} - {track_info['title']}")
-
- except Exception as e:
- print(f"✗ Error reading {audio_file.name}: {e}")
- continue
-
- # Check if ALL titles start with numbers
- # If so, strip the leading numbers from all titles
- import re
- all_have_leading_numbers = all(
- re.match(r'^\d+\s*[-.]?\s*', track['title'])
- for track in tracks
- )
-
- if all_have_leading_numbers and tracks:
- print("\nDetected track numbers in all titles. Stripping them...")
- for track in tracks:
- original_title = track['title']
- # Remove leading number pattern
- cleaned_title = re.sub(r'^\d+\s*[-.]?\s*', '', original_title)
- if cleaned_title: # Only update if something remains
- track['title'] = cleaned_title
- if cleaned_title != original_title:
- print(f" {original_title} → {cleaned_title}")
-
- if not tracks:
- print("\nNo valid audio files could be processed.")
- sys.exit(1)
-
- # Write to tracks.json
- try:
- with open(OUTPUT_FILE, 'w', encoding='utf-8') as f:
- json.dump(tracks, f, indent="\t", ensure_ascii=False)
-
- print(f"\n✓ Successfully generated {OUTPUT_FILE.name} with {len(tracks)} track(s).")
-
- except Exception as e:
- print(f"\nError writing {OUTPUT_FILE.name}: {e}")
- sys.exit(1)
-
-
-def main():
- """Main entry point"""
- # Check if we're already running in venv
- if "--in-venv" not in sys.argv:
- run_in_venv()
- else:
- scan_tracks()
156 + ]
157 + }
158 +
159 + with open(SCRIPT_DIR / "manifest.json", 'w', encoding='utf-8') as f:
160 + json.dump(manifest, f, indent=2)
161 + print("✓ Generated manifest.json")
162 +
163 + # Build static files list for service worker
164 + static_files = [
165 + "./",
166 + "index.html",
167 + "manifest.json",
168 + "resources/styles.css",
169 + "resources/script.js",
170 + "mix/tracks.json",
171 + "resources/icon.png",
172 + "resources/play.svg",
173 + "resources/pause.svg",
174 + "resources/prev.svg",
175 + "resources/next.svg",
176 + "resources/repeat.svg",
177 + ]
178 +
179 + # Conditionally include optional files if they exist
180 + for optional_file in ["album_art.jpg", "custom.css", "custom.js"]:
181 + if (SCRIPT_DIR / "mix" / optional_file).exists():
182 + static_files.append(f"mix/{optional_file}")
183 +
184 + static_files_js = json.dumps(static_files)
185 +
186 + # Generate service-worker.js
187 + service_worker_content = f'''// Auto-generated service worker for {app_name} PWA
188 +const CACHE_NAME = '{cache_name}';
189 +
190 +// Get the base path from the service worker location
191 +const getBasePath = () => {{
192 + const swPath = self.location.pathname;
193 + return swPath.substring(0, swPath.lastIndexOf('/') + 1);
194 +}};
195 +
196 +const basePath = getBasePath();
197 +
198 +// Static files to cache on install
199 +const STATIC_FILES = {static_files_js};
200 +
201 +// Install event - cache static resources
202 +// Audio files will be cached by the main app's blob preloading system
203 +self.addEventListener('install', (event) => {{
204 + console.log('Service Worker installing...', 'Base path:', basePath);
205 + event.waitUntil(
206 + caches.open(CACHE_NAME).then(cache => {{
207 + // Make URLs absolute relative to service worker location
208 + const absoluteUrls = STATIC_FILES.map(url => {{
209 + if (url === './') return basePath;
210 + return new URL(url, basePath + 'index.html').href;
211 + }});
212 + console.log('Caching', absoluteUrls.length, 'static resources');
213 +
214 + // Cache files individually with better error handling
215 + // Using Promise.allSettled to continue even if some fail
216 + return Promise.allSettled(
217 + absoluteUrls.map(url =>
218 + fetch(url, {{ cache: 'no-cache' }})
219 + .then(response => {{
220 + if (!response.ok) {{
221 + throw new Error(`HTTP error! status: ${{response.status}}`);
222 + }}
223 + return cache.put(url, response);
224 + }})
225 + .then(() => console.log('✓ Cached:', url))
226 + .catch(err => {{
227 + console.error('✗ Failed to cache:', url, err);
228 + throw err;
229 + }})
230 + )
231 + ).then(results => {{
232 + const failed = results.filter(r => r.status === 'rejected');
233 + const succeeded = results.filter(r => r.status === 'fulfilled');
234 + console.log(`Cached ${{succeeded.length}}/${{results.length}} static resources`);
235 + if (failed.length > 0) {{
236 + console.warn(`Failed to cache ${{failed.length}} resources`);
237 + }}
238 + }});
239 + }}).then(() => {{
240 + console.log('Service Worker installation complete');
241 + return self.skipWaiting();
242 + }}).catch(error => {{
243 + console.error('Service Worker installation failed:', error);
244 + }})
245 + );
246 +}});
247 +
248 +// Activate event - clean up old caches
249 +self.addEventListener('activate', (event) => {{
250 + console.log('Service Worker activating...');
251 + event.waitUntil(
252 + caches.keys().then((cacheNames) => {{
253 + return Promise.all(
254 + cacheNames.map((cacheName) => {{
255 + if (cacheName !== CACHE_NAME) {{
256 + console.log('Deleting old cache:', cacheName);
257 + return caches.delete(cacheName);
258 + }}
259 + }})
260 + );
261 + }}).then(() => self.clients.claim())
262 + );
263 +}});
264 +
265 +// Fetch event - cache first, network fallback
266 +self.addEventListener('fetch', (event) => {{
267 + // Ignore non-http(s) requests like blob: URLs, data: URLs, chrome-extension:, etc.
268 + if (!event.request.url.startsWith('http')) {{
269 + return;
270 + }}
271 +
272 + event.respondWith(
273 + caches.match(event.request)
274 + .then((cachedResponse) => {{
275 + if (cachedResponse) {{
276 + console.log('✓ Serving from cache:', event.request.url);
277 + return cachedResponse;
278 + }}
279 +
280 + // Not in cache - try network
281 + console.log('⟳ Fetching from network:', event.request.url);
282 + return fetch(event.request)
283 + .then((networkResponse) => {{
284 + // Check if valid response
285 + if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') {{
286 + return networkResponse;
287 + }}
288 +
289 + // Clone and cache for future offline use
290 + const responseToCache = networkResponse.clone();
291 + caches.open(CACHE_NAME)
292 + .then((cache) => {{
293 + cache.put(event.request, responseToCache);
294 + console.log('✓ Cached from network:', event.request.url);
295 + }})
296 + .catch(err => console.error('Failed to cache:', err));
297 +
298 + return networkResponse;
299 + }})
300 + .catch((error) => {{
301 + console.error('✗ Network fetch failed for:', event.request.url, error);
302 + throw error;
303 + }});
304 + }})
305 + );
306 +}});
307 +'''
308 +
309 + with open(SCRIPT_DIR / "service-worker.js", 'w', encoding='utf-8') as f:
310 + f.write(service_worker_content)
311 + print("✓ Generated service-worker.js")
312 + print()
313 + print("PWA manifests generated successfully!")
314
315
316 if __name__ == "__main__":
- main()
317 + # When run directly, get configuration and generate manifests
318 + app_name, base_path = get_configuration()
319 + generate_pwa_manifests(app_name, base_path)
-#!/usr/bin/env python3
-"""
-Creates manifest.json and service-worker.js
-based on the contents of tracks.json
-"""
-
-import json
-import re
-from pathlib import Path
-
-def get_configuration():
- """Prompt user for configuration values"""
- print("=" * 60)
- print("PWA Configuration")
- print("=" * 60)
- print()
-
- # Get app name
- app_name = input("Enter a name for your mixapp: ").strip()
- if not app_name:
- print("Error: App name is required")
- exit(1)
-
- # Get base path with smart default
- default_path = app_name.lower().replace(" ", "_")
- print()
- print(f"Enter the deployment path (or press Return/Enter for default)")
- print(f"Default: /{default_path}/")
- base_path_input = input("Path: ").strip()
-
- if base_path_input:
- # User provided a path - ensure it has leading/trailing slashes
- base_path = base_path_input
- if not base_path.startswith("/"):
- base_path = "/" + base_path
- if not base_path.endswith("/"):
- base_path = base_path + "/"
- else:
- # Use default
- base_path = f"/{default_path}/"
- print(f"Using default path: {base_path}")
-
- print()
- print(f"Configuration:")
- print(f" App Name: {app_name}")
- print(f" Base Path: {base_path}")
- print()
-
- return app_name, base_path
-
-# File paths (no need to edit these)
-SCRIPT_DIR = Path(__file__).parent.absolute()
-TRACKS_JSON = SCRIPT_DIR / "mix" / "tracks.json"
-STYLES_CSS = SCRIPT_DIR / "resources" / "styles.css"
-
-
-def get_background_color():
- """Extract the --background CSS variable from styles.css"""
- if not STYLES_CSS.exists():
- print("Warning: styles.css not found. Using default color.")
- return "#080a0c"
-
- with open(STYLES_CSS, 'r', encoding='utf-8') as f:
- content = f.read()
-
- # Look for --background: <color>; pattern
- match = re.search(
- r'--background:\s*([#a-zA-Z0-9(),.\s]+?)\s*;',
- content
- )
- if match:
- color = match.group(1).strip()
- print(f"Found background color in styles.css: {color}")
- return color
-
- print("Warning: --background not found in styles.css. Using default color.")
- return "#080a0c"
-
-
-def _next_cache_name(app_name, manifest_path):
- """Determine the next cache name by incrementing the version in an existing manifest.
-
- If manifest.json doesn't exist or has no versioned cache_name, returns "{app_name}-v1".
- If it already has e.g. "heaven-v1", returns "heaven-v1.1".
- If it already has e.g. "heaven-v1.3", returns "heaven-v1.4".
- """
- if manifest_path.exists():
- try:
- with open(manifest_path, 'r', encoding='utf-8') as f:
- existing = json.load(f)
- old_cache = existing.get("cache_name", "")
- # Match pattern like "appname-v1" or "appname-v1.3"
- match = re.match(r'^(.+)-v(\d+)(?:\.(\d+))?$', old_cache)
- if match:
- prefix = match.group(1)
- major = int(match.group(2))
- minor = int(match.group(3)) if match.group(3) is not None else 0
- # If app was renamed, start fresh with new name
- if prefix != app_name:
- return f"{app_name}-v1"
- return f"{app_name}-v{major}.{minor + 1}"
- except (json.JSONDecodeError, KeyError):
- pass
- return f"{app_name}-v1"
-
-
-def generate_pwa_manifests(app_name=None, base_path=None):
- """Generate PWA manifest files based on tracks.json
-
- Args:
- app_name: Name of the app. If None, will be prompted via get_configuration()
- base_path: Base path for the app. If None, will be prompted via get_configuration()
- """
- # Get configuration if not provided
- if app_name is None or base_path is None:
- app_name, base_path = get_configuration()
-
- # Derived values
- short_name = app_name
- cache_name = _next_cache_name(app_name, SCRIPT_DIR / "manifest.json")
- app_description = f"{app_name}"
-
- print("Generating PWA manifests...")
- print(f" Cache name: {cache_name}")
-
- # Load tracks.json
- if not TRACKS_JSON.exists():
- print("Error: tracks.json not found. Run build.py first.")
- return
-
- with open(TRACKS_JSON, 'r', encoding='utf-8') as f:
- tracks = json.load(f)
-
- # Get background color from styles.css
- background_color = get_background_color()
-
- # Generate manifest.json
- manifest = {
- "id": base_path,
- "name": app_name,
- "short_name": short_name,
- "description": app_description,
- "start_url": base_path,
- "scope": base_path,
- "display": "standalone",
- "background_color": background_color,
- "theme_color": background_color,
- "cache_name": cache_name, # Custom field for script.js to use
- "icons": [
- {
- "src": f"{base_path}resources/icon.png",
- "sizes": "640x640",
- "type": "image/png",
- "purpose": "any maskable"
- }
- ]
- }
-
- with open(SCRIPT_DIR / "manifest.json", 'w', encoding='utf-8') as f:
- json.dump(manifest, f, indent=2)
- print("✓ Generated manifest.json")
-
- # Build static files list for service worker
- static_files = [
- "./",
- "index.html",
- "manifest.json",
- "resources/styles.css",
- "resources/script.js",
- "mix/tracks.json",
- "resources/icon.png",
- "resources/play.svg",
- "resources/pause.svg",
- "resources/prev.svg",
- "resources/next.svg",
- "resources/repeat.svg",
- ]
-
- # Conditionally include optional files if they exist
- for optional_file in ["album_art.jpg", "custom.css", "custom.js"]:
- if (SCRIPT_DIR / "mix" / optional_file).exists():
- static_files.append(f"mix/{optional_file}")
-
- static_files_js = json.dumps(static_files)
-
- # Generate service-worker.js
- service_worker_content = f'''// Auto-generated service worker for {app_name} PWA
-const CACHE_NAME = '{cache_name}';
-
-// Get the base path from the service worker location
-const getBasePath = () => {{
- const swPath = self.location.pathname;
- return swPath.substring(0, swPath.lastIndexOf('/') + 1);
-}};
-
-const basePath = getBasePath();
-
-// Static files to cache on install
-const STATIC_FILES = {static_files_js};
-
-// Install event - cache static resources
-// Audio files will be cached by the main app's blob preloading system
-self.addEventListener('install', (event) => {{
- console.log('Service Worker installing...', 'Base path:', basePath);
- event.waitUntil(
- caches.open(CACHE_NAME).then(cache => {{
- // Make URLs absolute relative to service worker location
- const absoluteUrls = STATIC_FILES.map(url => {{
- if (url === './') return basePath;
- return new URL(url, basePath + 'index.html').href;
- }});
- console.log('Caching', absoluteUrls.length, 'static resources');
-
- // Cache files individually with better error handling
- // Using Promise.allSettled to continue even if some fail
- return Promise.allSettled(
- absoluteUrls.map(url =>
- fetch(url, {{ cache: 'no-cache' }})
- .then(response => {{
- if (!response.ok) {{
- throw new Error(`HTTP error! status: ${{response.status}}`);
- }}
- return cache.put(url, response);
- }})
- .then(() => console.log('✓ Cached:', url))
- .catch(err => {{
- console.error('✗ Failed to cache:', url, err);
- throw err;
- }})
- )
- ).then(results => {{
- const failed = results.filter(r => r.status === 'rejected');
- const succeeded = results.filter(r => r.status === 'fulfilled');
- console.log(`Cached ${{succeeded.length}}/${{results.length}} static resources`);
- if (failed.length > 0) {{
- console.warn(`Failed to cache ${{failed.length}} resources`);
- }}
- }});
- }}).then(() => {{
- console.log('Service Worker installation complete');
- return self.skipWaiting();
- }}).catch(error => {{
- console.error('Service Worker installation failed:', error);
- }})
- );
-}});
-
-// Activate event - clean up old caches
-self.addEventListener('activate', (event) => {{
- console.log('Service Worker activating...');
- event.waitUntil(
- caches.keys().then((cacheNames) => {{
- return Promise.all(
- cacheNames.map((cacheName) => {{
- if (cacheName !== CACHE_NAME) {{
- console.log('Deleting old cache:', cacheName);
- return caches.delete(cacheName);
- }}
- }})
- );
- }}).then(() => self.clients.claim())
- );
-}});
-
-// Fetch event - cache first, network fallback
-self.addEventListener('fetch', (event) => {{
- // Ignore non-http(s) requests like blob: URLs, data: URLs, chrome-extension:, etc.
- if (!event.request.url.startsWith('http')) {{
- return;
- }}
-
- event.respondWith(
- caches.match(event.request)
- .then((cachedResponse) => {{
- if (cachedResponse) {{
- console.log('✓ Serving from cache:', event.request.url);
- return cachedResponse;
- }}
-
- // Not in cache - try network
- console.log('⟳ Fetching from network:', event.request.url);
- return fetch(event.request)
- .then((networkResponse) => {{
- // Check if valid response
- if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') {{
- return networkResponse;
- }}
-
- // Clone and cache for future offline use
- const responseToCache = networkResponse.clone();
- caches.open(CACHE_NAME)
- .then((cache) => {{
- cache.put(event.request, responseToCache);
- console.log('✓ Cached from network:', event.request.url);
- }})
- .catch(err => console.error('Failed to cache:', err));
-
- return networkResponse;
- }})
- .catch((error) => {{
- console.error('✗ Network fetch failed for:', event.request.url, error);
- throw error;
- }});
- }})
- );
-}});
-'''
-
- with open(SCRIPT_DIR / "service-worker.js", 'w', encoding='utf-8') as f:
- f.write(service_worker_content)
- print("✓ Generated service-worker.js")
- print()
- print("PWA manifests generated successfully!")
-
-
-if __name__ == "__main__":
- # When run directly, get configuration and generate manifests
- app_name, base_path = get_configuration()
- generate_pwa_manifests(app_name, base_path)
1 # /mix
2
-add your audio files here, then run `build.py` to create `tracks.json`.
3 +add your audio files here, then run `catalog.py` to create `tracks.json`.
4
5 supported formats: `.mp3`, `.m4a`, `.ogg`, `.flac`, `.wav`
6
40 - add your audio files to the `/mix` directory, or use:
41 - `./rip.py` to rip tracks from a physical CD
42 - `./buy.py` to search for tracks to purchase (opens in iTunes on MacOS, <a href="https://song.link/i/1651294855">song.link</a> otherwise)
- - run `./build.py` to parse `/mix` and populate `tracks.json`, which defines the tracks available to the player. after running `./build.py` once, you can manually edit `tracks.json` to refine your mix.
43 + - run `./catalog.py` to parse `/mix` and populate `tracks.json`, which defines the tracks available to the player. after running `./catalog.py` once, you can manually edit `tracks.json` to refine your mix.
44 - optionally, add an `album_art.jpg` to `/mix` to set the cover art for your mix.
45 - supported audio formats: `.mp3`, `.m4a`, `.ogg`, `.flac`, `.wav`
46
48 - run `./host.py` to start a local HTTP server for testing. you can scan the QR code printed to the terminal to test the app from any device on your local network.
49
50 3. **manifesting**
- - run `./generate_manifests.py` and follow the interactive prompts to specify an app name and the remote server path where your app will be hosted.
- - this creates the config files that enable offline functionality: `manifest.json` and `service-worker.js`.
51 + - run `./build.py` and follow the interactive prompts to specify an app name and the remote server path where your mixapp will be hosted.
52 + - this creates the config files that enable offline functionality: `manifest.json` and `service-worker.js`
53
54 4. **ship it**
55 - upload the entire project directory to any web host with HTTPS support (GitHub Pages, AWS S3, etc.)
363 if success_count > 0:
364 print(f"\nTracks saved to: {MIX_DIR}")
365 print("\nNext steps:")
- print(" 1. Run build.py to generate tracks.json with metadata")
366 + print(" 1. Run catalog.py to generate tracks.json with metadata")
367 print(" 2. Run host.py to test your mixtape locally")
368
369 # Eject the CD