refactor: wire controller v2 into web runtime

This commit is contained in:
Media
2026-08-06 18:58:05 -05:00
parent 3ee7f94d72
commit 4e185b2e1c
5 changed files with 557 additions and 883 deletions
+124 -33
View File
@@ -1,10 +1,17 @@
from flask import Flask, render_template_string, request, redirect, url_for, flash
from __future__ import annotations
import logging
import os
from urllib.parse import urlsplit
from controller_core import enqueue_download, start_controller, stop_controller
from flask import Flask, flash, jsonify, redirect, render_template_string, request, url_for
app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-secret")
from controller.config import ControllerConfig, load_config
from controller.models import StoreError
from controller.service import ControllerService, ControllerUnavailableError
LOG = logging.getLogger(__name__)
HTML_FORM = """
<!doctype html>
@@ -217,41 +224,125 @@ HTML_FORM = """
</html>
"""
@app.route("/", methods=["GET"])
def index():
return render_template_string(HTML_FORM)
def _valid_media_url(value: str) -> bool:
try:
parsed = urlsplit(value)
except ValueError:
return False
return parsed.scheme.lower() in {"http", "https"} and bool(parsed.netloc)
@app.route("/submit", methods=["POST"])
def submit():
url = request.form.get("url", "").strip()
submitter = request.form.get("submitter", "").strip() or None
note = request.form.get("note", "").strip() or None
def _submission_message(result: dict[str, object]) -> str:
status = result.get("status")
if status == "queue_full":
seconds = max(0, int(result.get("oldest_expires_in_seconds") or 0))
hours = max(1, (seconds + 3599) // 3600)
return f"Queue is full. Oldest playlist expires in about {hours} hour(s)."
if status == "slot_busy":
return f"Slot {result.get('slot')} is already being downloaded."
if status == "slot_protected":
return (
f"Slot {result.get('slot')} is protected by active or uncertain playback; "
"the playlist was not replaced."
)
if status == "shutting_down":
return "The Controller is shutting down; please try again shortly."
if status == "worker_error":
return "The playlist was reserved, but its download worker could not start."
if status != "queued":
return "The Controller could not queue that playlist; please try again."
if not url.startswith("http"):
flash("Invalid URL.")
slot = result.get("slot")
message = f"Playlist queued in slot {slot}."
if result.get("reused_slot"):
message += " (Existing slot – only new videos will be added.)"
else:
message += " It will start downloading shortly."
return message
def create_app(
config: ControllerConfig,
service: ControllerService,
*,
testing: bool = False,
) -> Flask:
"""Create the HTTP surface without starting MQTT, threads, or persistence."""
secret_key = os.environ.get("FLASK_SECRET_KEY")
if not secret_key and not testing:
raise RuntimeError("FLASK_SECRET_KEY must be set for the Controller web service")
web_app = Flask(__name__)
web_app.config.update(
TESTING=testing,
SECRET_KEY=secret_key or "controller-test-only-secret",
)
web_app.extensions["controller_service"] = service
web_app.extensions["controller_config"] = config
@web_app.get("/")
def index():
return render_template_string(HTML_FORM)
@web_app.post("/submit")
def submit():
url = request.form.get("url", "").strip()
submitter = request.form.get("submitter", "").strip() or None
note = request.form.get("note", "").strip() or None
if not _valid_media_url(url):
flash("Invalid URL. Use a complete http:// or https:// address.")
return redirect(url_for("index"))
try:
result = service.enqueue_download(url, submitter, note)
except ControllerUnavailableError as exc:
LOG.warning("playlist submission was rejected: %s", exc)
flash("The Controller is unavailable; please try again shortly.")
except StoreError as exc:
LOG.error("playlist submission hit a storage error: %s", exc)
flash("Controller storage is unavailable; the playlist was not queued.")
except ValueError as exc:
LOG.warning("playlist submission was rejected: %s", exc)
flash(f"The playlist request was rejected: {exc}")
except Exception:
LOG.exception("playlist submission failed unexpectedly")
flash("The Controller could not queue that playlist; please try again.")
else:
flash(_submission_message(result))
return redirect(url_for("index"))
result = enqueue_download(url, submitter, note)
@web_app.get("/healthz")
def healthz():
try:
health = service.health()
except Exception as exc:
LOG.exception("Controller health check failed")
return jsonify(
{
"status": "unavailable",
"started": False,
"coordinator_alive": False,
"error": str(exc),
}
), 503
serving = bool(health.get("started") and health.get("coordinator_alive"))
return jsonify(health), 200 if serving else 503
if result.get("status") == "queue_full":
secs = result.get("oldest_expires_in_seconds", 0)
hours = max(1, secs // 3600)
flash(f"Queue is full. Oldest playlist expires in about {hours} hour(s).")
else:
slot = result.get("slot")
msg = f"Playlist queued in slot {slot}."
if result.get("reused_slot"):
msg += " (Existing slot – only new videos will be added.)"
else:
msg += " It will start downloading shortly."
flash(msg)
return web_app
def _run_development_server() -> None:
config = load_config()
service = ControllerService(config)
web_app = create_app(config, service)
service.start()
try:
web_app.run(host=config.web.host, port=config.web.port)
finally:
service.stop()
return redirect(url_for("index"))
if __name__ == "__main__":
start_controller()
try:
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7531)))
finally:
stop_controller()
_run_development_server()