GENERATOREN + MEDIEN-VERTEILUNG + AUTOSTART-SETTINGS (33/33 E2E)

1) MADRIX-artige Generator-Layer (15 prozedurale Quellen):
- engine.GENERATORS: ball (animiert), gradient, pinwheel (animiert),
  zone-plate (animiert), checkers, snow (Rauschen animiert), circular,
  spokes, smpte, gamut, red/green/blue/black/white - alle per
  gst-launch verifiziert, kombinierbar mit FX1/FX2 (Generator+FX
  gerendert, 28 Frames im Test)
- add_generator + Pattern-Wechsel zur Laufzeit (ball->snow) mit
  Rebuild, Katalog-Endpoint /api/generators, UI-Dropdown im Layer-Tab
- Generatoren in Projekten speicher-/ladbar (gradient nach Load aktiv)
- Ungueltige Generator-IDs werden mit 400 abgewiesen

2) Medien-Verteilung ueber mehrere Server:
- hms_app/distribution.py: Server-Check (/api/health),
  Remote-Medien-Liste, Streaming-Import mit Upload-Sicherung
- config.servers: Remote-Server registrieren (Name+URL, HTTP-only,
  Duplikat-Schutz), /api/servers (online/offline + Medien),
  servers/import: Datei von Remote ins lokale Archiv ziehen
- UI: Remote-Server in Einstellungen verwalten, Remote-Medien im
  Medien-Tab mit 'Ins Archiv ziehen'-Button
- run.py --data-root: mehrere Instanzen auf einer Maschine mit
  getrennten config/media/projects-Ordnern
- E2E mit ZWEI echten Servern: A registriert B, sieht B-Medien,
  zieht clip byte-identisch (SHA verifiziert) und spielt ihn als
  Layer; Offline-Erkennung nach B-Stop; 404/400-Faelle

3) Autostart in den Einstellungen:
- Einstellungen: Autostart-Projekt-Dropdown (aus Projektliste),
  abwaehlbar; Projekte-Tab weiterhin mit Autostart-Button
- FIX Settings-Save-Merge: POST /api/settings merged jetzt auf die
  AKTUELLEN Settings - Autostart und registrierte Server gehen beim
  Speichern anderer Einstellungen nicht mehr verloren (E2E: Autostart
  ueberlebt Settings-Save, FPS uebernommen)

E2E: 33/33 PASS (Verteilung/Generatoren) + Regressionen:
Projekte 25/25, App 14/14, FX 16/16 - alle gruen
This commit is contained in:
HMS MediaEngine Agent
2026-09-11 13:11:54 +02:00
parent 70502458d8
commit 7e191ff6c9
7 changed files with 550 additions and 21 deletions
+85 -1
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import copy
import json
import threading
import time
@@ -9,6 +10,7 @@ import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from . import config as cfgmod
from . import distribution
from .projects import ProjectStore
from .ui import HTML_UI
@@ -102,6 +104,23 @@ class EngineServer:
self._json(artnet.monitor())
elif path == "/api/fx":
self._json(engine.fx_catalog())
elif path == "/api/generators":
self._json({"items": engine.generators()})
elif path == "/api/servers":
out = []
for s_ in settings.get("servers", []):
entry = {"name": s_.get("name"),
"url": s_.get("url"),
"online": False, "items": []}
try:
data = distribution.list_media(
str(s_.get("url", "")))
entry["online"] = True
entry["items"] = data.get("items", [])
except Exception: # noqa: BLE001
pass
out.append(entry)
self._json({"servers": out})
elif path == "/api/projects":
self._json({"items": server.projects.list(),
"autostart": settings["engine"].get(
@@ -134,6 +153,67 @@ class EngineServer:
self._send_error_json(400, engine.last_error or "?" )
else:
self._json({"layer": layer})
elif path == "/api/layers/add-generator":
body = self._read_json()
layer = engine.add_generator(
str(body.get("generator", "")))
if layer is None:
self._send_error_json(
400, engine.last_error or "unbekannter Generator")
else:
self._json({"layer": layer})
elif path == "/api/servers/add":
body = self._read_json()
name = str(body.get("name", "")).strip()
url = str(body.get("url", "")).strip()
if (not name or not url.startswith(
("http://", "https://"))):
self._send_error_json(
400, "Name und http(s)-URL erforderlich")
elif any(s_.get("name") == name
for s_ in settings.get("servers", [])):
self._send_error_json(400, "Name bereits vergeben")
elif not distribution.check(url):
self._json({"added": False,
"error": "Server nicht erreichbar "
"oder kein HMS MediaEngine"})
else:
settings.setdefault("servers", []).append(
{"name": name, "url": url})
cfgmod.save(root, settings)
self._json({"added": True, "name": name})
elif path == "/api/servers/remove":
body = self._read_json()
name = str(body.get("name", ""))
before = len(settings.get("servers", []))
settings["servers"] = [
s_ for s_ in settings.get("servers", [])
if s_.get("name") != name]
if len(settings["servers"]) == before:
self._send_error_json(404, "Server nicht gefunden")
else:
cfgmod.save(root, settings)
self._json({"removed": True})
elif path == "/api/servers/import":
body = self._read_json()
srv_name = str(body.get("server", ""))
media_name = str(body.get("media", ""))
url = None
for s_ in settings.get("servers", []):
if s_.get("name") == srv_name:
url = str(s_.get("url", ""))
break
if url is None:
self._send_error_json(404, "Server nicht gefunden")
elif not media_name:
self._send_error_json(400, "Medienname fehlt")
else:
ok, result = distribution.import_media(
url, media_name, library)
if ok:
self._json({"saved": result})
else:
self._send_error_json(400, str(result))
elif path == "/api/layers/remove":
body = self._read_json()
ok = engine.remove_layer(int(body.get("id", -1)))
@@ -226,7 +306,11 @@ class EngineServer:
def _post_settings(self):
body = self._read_json()
normalized, err = cfgmod.validate(body)
# WICHTIG: auf aktuelle Settings mergen, damit nicht
# gesendete Felder (autostart_project, servers) erhalten
# bleiben und nicht auf Defaults zurueckfallen.
merged = cfgmod.deep_merge(copy.deepcopy(settings), body)
normalized, err = cfgmod.validate(merged)
if err is not None:
self._send_error_json(400, err)
return