diff --git a/packages/app-launcher-server/app-launcher-server.py b/packages/app-launcher-server/app-launcher-server.py index 5dd3bb4..73ca205 100644 --- a/packages/app-launcher-server/app-launcher-server.py +++ b/packages/app-launcher-server/app-launcher-server.py @@ -5,6 +5,7 @@ import logging import os import subprocess import sys +from datetime import date from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse import psutil @@ -22,6 +23,9 @@ ALLOWED_APPS = { 'kodi': 'kodi' } +# Workout card base URL +WORKOUT_CARD_BASE_URL = 'https://ogle.fyi/ash/workout' + def is_app_running(app_name): """Check if an application is already running, returns (is_running, pid)""" command = ALLOWED_APPS.get(app_name) @@ -88,7 +92,10 @@ class AppLauncherHandler(BaseHTTPRequestHandler): response = { 'status': 'running', 'available_apps': list(ALLOWED_APPS.keys()), - 'usage': 'POST /launch/ to launch an application' + 'endpoints': { + 'POST /launch/': 'Launch an application (optional JSON body: {"args": ["url"]})', + 'POST /workout': 'Open today\'s workout card in Firefox' + } } self.wfile.write(json.dumps(response, indent=2).encode()) else: @@ -101,8 +108,21 @@ class AppLauncherHandler(BaseHTTPRequestHandler): if len(path_parts) == 2 and path_parts[0] == 'launch': app_name = path_parts[1] self.launch_app(app_name) + elif len(path_parts) == 1 and path_parts[0] == 'workout': + self.open_workout_card() else: - self.send_error(404, "Invalid endpoint. Use /launch/") + self.send_error(404, "Invalid endpoint. Use /launch/ or /workout") + + def read_post_body(self): + """Read and parse JSON body from POST request, return dict or empty dict.""" + content_length = int(self.headers.get('Content-Length', 0)) + if content_length > 0: + try: + body = self.rfile.read(content_length) + return json.loads(body.decode('utf-8')) + except (json.JSONDecodeError, UnicodeDecodeError) as e: + logger.warning(f"Failed to parse POST body as JSON: {e}") + return {} def launch_app(self, app_name): if app_name not in ALLOWED_APPS: @@ -111,30 +131,44 @@ class AppLauncherHandler(BaseHTTPRequestHandler): command = ALLOWED_APPS[app_name] + # Read optional args from POST body + body = self.read_post_body() + extra_args = body.get('args', []) + # Validate args are strings + if not isinstance(extra_args, list) or not all(isinstance(a, str) for a in extra_args): + self.send_error(400, "'args' must be a list of strings") + return + + full_command = [command] + extra_args + # Check if app is already running is_running, existing_pid = is_app_running(app_name) if is_running: - logger.info(f"Application {app_name} is already running (PID: {existing_pid}), skipping launch") - self.send_response(200) - self.send_header('Content-type', 'application/json') - self.end_headers() - response = { - 'status': 'success', - 'message': f'{app_name} is already running', - 'pid': existing_pid, - 'already_running': True - } - self.wfile.write(json.dumps(response).encode()) - return + # If extra args provided, still launch a new instance (e.g., open a URL) + if extra_args: + logger.info(f"Application {app_name} already running (PID: {existing_pid}), but extra args provided — launching new instance") + else: + logger.info(f"Application {app_name} is already running (PID: {existing_pid}), skipping launch") + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + response = { + 'status': 'success', + 'message': f'{app_name} is already running', + 'pid': existing_pid, + 'already_running': True + } + self.wfile.write(json.dumps(response).encode()) + return try: # Launch the application in the background # Ensure we have the proper environment for GUI apps env = os.environ.copy() - logger.info(f"Launching application: {command}") + logger.info(f"Launching application: {' '.join(full_command)}") process = subprocess.Popen( - [command], + full_command, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, @@ -159,12 +193,50 @@ class AppLauncherHandler(BaseHTTPRequestHandler): logger.error(f"Error launching {command}: {e}") self.send_error(500, f"Failed to launch {app_name}: {str(e)}") + def open_workout_card(self): + """Open today's workout card in Firefox.""" + today = date.today().strftime('%Y-%m-%d') + url = f"{WORKOUT_CARD_BASE_URL}/{today}.html" + logger.info(f"Opening workout card for {today}: {url}") + + # Always launch Firefox with the URL, even if already running + command = ALLOWED_APPS['firefox'] + env = os.environ.copy() + + try: + process = subprocess.Popen( + [command, url], + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True + ) + + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + response = { + 'status': 'success', + 'message': f'Opened workout card in Firefox', + 'url': url, + 'pid': process.pid + } + self.wfile.write(json.dumps(response).encode()) + + except FileNotFoundError: + logger.error(f"Firefox not found: {command}") + self.send_error(500, "Firefox not found on system") + except Exception as e: + logger.error(f"Error launching Firefox with workout card: {e}") + self.send_error(500, f"Failed to open workout card: {str(e)}") + def main(): port = int(sys.argv[1]) if len(sys.argv) > 1 else 8081 server = HTTPServer(('0.0.0.0', port), AppLauncherHandler) logger.info(f"App launcher server starting on port {port}") logger.info(f"Available applications: {list(ALLOWED_APPS.keys())}") + logger.info(f"Workout card URL: {WORKOUT_CARD_BASE_URL}/.html") try: server.serve_forever()