Files
hms-mediaengine/installer_gui.py
T

337 lines
13 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""HMS MediaEngine Grafischer Installer (Fenster statt Konsole).
Wird vom macOS-App-Bundle oder HMS-Start.vbs (Windows) geöffnet, wenn
Abhaengigkeiten fehlen. Der Nutzer sieht ein Fenster kein Terminal:
- macOS: GStreamer/PyGObject via Homebrew. Native Passwort-Eingabe durch
macOS-Systemdialog (osascript 'with administrator privileges').
- Windows: Offizielle Installer-GUIs (GStreamer-MSI, Python-Setup) werden
gestartet der Nutzer klickt nur 'Weiter/Fertig'.
Nach der Installation: Button 'MediaEngine starten' die App laeuft
danach unsichtbar im Hintergrund, nur der Browser ist sichtbar.
"""
from __future__ import annotations
import os
import subprocess
import sys
import tempfile
import threading
import urllib.request
import webbrowser
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox
ROOT = Path(__file__).resolve().parent
IS_MAC = sys.platform == "darwin"
IS_WIN = sys.platform == "win32"
GST_MSI = ("https://gstreamer.freedesktop.org/data/pkg/windows/"
"1.28.4/msvc/gstreamer-1.0-msvc-x86_64-1.28.4.msi")
PY_EXE = "https://www.python.org/ftp/python/3.13.2/python-3.13.2-amd64.exe"
BREW_PKGS = [
"python@3.12", "python-tk", "gstreamer", "gst-plugins-base",
"gst-plugins-good", "gst-plugins-bad", "gst-plugins-ugly",
"gst-libav", "pygobject",
]
def brew_path() -> str | None:
for p in ("/opt/homebrew/bin/brew", "/usr/local/bin/brew"):
if Path(p).exists():
return p
return None
def gst_ok() -> bool:
try:
import gi # noqa: F401
gi.require_version("Gst", "1.0")
from gi.repository import Gst
Gst.init(None)
return True
except Exception: # noqa: BLE001
return False
def py_ok() -> bool:
try:
r = subprocess.run(["python", "--version"], capture_output=True,
timeout=10,
creationflags=0x08000000 if IS_WIN else 0)
return r.returncode == 0
except Exception: # noqa: BLE001
return False
def mac_python_ok() -> bool:
"""Ein python3, das spaeter auch gi importieren kann (brew-Python)."""
for cand in ("/opt/homebrew/bin/python3", "/usr/local/bin/python3",
"python3"):
try:
r = subprocess.run([cand, "-c", "import tkinter"],
capture_output=True, timeout=10)
if r.returncode == 0:
return True
except Exception: # noqa: BLE001
continue
return False
def dl(url: str, dest: Path) -> None:
def hook(n, bs, total):
if total > 0:
pct = min(100, n * bs * 100 // total)
app.on_progress(pct)
urllib.request.urlretrieve(url, dest, hook)
def run_quiet(cmd: list[str]) -> None:
subprocess.run(cmd, check=False,
creationflags=0x08000000 if IS_WIN else 0)
class InstallerApp:
def __init__(self):
self.root = tk.Tk()
self.root.title("HMS MediaEngine Installation")
self.root.geometry("680x520")
self.root.configure(bg="#16181d")
try:
style = ttk.Style(self.root)
style.theme_use("clam")
except Exception: # noqa: BLE001
pass
self.steps: list[tuple[str, bool]] = []
self.log_lines: list[str] = []
self._build()
self.refresh_status()
# ---------- UI ----------
def _build(self):
head = tk.Frame(self.root, bg="#16181d")
head.pack(fill="x", padx=16, pady=(12, 6))
tk.Label(head, text="HMS MediaEngine",
font=("System", 17, "bold"),
bg="#16181d", fg="#e8eaee").pack(anchor="w")
tk.Label(head, text="Grafische Installation kein Terminal nötig",
bg="#16181d", fg="#8a8f9a").pack(anchor="w")
self.status_var = tk.StringVar()
tk.Label(self.root, textvariable=self.status_var,
bg="#16181d", fg="#c8ccd4").pack(fill="x", padx=16)
self.bar = ttk.Progressbar(self.root, mode="determinate",
length=640)
self.bar.pack(fill="x", padx=16, pady=8)
logbox = tk.Frame(self.root, bg="#0d0f12")
logbox.pack(fill="both", expand=True, padx=16, pady=6)
self.log = tk.Text(logbox, bg="#0d0f12", fg="#c8ccd4",
insertbackground="#c8ccd4", height=14,
relief="flat", font=("Menlo" if IS_MAC
else "Consolas", 10))
self.log.pack(fill="both", expand=True, side="left")
scroll = ttk.Scrollbar(logbox, command=self.log.yview)
scroll.pack(side="right", fill="y")
self.log.configure(yscrollcommand=scroll.set)
btns = tk.Frame(self.root, bg="#16181d")
btns.pack(fill="x", padx=16, pady=10)
self.install_btn = tk.Button(
btns, text="Fehlendes installieren",
command=self.start_install, bg="#1d3a5f", fg="#dbe9ff",
activebackground="#2a4f7f", relief="flat", padx=14, pady=7)
self.install_btn.pack(side="left")
self.start_btn = tk.Button(
btns, text="▶ MediaEngine starten",
command=self.start_engine, bg="#1f4d2a", fg="#d8ffd8",
activebackground="#2a6a3a", relief="flat", padx=14, pady=7)
self.start_btn.pack(side="left", padx=8)
tk.Button(btns, text="Beenden", command=self.root.destroy,
bg="#262a33", fg="#c8ccd4", relief="flat",
padx=14, pady=7).pack(side="right")
def on_log(self, text: str):
self.log_lines.append(text)
self.log.insert("end", text + "\n")
self.log.see("end")
self.root.update_idletasks()
def on_progress(self, pct: int):
self.bar["value"] = pct
self.root.update_idletasks()
def on_status(self, text: str):
self.status_var.set(text)
self.root.update_idletasks()
# ---------- Status ----------
def refresh_status(self):
self.steps = []
if IS_WIN:
self.steps.append(("Python 3.13", py_ok()))
gs = (os.environ.get("GSTREAMER_1_0_ROOT_MSVC_X86_64")
or "")
self.steps.append(("GStreamer 1.28",
bool(gs) or Path(
"C:\\gstreamer\\1.0\\msvc_x86_64"
).exists()))
elif IS_MAC:
self.steps.append(("Homebrew", brew_path() is not None))
self.steps.append(("Python 3 (mit tkinter)", mac_python_ok()))
self.steps.append(("GStreamer + PyGObject", gst_ok()))
else:
self.steps.append(("GStreamer (PyGObject)", gst_ok()))
self.log.delete("1.0", "end")
for name, ok in self.steps:
mark = "" if ok else ""
self.on_log(f"{mark} {name}: "
+ ("vorhanden" if ok else "fehlt wird installiert"))
missing = [n for n, ok in self.steps if not ok]
if not missing:
self.on_status("Alles bereit du kannst MediaEngine starten.")
self.install_btn.config(state="disabled")
self.start_btn.config(state="normal", bg="#1f4d2a")
else:
self.on_status("Fehlt: " + ", ".join(missing)
+ " → Button links drücken.")
self.install_btn.config(state="normal")
self.start_btn.config(state="disabled", bg="#262a33")
# ---------- Installation ----------
def start_install(self):
self.install_btn.config(state="disabled")
threading.Thread(target=self._install_worker, daemon=True).start()
def _install_worker(self):
try:
if IS_MAC:
self._install_mac()
elif IS_WIN:
self._install_win()
self.on_log("")
self.on_log("Installation abgeschlossen.")
self.on_log("Falls gerade etwas installiert wurde, das Python "
"betrifft: dieses Fenster einmal schließen und "
"die App erneut starten.")
self.refresh_status()
except Exception as e: # noqa: BLE001
self.on_log(f"FEHLER: {e}")
self.on_status("Installation fehlgeschlagen siehe Log. "
"(Meldung an den Support reicht zum Fixen.)")
self.install_btn.config(state="normal")
def _osascript_admin(self, script: str, prompt: str) -> str:
esc = script.replace('\\', '\\\\').replace('"', '\\"')
code = (f'do shell script "{esc}" '
f'with administrator privileges '
f'with prompt "{prompt}"')
r = subprocess.run(["osascript", "-e", code],
capture_output=True, text=True, timeout=3600)
if r.returncode != 0:
raise RuntimeError(r.stderr.strip() or "osascript fehlgeschlagen")
return r.stdout
def _install_mac(self):
self.on_status("macOS: Installation läuft (Passwort-Dialog folgt)…")
lines = ["set -e"]
brew = brew_path()
if brew is None:
self.on_log("Homebrew fehlt wird installiert (mehrere Minuten)…")
lines.append(
'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL '
'https://raw.githubusercontent.com/Homebrew/'
'install/HEAD/install.sh)"')
# brew-Pfad robust im Skript auswaehlen (Apple Silicon vs. Intel);
# absolute Pfade machen ein eval/shellenv ueberfluessig
lines.append(
'if [ -x /opt/homebrew/bin/brew ]; then '
'BREW=/opt/homebrew/bin/brew; '
'else BREW=/usr/local/bin/brew; fi')
lines.append('"$BREW" install ' + " ".join(BREW_PKGS))
script = "\n".join(lines)
with tempfile.NamedTemporaryFile("w", suffix=".sh",
delete=False) as f:
f.write(script)
sh = f.name
os.chmod(sh, 0o700)
self.on_log("Starte Installation (macOS fragt nach deinem "
"Administrator-Passwort)…")
out = self._osascript_admin(f"/bin/bash {sh}",
"HMS MediaEngine: GStreamer und "
"Python-Komponenten installieren")
for line in (out or "").strip().splitlines()[-12:]:
self.on_log(" " + line)
Path(sh).unlink(missing_ok=True)
def _install_win(self):
if not Path("C:\\gstreamer\\1.0\\msvc_x86_64").exists() \
and not os.environ.get("GSTREAMER_1_0_ROOT_MSVC_X86_64"):
self.on_status("Windows: GStreamer-Installer (GUI) bitte "
"mit 'Weiter/Fertig' bestätigen…")
dest = Path(tempfile.gettempdir()) / "gstreamer.msi"
self.on_log("Lade GStreamer-Installer herunter…")
dl(GST_MSI, dest)
self.on_log("Öffne GStreamer-Installation (Fenster erscheint)…")
r = subprocess.run(["msiexec", "/i", str(dest)],
capture_output=False, timeout=3600)
self.on_log(f"GStreamer-Installer beendet (Code {r.returncode}).")
if not py_ok():
self.on_status("Windows: Python-Installer (GUI) bitte "
"'Add python.exe to PATH' anhaken und installieren…")
dest = Path(tempfile.gettempdir()) / "python-setup.exe"
self.on_log("Lade Python-Installer herunter…")
dl(PY_EXE, dest)
self.on_log("Öffne Python-Installation (Fenster erscheint)…")
subprocess.run([str(dest)], timeout=3600)
self.on_log("Python-Installation abgeschlossen.")
self.on_log("WICHTIG: Falls PATH soeben geändert wurde, dieses "
"Fenster schließen und HMS-Start.vbs erneut "
"doppelklicken.")
# ---------- Start ----------
def start_engine(self):
try:
if IS_WIN:
pyw = "pythonw"
subprocess.Popen(
[pyw if Path(sys.executable).stem == "python"
else sys.executable, str(ROOT / "run.py")],
cwd=str(ROOT),
creationflags=0x08000000,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
else:
subprocess.Popen(
["python3", str(ROOT / "run.py")], cwd=str(ROOT),
stdout=open("/tmp/hms-mediaengine.log", "ab"),
stderr=subprocess.STDOUT,
start_new_session=True)
self.on_status("MediaEngine startet Browser öffnet sich…")
self.root.after(2500, lambda: webbrowser.open(
"http://localhost:8080"))
self.root.after(4000, self.root.destroy)
except Exception as e: # noqa: BLE001
messagebox.showerror("HMS MediaEngine",
f"Start fehlgeschlagen:\n{e}")
def run(self):
self.root.mainloop()
if __name__ == "__main__":
app = InstallerApp()
app.run()