cache first, only hit server on cache miss; remove hardcoded strings

Hunter
Sunday, November 9th, 2025 at 9:08 PM

generate_manifests.py+67-35
readme.md+12-15
script.js+16-3

generate_manifests.py
  8  import re
  9  from pathlib import Path
 10  
    -# Configuration - Edit these for your deployment
    -BASE_PATH = "/vibe_capsule/"  # Path where this PWA will be hosted (e.g., "/" for root, "/my-playlist/" for subdirectory)
    -APP_NAME = "worn grooves"
 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:
 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 +
 49 +	return app_name, base_path
 50 +
 51 +# Get configuration from user
 52 +APP_NAME, BASE_PATH = get_configuration()
 53  
 54  # Derived values (can be manually overridden if desired)
 55  SHORT_NAME = APP_NAME

111  		"display": "standalone",
112  		"background_color": background_color,
113  		"theme_color": background_color,
114 +		"cache_name": CACHE_NAME,  # Custom field for script.js to use
115  		"icons": [
116  			{
117  				"src": "resources/icon.png",

231  	);
232  }});
233  
    -// Fetch event - serve from cache, fallback to network
234 +// Fetch event - cache first, network fallback
235  self.addEventListener('fetch', (event) => {{
236  	// Ignore non-http(s) requests like blob: URLs, data: URLs, chrome-extension:, etc.
237  	if (!event.request.url.startsWith('http')) {{

240  
241  	event.respondWith(
242  		caches.match(event.request)
    -			.then((response) => {{
    -				// Cache hit - return response
    -				if (response) {{
243 +			.then((cachedResponse) => {{
244 +				if (cachedResponse) {{
245  					console.log('✓ Serving from cache:', event.request.url);
    -					return response;
246 +					return cachedResponse;
247  				}}
248  
    -				// Cache miss - try network
249 +				// Not in cache - try network
250  				console.log('⟳ Fetching from network:', event.request.url);
    -				return fetch(event.request).then((response) => {{
    -					// Check if valid response
    -					if (!response || response.status !== 200 || response.type === 'error') {{
    -						return response;
    -					}}
    -
    -					// Clone the response for caching
    -					const responseToCache = response.clone();
    -
    -					// Cache the fetched resource
    -					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 response;
    -				}}).catch((error) => {{
    -					console.error('✗ Fetch failed for:', event.request.url, error);
    -					// If fetch fails, try to return from cache one more time
    -					return caches.match(event.request).then(cachedResponse => {{
    -						if (cachedResponse) {{
    -							console.log('✓ Serving from cache (after fetch failed):', event.request.url);
    -							return cachedResponse;
251 +				return fetch(event.request)
252 +					.then((networkResponse) => {{
253 +						// Check if valid response
254 +						if (!networkResponse || networkResponse.status !== 200 || networkResponse.type === 'error') {{
255 +							return networkResponse;
256  						}}
257 +
258 +						// Clone and cache for future offline use
259 +						const responseToCache = networkResponse.clone();
260 +						caches.open(CACHE_NAME)
261 +							.then((cache) => {{
262 +								cache.put(event.request, responseToCache);
263 +								console.log('✓ Cached from network:', event.request.url);
264 +							}})
265 +							.catch(err => console.error('Failed to cache:', err));
266 +
267 +						return networkResponse;
268 +					}})
269 +					.catch((error) => {{
270 +						console.error('✗ Network fetch failed for:', event.request.url, error);
271  						throw error;
272  					}});
    -				}});
273  			}})
274  	);
275  }});
readme.md
 1  # 💿 vibe capsule
 2  
   -resurrect the lost art of <a href="https://ihavethatonvinyl.com/liner-notes/the-lost-art-of-the-mixtape/">mixtape</a><a href="https://melos.audio/blogs/information/the-lost-art-of-the-mixtape">-making</a> by packaging folders of .MP3s as progressive web apps. 
 3 +resurrect the lost art of <a href="https://ihavethatonvinyl.com/liner-notes/the-lost-art-of-the-mixtape/">mixtape</a><a href="https://melos.audio/blogs/information/the-lost-art-of-the-mixtape">-making</a> by packaging folders of .MP3s as progressive web apps.
 4  
 5  <img src="readme_images/collection.jpeg" width="550">
 6  

24  
25  but digital things can be gifts too, if we preserve the gift-giving structure.
26  
   -this project aims to resurrect what made mixtapes meaningful: permanence, ownership, and intention. when you gift someone a vibe capsule, you're giving them a digital artifact. something that can persist on their device independent of platforms, algorithms, or corporate whim.
27 +this project aims to resurrect what made mixtapes meaningful: permanence, ownership, and intention. when you gift someone a mixapp, you're giving them a digital artifact. something that can persist on their device independent of platforms, algorithms, or corporate whim.
28  
29  it's yours, and then it's theirs.
30  

36  	- run `host.py` to start a local HTTP server for testing. scan the QR code printed to the terminal to test the app from any device on your local network.
37  
38  3. **build the PWA**
   -	- in `generate_manifests.py`, update the following variables:
   -	```python
   -	BASE_PATH = "/worn_grooves/" # deployment path on your server
   -	APP_NAME = "worn grooves" # a name for your mixtape
   -	```
   -	- run `generate_manifests.py` to create:
   -		- `manifest.json` (PWA installation requirement)
39 +	- run `generate_manifests.py` and follow the interactive prompts to specify an app name and remote server path. this generates:
40 +		- `manifest.json` (PWA configuration file)
41  		- `resource-manifest.json` (defines the files to be cached for offline use)
   -		- `service-worker.js` (manages the static file cache)
42 +		- `service-worker.js` (manages offline caching)
43  	- optionally, add an `album_art.jpg` to `/resources`. it'll be used as the cover for your mix when playing on supported devices.
   -4. **deploy your playlist** as a Progressive Web App
44 +4. **deploy your mixapp**
45  	- upload the entire project directory to any web host with HTTPS support (GitHub Pages, AWS S3, etc.)
   -	- visit your hosted URL and follow your browser's prompts to "install" or "add app to home screen" (detailed instructions <a href="https://hunterirving.github.io/web_workshop/pwa">here</a>)
   -	- once installed, the app works completely offline and runs like a native application:<br><br>
   -	<img src="readme_images/lock_screen.jpeg" width="275">
46 +5. **install on your device**
47 +	- visit the hosted URL and follow your browser's prompts to "install" or "add app to home screen" (detailed instructions <a href="https://hunterirving.github.io/web_workshop/pwa">here</a>)
48 +	- once installed, your mixapp works completely offline and runs like a native application:<br><br>
49 +	<img src="readme_images/lock_screen.jpeg" width="275"><br>
50 +	(pictured: integration with iOS lockscreen controls)
51  
52  ## intellectual property notice
   -always ensure you have the right to distribute any media files you include in public vibe capsules. 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.
53 +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.
54  
55  it may have looked like i winked just now, but that was a blink. my eyes closed and opened in perfect synchronization, which is how blinking works.
56  
script.js
  45  let isPreloadingPriority = false;
  46  let totalBytesLoaded = 0; // Track total filesize of all preloaded songs
  47  let cachedTracks = new Set(); // Track which songs are cached for offline use
  48 +let CACHE_NAME = null; // Will be loaded from manifest.json
  49 +
  50 +// Load cache name from manifest.json first
  51 +fetch('manifest.json')
  52 +	.then(response => response.json())
  53 +	.then(manifest => {
  54 +		CACHE_NAME = manifest.cache_name || manifest.name;
  55 +		console.log('Using cache name:', CACHE_NAME);
  56 +	})
  57 +	.catch(error => {
  58 +		console.error('Error loading manifest:', error);
  59 +		updateCurrentSongDisplay('Failed to load app configuration');
  60 +	});
  61  
  62  // Load tracks from tracks.json
  63  fetch('tracks.json')

 922  // Check which tracks are already cached on app load
 923  async function checkCachedTracks() {
 924  	try {
     -		const cache = await caches.open('vibe-capsule');
 925 +		const cache = await caches.open(CACHE_NAME);
 926  		const cachedRequests = await cache.keys();
 927  
 928  		// Check each song to see if it's cached

 964  // Load a track from cache into memory
 965  async function loadFromCache(filename) {
 966  	try {
     -		const cache = await caches.open('vibe-capsule');
 967 +		const cache = await caches.open(CACHE_NAME);
 968  		// Try both relative and absolute URLs
 969  		let response = await cache.match(`tracks/${filename}`);
 970  		if (!response) {

 999  // Store blob in Cache API for offline access
1000  async function storeBlobInCache(filename, blob) {
1001  	try {
     -		const cache = await caches.open('vibe-capsule');
1002 +		const cache = await caches.open(CACHE_NAME);
1003  		const response = new Response(blob, {
1004  			headers: {
1005  				'Content-Type': 'audio/mpeg',