PROJEKTE: Speichern/Laden/Autostart (25/25 E2E inkl. Server-Neustart)
- hms_app/projects.py: ProjectStore mit atomarem Speichern (.hms.json in projects/), Format-Kennung, Namens-Sanitize (Pfad-Traversal-Schutz, Leerzeichen erlaubt), Liste/Loeschen - engine.py: export_state() (Layer mit Alpha/Pos/Groesse/Z/FX + Intensitaeten + Master) und load_state() (ersetzt alle Layer, fehlende Medien werden uebersprungen und gemeldet, Werte geclamped, ungueltige FX-IDs verworfen) - server.py: /api/projects (GET Liste + Autostart), POST projects/save|load|delete|autostart; Loeschen des Autostart-Projekts raeumt config.json automatisch auf - config.py: engine.autostart_project (validiert, optional) - run.py: laedt Autostart-Projekt beim Programmstart - ui.py: neuer Reiter 'Projekte': Name eingeben + Speichern, Tabelle mit Laden/Autostart/Loeschen, Autostart-Badge E2E-BEWEIS (25/25 PASS, mit ECHTEM Server-Neustart): 1 Server startet · 2 Upload · 3 Layer · 4 FX/Alpha/Pos gesetzt · 5 Projekt 'Show A' gespeichert · 6 Traversal-Name sanitizet zu 'evil' · 7 Liste · 8 Layerzahl · 9 Zustand geleert (master=0.1) · 10 Projekt geladen · 11 Layer wiederhergestellt · 12 Alpha 0.7 wiederhergestellt · 13 FX1 blur 0.8 wiederhergestellt · 14 Position x=50 y=20 wiederhergestellt · 15 Master 0.9 · 16 Engine rendert · 17 Autostart gesetzt · 18 Server NEU GESTARTET · 19 Autostart: Layer+FX automatisch geladen · 20 Autostart: Engine rendert (60 Frames) · 21 config.json Autostart · 22 fehlendes Projekt 404 · 23 geloescht · 24 Autostart automatisch geleert · 25 config.json geleert Regressionen: App-E2E 14/14 (app=0) + FX-E2E 16/16 (fx=0) unveraendert gruen
This commit is contained in:
@@ -509,6 +509,69 @@ class MediaEngine:
|
||||
"error": self.last_error,
|
||||
}
|
||||
|
||||
def export_state(self) -> dict:
|
||||
"""Kompletten Showzustand exportieren (fuer Projektdatei)."""
|
||||
with self._lock:
|
||||
return {
|
||||
"layers": [{
|
||||
"name": l.name, "kind": l.kind,
|
||||
"alpha": l.alpha, "x": l.x, "y": l.y,
|
||||
"width": l.width, "height": l.height, "z": l.z,
|
||||
"fx1": l.fx1, "fx2": l.fx2,
|
||||
"fx1_intensity": l.fx1_intensity,
|
||||
"fx2_intensity": l.fx2_intensity,
|
||||
} for l in self.layers],
|
||||
"master": self.master,
|
||||
}
|
||||
|
||||
def load_state(self, state: dict) -> bool:
|
||||
"""Showzustand aus Projektdatei laden (ersetzt alle Layer)."""
|
||||
if not isinstance(state, dict) or not isinstance(
|
||||
state.get("layers"), list):
|
||||
self.last_error = "ungueltiger Projekt-Zustand"
|
||||
return False
|
||||
with self._lock:
|
||||
max_l = int(self.settings["engine"].get("max_layers", 8))
|
||||
self.layers = []
|
||||
self._next_id = 1
|
||||
ok_count = 0
|
||||
for raw in state["layers"][:max_l]:
|
||||
if not isinstance(raw, dict):
|
||||
continue
|
||||
name = str(raw.get("name", ""))
|
||||
f = self.library.file_path(name)
|
||||
kind = self.library.kind_of(name)
|
||||
if not f.exists() or kind is None:
|
||||
print(f"[Projects] WARNUNG: Medium fehlt, Layer "
|
||||
f"uebersprungen: {name}")
|
||||
continue
|
||||
layer = Layer(self._next_id, name, kind)
|
||||
self._next_id += 1
|
||||
layer.alpha = max(0.0, min(1.0,
|
||||
float(raw.get("alpha", 1.0))))
|
||||
layer.x = int(raw.get("x", 0))
|
||||
layer.y = int(raw.get("y", 0))
|
||||
layer.width = max(16, min(7680, int(raw.get("width", 640))))
|
||||
layer.height = max(16, min(4320, int(raw.get("height", 360))))
|
||||
layer.z = max(0, min(15, int(raw.get("z", ok_count))))
|
||||
layer.fx1 = raw.get("fx1") if raw.get("fx1") in FX_CATALOG else None
|
||||
layer.fx2 = raw.get("fx2") if raw.get("fx2") in FX_CATALOG else None
|
||||
layer.fx1_intensity = max(0.0, min(1.0,
|
||||
float(raw.get("fx1_intensity", 0.5))))
|
||||
layer.fx2_intensity = max(0.0, min(1.0,
|
||||
float(raw.get("fx2_intensity", 0.5))))
|
||||
self.layers.append(layer)
|
||||
ok_count += 1
|
||||
m = state.get("master")
|
||||
if isinstance(m, (int, float)):
|
||||
self.master = max(0.0, min(1.0, float(m)))
|
||||
self.playing = True
|
||||
self.blackout = False
|
||||
rebuilt = self.rebuild()
|
||||
print(f"[Projects] {ok_count} Layer geladen "
|
||||
f"(Rebuild {'OK' if rebuilt else 'FEHLER'})")
|
||||
return rebuilt
|
||||
|
||||
def fx_catalog(self) -> list[dict]:
|
||||
"""Effekt-Katalog fuer die UI (id, Element, Label, Int-Bereich)."""
|
||||
out = []
|
||||
|
||||
Reference in New Issue
Block a user