119 lines
3.6 KiB
Python
119 lines
3.6 KiB
Python
|
|
"""Projekt-Speicherung: Layer-/FX-Setup als Datei in projects/.
|
||
|
|
|
||
|
|
Ein Projekt enthält den kompletten Showzustand (Layer mit Position,
|
||
|
|
Alpha, Z-Order, Effekte + Intensitaeten, Master) und kann beim
|
||
|
|
Programmstart automatisch geladen werden (Autostart).
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import re
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
FORMAT = "hms-project"
|
||
|
|
VERSION = 1
|
||
|
|
SUFFIX = ".hms.json"
|
||
|
|
|
||
|
|
|
||
|
|
def sanitize(name: str) -> str:
|
||
|
|
"""Projektname sicher: keine Pfade/Sonderzeichen, Leerzeichen erlaubt."""
|
||
|
|
base = Path(str(name)).name # schneidet Pfadanteile ab (Traversal-Schutz)
|
||
|
|
base = re.sub(r"[^A-Za-z0-9._ -]", "_", base).strip("._ ")
|
||
|
|
base = re.sub(r"\s+", " ", base) # Mehrfach-Leerzeichen zusammenfassen
|
||
|
|
return base or "projekt"
|
||
|
|
|
||
|
|
|
||
|
|
class ProjectStore:
|
||
|
|
"""Dateibasierter Projektspeicher in <root>/projects/."""
|
||
|
|
|
||
|
|
def __init__(self, root: Path):
|
||
|
|
self.dir = Path(root) / "projects"
|
||
|
|
|
||
|
|
def _ensure(self) -> None:
|
||
|
|
self.dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
def path(self, name: str) -> Path:
|
||
|
|
return self.dir / (sanitize(name) + SUFFIX)
|
||
|
|
|
||
|
|
# ---------------- Speichern ----------------
|
||
|
|
|
||
|
|
def save(self, name: str, state: dict) -> dict | None:
|
||
|
|
"""Speichert den Zustand atomar. Rueckgabe: Info-Dict oder None."""
|
||
|
|
self._ensure()
|
||
|
|
final = sanitize(name)
|
||
|
|
if not final:
|
||
|
|
return None
|
||
|
|
doc = {
|
||
|
|
"format": FORMAT,
|
||
|
|
"version": VERSION,
|
||
|
|
"name": final,
|
||
|
|
"saved_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||
|
|
"saved_at_epoch": time.time(),
|
||
|
|
"state": state,
|
||
|
|
}
|
||
|
|
target = self.dir / (final + SUFFIX)
|
||
|
|
tmp = target.with_suffix(SUFFIX + ".tmp")
|
||
|
|
try:
|
||
|
|
tmp.write_text(
|
||
|
|
json.dumps(doc, ensure_ascii=False, indent=2), "utf-8")
|
||
|
|
tmp.replace(target)
|
||
|
|
except OSError as e:
|
||
|
|
print(f"[Projects] Schreiben fehlgeschlagen: {e}")
|
||
|
|
tmp.unlink(missing_ok=True)
|
||
|
|
return None
|
||
|
|
return {
|
||
|
|
"saved": final,
|
||
|
|
"saved_at": doc["saved_at"],
|
||
|
|
"layers": len(state.get("layers", [])),
|
||
|
|
}
|
||
|
|
|
||
|
|
# ---------------- Laden ----------------
|
||
|
|
|
||
|
|
def load(self, name: str) -> dict | None:
|
||
|
|
"""Liest ein Projekt. Rueckgabe: geparstes Dokument oder None."""
|
||
|
|
p = self.path(name)
|
||
|
|
if not p.is_file():
|
||
|
|
return None
|
||
|
|
try:
|
||
|
|
doc = json.loads(p.read_text("utf-8"))
|
||
|
|
except (OSError, ValueError) as e:
|
||
|
|
print(f"[Projects] Lesefehler ({p.name}): {e}")
|
||
|
|
return None
|
||
|
|
if not isinstance(doc, dict) or doc.get("format") != FORMAT:
|
||
|
|
return None
|
||
|
|
return doc
|
||
|
|
|
||
|
|
def exists(self, name: str) -> bool:
|
||
|
|
return self.load(name) is not None
|
||
|
|
|
||
|
|
# ---------------- Liste / Loeschen ----------------
|
||
|
|
|
||
|
|
def list(self) -> list[dict]:
|
||
|
|
self._ensure()
|
||
|
|
items: list[dict] = []
|
||
|
|
for f in sorted(self.dir.glob("*" + SUFFIX)):
|
||
|
|
try:
|
||
|
|
doc = json.loads(f.read_text("utf-8"))
|
||
|
|
except (OSError, ValueError):
|
||
|
|
continue
|
||
|
|
if not isinstance(doc, dict) or doc.get("format") != FORMAT:
|
||
|
|
continue
|
||
|
|
items.append({
|
||
|
|
"name": str(doc.get("name", f.stem)),
|
||
|
|
"saved_at": doc.get("saved_at"),
|
||
|
|
"layers": len((doc.get("state") or {}).get("layers", [])),
|
||
|
|
})
|
||
|
|
return items
|
||
|
|
|
||
|
|
def delete(self, name: str) -> bool:
|
||
|
|
p = self.path(name)
|
||
|
|
if not p.is_file():
|
||
|
|
return False
|
||
|
|
try:
|
||
|
|
p.unlink()
|
||
|
|
return True
|
||
|
|
except OSError:
|
||
|
|
return False
|