AUFGERAUMT: Root auf 10 sichtbare Elemente reduziert
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell. Jetzt ist sauber getrennt: ROOT (was der Nutzer sieht und braucht): - run.py = das Programm - hms_app/ = der Anwendungscode - HMS MediaEngine.app = macOS Doppelklick-Starter - HMS-Start.vbs = Windows Doppelklick-Starter - HMS-Install.vbs = Windows Erst-Installation - HMS-Mac-Install.command = macOS Homebrew-Installation - HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix) - installer_gui.py = grafischer Installer - launcher.pyw + launcher_core.py = interne Start-Logik - LIESMICH.txt = 10-Zeilen-Kurzanleitung - .gitignore _entwicklung/ (alles andere, NICHT benoetigt): - packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/ build/ fixture_profiles/ - PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md - pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann _entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne. Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""Discovery: mDNS-Service-Modell + manuelle Fallback-Liste (PLAN.md §6.3;
|
||||
ADR-0009).
|
||||
|
||||
- Service-Typ: _hmsmedia._tcp.local.
|
||||
- TXT nur kleine, nicht vertrauliche Daten: proto, node, roles, port, caps
|
||||
- manuelle Node-Liste für VLANs/geroutete Netze (§6.3)
|
||||
- IP-Wechsel ändert die node_id nicht (§6.3)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
SERVICE_TYPE = "_hmsmedia._tcp.local."
|
||||
DISCOVERY_PROTOCOL_VERSION = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ServiceInfo:
|
||||
"""mDNS-Ankündigung eines Nodes (TXT-Inhalt, ADR-0009)."""
|
||||
|
||||
node_id: str
|
||||
display_name: str
|
||||
port: int
|
||||
roles: tuple[str, ...]
|
||||
protocol_version: int = DISCOVERY_PROTOCOL_VERSION
|
||||
capability_digest: str = ""
|
||||
|
||||
@property
|
||||
def instance_name(self) -> str:
|
||||
"""Eindeutiger Instanzname: bereinigter Anzeigename."""
|
||||
safe = "".join(c for c in self.display_name if c.isalnum() or c in " -_")
|
||||
return safe[:63] or self.node_id[:8]
|
||||
|
||||
def txt(self) -> dict[str, str]:
|
||||
"""TXT-Record: klein, nicht vertraulich (ADR-0009, §27.1)."""
|
||||
return {
|
||||
"proto": str(self.protocol_version),
|
||||
"node": self.node_id,
|
||||
"roles": ",".join(self.roles),
|
||||
"port": str(self.port),
|
||||
"caps": self.capability_digest[:16],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_txt(cls, instance_name: str, port: int, txt: dict[str, str]) -> ServiceInfo:
|
||||
"""Parst eine fremde Ankündigung; wirft bei unvollständigen Daten."""
|
||||
required = ("proto", "node", "port")
|
||||
missing = [k for k in required if k not in txt]
|
||||
if missing:
|
||||
raise ValueError(f"TXT unvollstaendig, fehlt: {missing}")
|
||||
node_id = txt["node"]
|
||||
if len(node_id) < 8:
|
||||
raise ValueError("node-Eintrag ungueltig")
|
||||
roles = tuple(r for r in txt.get("roles", "").split(",") if r)
|
||||
return cls(
|
||||
node_id=node_id,
|
||||
display_name=instance_name,
|
||||
port=int(txt["port"]) or port,
|
||||
roles=roles,
|
||||
protocol_version=int(txt["proto"]),
|
||||
capability_digest=txt.get("caps", ""),
|
||||
)
|
||||
|
||||
|
||||
def capability_digest(capabilities: dict) -> str:
|
||||
"""Kurzer, stabiler Digest über Capabilities (ADR-0009 TXT 'caps')."""
|
||||
canonical = json.dumps(capabilities, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ManualNodeList:
|
||||
"""Persistente manuelle Node-Liste (Fallback ohne mDNS, §6.3).
|
||||
|
||||
JSON-Format: Liste von {"host", "port", "node_id"}; ungeprüfte Hosts
|
||||
bleiben Kategorie `unknown`, bis sich die Node legitim identifiziert.
|
||||
"""
|
||||
|
||||
path: Path
|
||||
|
||||
def save(self, entries: list[dict]) -> None:
|
||||
clean = [
|
||||
{
|
||||
"host": str(e.get("host", "")),
|
||||
"port": int(e.get("port", 0)),
|
||||
"node_id": str(e.get("node_id", "")),
|
||||
}
|
||||
for e in entries
|
||||
]
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.path.write_text(
|
||||
json.dumps(clean, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
|
||||
def load(self) -> list[dict]:
|
||||
if not self.path.is_file():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
entries = []
|
||||
for item in data if isinstance(data, list) else []:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
host = str(item.get("host", ""))
|
||||
if not host:
|
||||
continue
|
||||
try:
|
||||
port = int(item.get("port", 0))
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
entries.append(
|
||||
{"host": host, "port": port, "node_id": str(item.get("node_id", ""))}
|
||||
)
|
||||
return entries
|
||||
|
||||
def add(self, host: str, port: int, node_id: str = "") -> None:
|
||||
entries = self.load()
|
||||
if any(e["host"] == host and e["port"] == port for e in entries):
|
||||
return
|
||||
entries.append({"host": host, "port": port, "node_id": node_id})
|
||||
self.save(entries)
|
||||
|
||||
def remove(self, host: str, port: int) -> None:
|
||||
entries = [e for e in self.load() if not (e["host"] == host and e["port"] == port)]
|
||||
self.save(entries)
|
||||
Reference in New Issue
Block a user