working containerized build

This commit is contained in:
2026-06-13 13:27:29 -07:00
parent 34690b3691
commit 530fd93a0b
12 changed files with 441 additions and 10 deletions
View File
+136
View File
@@ -0,0 +1,136 @@
"""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"""
<div class="episode">
<audio controls preload="none">
<source src="/{f}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<div class="episode-meta">
<span class="episode-title">{safe_name}</span>
<a href="/{f}" class="download-btn" download>Download</a>
</div>
</div>"""
if not mp3_files:
audio_entries = '<p class="empty">No newscast files available yet. The pipeline will populate this directory.</p>'
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Guardian Daily Newscast</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f1117; color: #e1e4e8; min-height: 100vh; }}
.container {{ max-width: 720px; margin: 0 auto; padding: 2rem 1rem; }}
h1 {{ font-size: 1.8rem; margin-bottom: 0.25rem; color: #fff; }}
.subtitle {{ color: #8b949e; margin-bottom: 2rem; font-size: 0.95rem; }}
.episode {{ background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; margin-bottom: 0.75rem; }}
audio {{ width: 100%; margin-bottom: 0.5rem; border-radius: 4px; }}
.episode-meta {{ display: flex; align-items: center; justify-content: space-between; gap: 1rem; }}
.episode-title {{ color: #c9d1d9; font-size: 0.9rem; word-break: break-word; }}
.download-btn {{ background: #238636; color: #fff; padding: 0.35rem 0.75rem; border-radius: 6px; text-decoration: none; font-size: 0.85rem; white-space: nowrap; }}
.download-btn:hover {{ background: #2ea043; }}
.empty {{ text-align: center; color: #8b949e; padding: 3rem 0; }}
.refresh-btn {{ background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; margin-bottom: 1.5rem; }}
.refresh-btn:hover {{ background: #30363d; }}
</style>
<script>
function refresh() {{ location.reload(); }}
setInterval(refresh, 300000);
</script>
</head>
<body>
<div class="container">
<h1>Guardian Daily Newscast</h1>
<p class="subtitle">Daily US news — spoken by AI</p>
<button class="refresh-btn" onclick="refresh()">Refresh</button>
{audio_entries}
</div>
</body>
</html>"""
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()