"""HTTP server that serves the Guardian Daily Newscast MP3 index page and audio files.""" import os import json import threading from http.server import HTTPServer, SimpleHTTPRequestHandler from socketserver import ThreadingMixIn from urllib.parse import unquote from loguru import logger class ThreadingHTTPServer(ThreadingMixIn, HTTPServer): """HTTP server that handles each request in a new thread.""" daemon_threads = True class NewscastHandler(SimpleHTTPRequestHandler): """Custom handler that serves a dynamic MP3 index at / and static files below.""" data_dir: str = "/app/data" def do_GET(self): if self.path == "/" or self.path == "": self.serve_index() else: # Strip any leading / to get a relative path rel_path = self.path.lstrip("/") # Prevent directory traversal if ".." in rel_path or rel_path.startswith("/"): self.send_error(403, "Forbidden") return super().do_GET() def serve_index(self): """Serve the dynamic MP3 listing page.""" audio_dir = os.path.join(self.data_dir, "audio") scripts_file = os.path.join(self.data_dir, "scripts.json") # Load script metadata for timestamps scripts_map = {} if os.path.exists(scripts_file): try: with open(scripts_file, "r", encoding="utf-8") as f: scripts = json.load(f) for item in scripts: scripts_map[item["headline"]] = item except (json.JSONDecodeError, KeyError): pass mp3_files = [] if os.path.isdir(audio_dir): mp3_files = sorted( f for f in os.listdir(audio_dir) if f.lower().endswith(".mp3") ) html = _generate_index(mp3_files, scripts_map) self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(html.encode("utf-8")))) self.end_headers() self.wfile.write(html.encode("utf-8")) def log_message(self, format, *args): """Suppress per-request logs to avoid noisy output.""" pass def _generate_index(mp3_files, scripts_map): """Generate the HTML index page for the MP3 newscast.""" audio_entries = "" for f in mp3_files: safe_name = f.replace('"', """) audio_entries += f"""
{safe_name} Download
""" if not mp3_files: audio_entries = '

No newscast files available yet. The pipeline will populate this directory.

' return f""" Guardian Daily Newscast

Guardian Daily Newscast

Daily US news — spoken by AI

{audio_entries}
""" def run_server(port: int, data_dir: str) -> None: """Start the HTTP server in a background thread and block.""" os.chdir("/") # Serve from root (paths are absolute) NewscastHandler.data_dir = data_dir handler = lambda *a, **kw: NewscastHandler( *a, directory=data_dir, **kw ) server = ThreadingHTTPServer(("0.0.0.0", port), handler) logger.info(f"Web server listening on port {port}") server.serve_forever()