commit 1f8cfbc903fb4c672a2a28bae6186c8943995b46
parent c8fc1d53886edca0d076d7088b33f4bc19f4860f
Author: Hunter
Date:   Sun,  9 Nov 2025 21:08:41 -0500

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

Diffstat:
Mgenerate_manifests.py | 102++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mreadme.md | 27++++++++++++---------------
Mscript.js | 19++++++++++++++++---
3 files changed, 95 insertions(+), 53 deletions(-)

diff --git a/generate_manifests.py b/generate_manifests.py @@ -8,9 +8,48 @@ import json import re from pathlib import Path -# 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" +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 + +# Get configuration from user +APP_NAME, BASE_PATH = get_configuration() # Derived values (can be manually overridden if desired) SHORT_NAME = APP_NAME @@ -72,6 +111,7 @@ def generate_pwa_manifests(): "display": "standalone", "background_color": background_color, "theme_color": background_color, + "cache_name": CACHE_NAME, # Custom field for script.js to use "icons": [ { "src": "resources/icon.png", @@ -191,7 +231,7 @@ self.addEventListener('activate', (event) => {{ ); }}); -// Fetch event - serve from cache, fallback to network +// 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')) {{ @@ -200,44 +240,36 @@ self.addEventListener('fetch', (event) => {{ event.respondWith( caches.match(event.request) - .then((response) => {{ - // Cache hit - return response - if (response) {{ + .then((cachedResponse) => {{ + if (cachedResponse) {{ console.log('✓ Serving from cache:', event.request.url); - return response; + return cachedResponse; }} - // Cache miss - try network + // Not in cache - try network 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; + 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; }}); - }}); }}) ); }}); diff --git a/readme.md b/readme.md @@ -1,6 +1,6 @@ # 💿 vibe capsule -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. +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. <img src="readme_images/collection.jpeg" width="550"> @@ -24,7 +24,7 @@ in the transition from physical mixtapes to cloud-hosted playlists, we lost the but digital things can be gifts too, if we preserve the gift-giving structure. -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. +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. it's yours, and then it's theirs. @@ -36,24 +36,21 @@ it's yours, and then it's theirs. - 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. 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) + - run `generate_manifests.py` and follow the interactive prompts to specify an app name and remote server path. this generates: + - `manifest.json` (PWA configuration file) - `resource-manifest.json` (defines the files to be cached for offline use) - - `service-worker.js` (manages the static file cache) + - `service-worker.js` (manages offline caching) - 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 +4. **deploy your mixapp** - 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"> +5. **install on your device** + - 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>) + - once installed, your mixapp works completely offline and runs like a native application:<br><br> + <img src="readme_images/lock_screen.jpeg" width="275"><br> + (pictured: integration with iOS lockscreen controls) ## 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. +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. 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. diff --git a/script.js b/script.js @@ -45,6 +45,19 @@ let priorityPreloadQueue = []; // Songs requested by user that need priority pre let isPreloadingPriority = false; let totalBytesLoaded = 0; // Track total filesize of all preloaded songs let cachedTracks = new Set(); // Track which songs are cached for offline use +let CACHE_NAME = null; // Will be loaded from manifest.json + +// Load cache name from manifest.json first +fetch('manifest.json') + .then(response => response.json()) + .then(manifest => { + CACHE_NAME = manifest.cache_name || manifest.name; + console.log('Using cache name:', CACHE_NAME); + }) + .catch(error => { + console.error('Error loading manifest:', error); + updateCurrentSongDisplay('Failed to load app configuration'); + }); // Load tracks from tracks.json fetch('tracks.json') @@ -909,7 +922,7 @@ function fetchAndPreloadSong(song, filename) { // Check which tracks are already cached on app load async function checkCachedTracks() { try { - const cache = await caches.open('vibe-capsule'); + const cache = await caches.open(CACHE_NAME); const cachedRequests = await cache.keys(); // Check each song to see if it's cached @@ -951,7 +964,7 @@ window.debugAudioState = function() { // Load a track from cache into memory async function loadFromCache(filename) { try { - const cache = await caches.open('vibe-capsule'); + const cache = await caches.open(CACHE_NAME); // Try both relative and absolute URLs let response = await cache.match(`tracks/${filename}`); if (!response) { @@ -986,7 +999,7 @@ async function loadFromCache(filename) { // Store blob in Cache API for offline access async function storeBlobInCache(filename, blob) { try { - const cache = await caches.open('vibe-capsule'); + const cache = await caches.open(CACHE_NAME); const response = new Response(blob, { headers: { 'Content-Type': 'audio/mpeg',