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()