wip pwa support

Hunter
Sunday, November 2nd, 2025 at 11:39 PM

.gitignore+8 
generate_manifests.pynew
host.py+1-3
index.html+6 
resources/icon.pngnew
script.js+14-1

.gitignore
22  *.swp
23  *.swo
24  *~
25 +
26 +# auto-generated PWA files
27 +manifest.json
28 +resource-manifest.json
29 +service-worker.js
30 +
31 +# Deployment script (personal)
32 +push.py
generate_manifests.py
  1 +#!/usr/bin/env python3
  2 +"""
  3 +Creates manifest.json, resource-manifest.json, and service-worker.js
  4 +based on the contents of tracks.json
  5 +"""
  6 +
  7 +import json
  8 +from pathlib import Path
  9 +
 10 +SCRIPT_DIR = Path(__file__).parent.absolute()
 11 +TRACKS_JSON = SCRIPT_DIR / "tracks.json"
 12 +
 13 +
 14 +def generate_pwa_manifests():
 15 +	"""Generate PWA manifest files based on tracks.json"""
 16 +	print("Generating PWA manifests...")
 17 +
 18 +	# Load tracks.json
 19 +	if not TRACKS_JSON.exists():
 20 +		print("Error: tracks.json not found. Run scan.py first.")
 21 +		return
 22 +
 23 +	with open(TRACKS_JSON, 'r', encoding='utf-8') as f:
 24 +		tracks = json.load(f)
 25 +
 26 +	# Generate manifest.json
 27 +	manifest = {
 28 +		"id": "/vibe_capsule/",
 29 +		"name": "vibe capsule",
 30 +		"short_name": "vibe capsule",
 31 +		"description": "mixtape as artifact",
 32 +		"start_url": "/vibe_capsule/",
 33 +		"scope": "/vibe_capsule/",
 34 +		"display": "standalone",
 35 +		"background_color": "#1a1a1a",
 36 +		"theme_color": "#1a1a1a",
 37 +		"icons": [
 38 +			{
 39 +				"src": "resources/icon.png",
 40 +				"sizes": "640x640",
 41 +				"type": "image/png",
 42 +				"purpose": "any maskable"
 43 +			}
 44 +		]
 45 +	}
 46 +
 47 +	with open(SCRIPT_DIR / "manifest.json", 'w', encoding='utf-8') as f:
 48 +		json.dump(manifest, f, indent=2)
 49 +	print("✓ Generated manifest.json")
 50 +
 51 +	# Generate resource-manifest.json
 52 +	resource_manifest = {
 53 +		"static_files": [
 54 +			"./",
 55 +			"index.html",
 56 +			"styles.css",
 57 +			"script.js",
 58 +			"tracks.json",
 59 +			"resources/icon.png",
 60 +			"resources/play.png",
 61 +			"resources/pause.png",
 62 +			"resources/prev.png",
 63 +			"resources/next.png",
 64 +			"resources/album_art.jpg"
 65 +		],
 66 +		"tracks": [f"tracks/{track['filename']}" for track in tracks]
 67 +	}
 68 +
 69 +	with open(SCRIPT_DIR / "resource-manifest.json", 'w', encoding='utf-8') as f:
 70 +		json.dump(resource_manifest, f, indent=2)
 71 +	print("✓ Generated resource-manifest.json")
 72 +
 73 +	# Generate service-worker.js
 74 +	all_files = resource_manifest["static_files"] + resource_manifest["tracks"]
 75 +	service_worker_content = f'''// Auto-generated service worker for vibe capsule PWA
 76 +const CACHE_NAME = 'vibe-capsule-v3';
 77 +const urlsToCache = {json.dumps(all_files, indent=2)};
 78 +
 79 +// Get the base path from the service worker location
 80 +const getBasePath = () => {{
 81 +	const swPath = self.location.pathname;
 82 +	return swPath.substring(0, swPath.lastIndexOf('/') + 1);
 83 +}};
 84 +
 85 +const basePath = getBasePath();
 86 +
 87 +// Install event - cache all resources
 88 +self.addEventListener('install', (event) => {{
 89 +	console.log('Service Worker installing...', 'Base path:', basePath);
 90 +	event.waitUntil(
 91 +		caches.open(CACHE_NAME)
 92 +			.then((cache) => {{
 93 +				console.log('Opened cache');
 94 +				// Make URLs absolute relative to service worker location
 95 +				const absoluteUrls = urlsToCache.map(url => {{
 96 +					if (url === './') return basePath;
 97 +					return new URL(url, basePath + 'index.html').href;
 98 +				}});
 99 +				console.log('Caching', absoluteUrls.length, 'resources');
100 +				console.log('URLs to cache:', absoluteUrls);
101 +
102 +				// Cache files one by one with better error handling
103 +				return Promise.all(
104 +					absoluteUrls.map(url =>
105 +						cache.add(url)
106 +							.then(() => console.log('✓ Cached:', url))
107 +							.catch(err => console.error('✗ Failed to cache:', url, err))
108 +					)
109 +				);
110 +			}})
111 +			.then(() => {{
112 +				console.log('All resources cached successfully');
113 +				return self.skipWaiting();
114 +			}})
115 +			.catch(error => {{
116 +				console.error('Service Worker installation failed:', error);
117 +			}})
118 +	);
119 +}});
120 +
121 +// Activate event - clean up old caches
122 +self.addEventListener('activate', (event) => {{
123 +	console.log('Service Worker activating...');
124 +	event.waitUntil(
125 +		caches.keys().then((cacheNames) => {{
126 +			return Promise.all(
127 +				cacheNames.map((cacheName) => {{
128 +					if (cacheName !== CACHE_NAME) {{
129 +						console.log('Deleting old cache:', cacheName);
130 +						return caches.delete(cacheName);
131 +					}}
132 +				}})
133 +			);
134 +		}}).then(() => self.clients.claim())
135 +	);
136 +}});
137 +
138 +// Fetch event - serve from cache, fallback to network
139 +self.addEventListener('fetch', (event) => {{
140 +	event.respondWith(
141 +		caches.match(event.request)
142 +			.then((response) => {{
143 +				// Cache hit - return response
144 +				if (response) {{
145 +					console.log('Serving from cache:', event.request.url);
146 +					return response;
147 +				}}
148 +
149 +				// Cache miss - try network
150 +				console.log('Fetching from network:', event.request.url);
151 +				return fetch(event.request).then((response) => {{
152 +					// Check if valid response
153 +					if (!response || response.status !== 200) {{
154 +						return response;
155 +					}}
156 +
157 +					// Clone the response for caching
158 +					const responseToCache = response.clone();
159 +
160 +					caches.open(CACHE_NAME)
161 +						.then((cache) => {{
162 +							cache.put(event.request, responseToCache);
163 +						}});
164 +
165 +					return response;
166 +				}}).catch((error) => {{
167 +					console.error('Fetch failed; returning offline page if available:', error);
168 +					// If fetch fails, try to return from cache one more time
169 +					return caches.match(event.request);
170 +				}});
171 +			}})
172 +	);
173 +}});
174 +'''
175 +
176 +	with open(SCRIPT_DIR / "service-worker.js", 'w', encoding='utf-8') as f:
177 +		f.write(service_worker_content)
178 +	print("✓ Generated service-worker.js")
179 +	print()
180 +	print("PWA manifests generated successfully!")
181 +
182 +
183 +if __name__ == "__main__":
184 +	generate_pwa_manifests()
host.py
 1  #!/usr/bin/env python3
 2  """
   -Simple HTTP Server for vibe capsule MP3 player
   -Starts server and opens browser automatically
 3 +Starts HTTP server for local testing
 4  Automatically manages a virtual environment for dependencies
 5  """
 6  
 7  import http.server
 8  import socketserver
   -import webbrowser
 9  import socket
10  import sys
11  import os
index.html
 4  	<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>💿</text></svg>">
 5  	<meta charset="UTF-8">
 6  	<meta name="viewport" content="width=device-width, initial-scale=1.0">
 7 +	<base href="/vibe_capsule/">
 8 +	<meta name="theme-color" content="#1a1a1a">
 9 +	<meta name="apple-mobile-web-app-capable" content="yes">
10 +	<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
11 +	<link rel="apple-touch-icon" href="resources/icon.png">
12 +	<link rel="manifest" href="manifest.json">
13  	<title>vibe capsule</title>
14  	<link rel="stylesheet" href="styles.css">
15  </head>
resources/icon.png
Binary files differ.
script.js
 1 +// Register service worker for PWA functionality
 2 +if ('serviceWorker' in navigator) {
 3 +	window.addEventListener('load', () => {
 4 +		navigator.serviceWorker.register('service-worker.js')
 5 +			.then(registration => {
 6 +				console.log('Service Worker registered successfully:', registration.scope);
 7 +			})
 8 +			.catch(error => {
 9 +				console.log('Service Worker registration failed:', error);
10 +			});
11 +	});
12 +}
13 +
14  const playPauseBtn = document.getElementById('playPause');
15  const prevBtn = document.getElementById('prev');
16  const nextBtn = document.getElementById('next');

69  	})
70  	.catch(error => {
71  		console.error('Error loading tracks:', error);
   -		updateCurrentSongDisplay('Error: tracks.json not found. Run scan.py first.');
72 +		updateCurrentSongDisplay('Unable to load tracks. Please check your connection.');
73  	});
74  
75  // Audio event listeners