serve.py (6.0 KB)


  1 #!/usr/bin/env python3
  2 """
  3 Starts HTTP server for local testing
  4 Automatically manages a virtual environment for dependencies
  5 """
  6 
  7 import http.server
  8 import socketserver
  9 import socket
 10 import sys
 11 import os
 12 import json
 13 import subprocess
 14 from pathlib import Path
 15 
 16 DEFAULT_PORT = 8000
 17 SCRIPT_DIR = Path(__file__).parent.absolute()
 18 VENV_DIR = SCRIPT_DIR / "venv"
 19 
 20 
 21 def setup_venv():
 22 	"""Create and setup virtual environment if it doesn't exist"""
 23 	# Determine the path to pip and python in the venv
 24 	if sys.platform == "win32":
 25 		pip_path = VENV_DIR / "Scripts" / "pip"
 26 		python_path = VENV_DIR / "Scripts" / "python"
 27 	else:
 28 		pip_path = VENV_DIR / "bin" / "pip"
 29 		python_path = VENV_DIR / "bin" / "python3"
 30 
 31 	# Check if venv needs to be created or recreated
 32 	if not VENV_DIR.exists() or not python_path.exists():
 33 		if VENV_DIR.exists():
 34 			print("Virtual environment incomplete, recreating...")
 35 			import shutil
 36 			shutil.rmtree(VENV_DIR)
 37 		else:
 38 			print("Creating virtual environment...")
 39 
 40 		try:
 41 			subprocess.check_call([sys.executable, "-m", "venv", str(VENV_DIR)])
 42 			print("Virtual environment created successfully.")
 43 		except subprocess.CalledProcessError as e:
 44 			print(f"Error creating virtual environment: {e}")
 45 			sys.exit(1)
 46 
 47 	# Ensure pip is available (sometimes venv doesn't include it)
 48 	if not pip_path.exists():
 49 		print("Installing pip in virtual environment...")
 50 		try:
 51 			subprocess.check_call([str(python_path), "-m", "ensurepip", "--upgrade"])
 52 		except subprocess.CalledProcessError as e:
 53 			print(f"Error ensuring pip: {e}")
 54 			sys.exit(1)
 55 
 56 	check = subprocess.run(
 57 		[str(python_path), "-c", "import qrcode"],
 58 		capture_output=True
 59 	)
 60 	if check.returncode != 0:
 61 		try:
 62 			subprocess.check_call([str(python_path), "-m", "pip", "install", "-q", "qrcode"])
 63 		except subprocess.CalledProcessError:
 64 			print("Note: Could not install qrcode (offline?). QR codes will be unavailable.\n")
 65 
 66 	return python_path
 67 
 68 
 69 def run_in_venv():
 70 	"""Re-run this script in the virtual environment"""
 71 	python_path = setup_venv()
 72 
 73 	# Re-run this script with the venv Python
 74 	try:
 75 		subprocess.check_call([str(python_path), __file__, "--in-venv"])
 76 	except (KeyboardInterrupt, subprocess.CalledProcessError):
 77 		pass
 78 	sys.exit(0)
 79 
 80 
 81 def get_local_ip():
 82 	"""Get the local IP address for network access"""
 83 	try:
 84 		# Create a socket to determine the local IP
 85 		s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 86 		# Connect to a public DNS server (doesn't actually send data)
 87 		s.connect(("8.8.8.8", 80))
 88 		local_ip = s.getsockname()[0]
 89 		s.close()
 90 		return local_ip
 91 	except Exception:
 92 		return "Unable to determine"
 93 
 94 
 95 def find_available_port(start_port=DEFAULT_PORT, max_attempts=10):
 96 	"""Find an available port starting from start_port"""
 97 	for port in range(start_port, start_port + max_attempts):
 98 		try:
 99 			with socketserver.TCPServer(("", port), None) as s:
100 				return port
101 		except OSError:
102 			continue
103 	return None
104 
105 
106 def print_qr_code(url):
107 	"""Generate and print a QR code using block characters"""
108 	try:
109 		import qrcode
110 
111 		qr = qrcode.QRCode(
112 			version=1,
113 			error_correction=qrcode.constants.ERROR_CORRECT_L,
114 			box_size=1,
115 			border=1,
116 		)
117 		qr.add_data(url)
118 		qr.make(fit=True)
119 
120 		# Get the QR code matrix
121 		matrix = qr.get_matrix()
122 
123 		print("\nScan to connect:")
124 		for y in range(0, len(matrix), 2):
125 			line = ""
126 			for x in range(len(matrix[y])):
127 				top = matrix[y][x]
128 				bottom = matrix[y + 1][x] if y + 1 < len(matrix) else False
129 				if top and bottom:
130 					line += "█"
131 				elif top:
132 					line += "▀"
133 				elif bottom:
134 					line += "▄"
135 				else:
136 					line += " "
137 			print(line)
138 		print()
139 	except ImportError:
140 		print("\nQR code generation unavailable (qrcode library not installed)")
141 	except Exception as e:
142 		print(f"\nCould not generate QR code: {e}")
143 
144 def start_server():
145 	"""Start the HTTP server (runs after venv is set up)"""
146 	# Change to script directory
147 	os.chdir(SCRIPT_DIR)
148 
149 	# Find an available port
150 	port = find_available_port(DEFAULT_PORT)
151 
152 	if port is None:
153 		print(f"Error: Could not find an available port (tried {DEFAULT_PORT}-{DEFAULT_PORT + 9})")
154 		sys.exit(1)
155 
156 	# Get local IP for network access
157 	local_ip = get_local_ip()
158 
159 	# Create server
160 	Handler = http.server.SimpleHTTPRequestHandler
161 	tracks_path = SCRIPT_DIR / "mix" / "tracks.json"
162 
163 	# Suppress default logging and broken pipe errors
164 	class QuietHandler(Handler):
165 		def end_headers(self):
166 			self.send_header('Cache-Control', 'no-cache')
167 			super().end_headers()
168 
169 		def log_message(self, format, *args):
170 			pass
171 
172 		def handle(self):
173 			"""Handle requests and suppress broken pipe errors"""
174 			try:
175 				super().handle()
176 			except (BrokenPipeError, ConnectionResetError):
177 				# Browser cancelled the request (normal for media streaming/preloading)
178 				pass
179 
180 		def do_POST(self):
181 			# Local-only: receive a reordered tracks array and overwrite tracks.json
182 			if self.path != '/tracks':
183 				self.send_response(404)
184 				self.end_headers()
185 				return
186 			length = int(self.headers.get('Content-Length', '0'))
187 			payload = json.loads(self.rfile.read(length))
188 			tracks_path.write_text(
189 				json.dumps(payload, indent='\t', ensure_ascii=False) + '\n',
190 				encoding='utf-8',
191 			)
192 			self.send_response(204)
193 			self.end_headers()
194 
195 	try:
196 		with socketserver.ThreadingTCPServer(("", port), QuietHandler) as httpd:
197 			httpd.daemon_threads = True
198 			local_url = f"http://localhost:{port}"
199 			network_url = f"http://{local_ip}:{port}"
200 
201 			print("=" * 60)
202 			print("💿 mixapps")
203 			print("=" * 60)
204 			print(f"\nServer running on port {port}")
205 
206 			# Print QR code for easy mobile access
207 			print_qr_code(network_url)
208 
209 			print(f"Local access:   {local_url}")
210 			print(f"Network access: {network_url}")
211 			print("\nPress Ctrl+C to stop the server")
212 
213 			# Serve forever
214 			httpd.serve_forever()
215 
216 	except KeyboardInterrupt:
217 		print("\n\nShutting down server...")
218 		sys.exit(0)
219 	except Exception as e:
220 		print(f"\nError starting server: {e}")
221 		sys.exit(1)
222 
223 
224 def main():
225 	"""Main entry point"""
226 	# Check if we're already running in venv
227 	if "--in-venv" not in sys.argv:
228 		run_in_venv()
229 	else:
230 		start_server()
231 
232 
233 if __name__ == "__main__":
234 	main()