refactor: wire controller v2 into web runtime
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user