from __future__ import annotations import logging import os from urllib.parse import urlsplit from flask import Flask, flash, jsonify, redirect, render_template_string, request, url_for from controller.config import ControllerConfig, load_config from controller.models import StoreError from controller.service import ControllerService, ControllerUnavailableError LOG = logging.getLogger(__name__) HTML_FORM = """ YouTube Playlist Submit

Submit YouTube Playlist

Paste a playlist link below. It’ll be queued for download and assigned to the next free slot.

{% with messages = get_flashed_messages() %} {% if messages %} {% for m in messages %}
{{ m }}
{% endfor %} {% endif %} {% endwith %}

Playlists are kept for roughly 48 hours after they’re downloaded.

""" 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) 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." 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")) @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 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() if __name__ == "__main__": _run_development_server()