feat(windows): ein echtes Setup — Voraussetzungs-Pruefung, Zielordner, Ablage
Commander: "Dann haette ich beim 'Setup' auch erwartet das nen echtes setup
passiert - wo will ich das hinspeichern, nen pre requirement check usw. Da
kommt garnichts Rippy geht einfach auf."
Er hatte recht. Ein Doppelklick installierte stillschweigend nach
%LOCALAPPDATA%\Rippy und oeffnete das Fenster. Der Nutzer wurde nichts
gefragt und erfuhr nichts -- auch nicht, ob sein Rechner ueberhaupt kann, was
Rippy braucht.
Jetzt: ein Assistent mit acht Pruefungen, zwei Ordner-Wahlen und drei
Schaltern. Auf diesem Rechner nachgemessen:
OK Betriebssystem Windows 11 (Build 26200)
OK Fenster (WebView2) 151.0.4129.107
OK Optisches Laufwerk \.\G:
OK HandBrakeCLI 1.11.2
! MakeMKV nicht gefunden
-> wird von makemkv.com geholt
OK Speicherplatz 59.1 GB frei
OK Schreibrecht C:\Users\...\AppData\Local\Rippy
OK Netzwerk-Port 7788 ist frei
Drei Entwurfsentscheidungen, die dazugehoeren:
* Nur ein FEHLER blockiert, eine Warnung nicht. Eine Warnung, die den Knopf
sperrt, ist eine Bevormundung; ein Fehler, der nur warnt, ist eine Falle.
Kein Laufwerk = Warnung (reine Komprimier-Maschine ist ein vorgesehener
Betriebsfall). Kein Schreibrecht = Fehler.
* Jeder Befund sagt, was zu tun ist. Ein Befund ohne Abhilfe laesst den
Nutzer ratlos zurueck -- ein Test haelt das fuer jede Pruefung fest.
* Die Pruefungen stehen in einrichtung.py, komplett ohne Fenster pruefbar.
Im Fenstermodul steht nur Aufbau und Bruecke.
WebView2 statt Win32-Dialog: Es ist ohnehin da (Entscheid 4), der Assistent
kostet damit NULL zusaetzliche Bytes. Die Seite laedt nichts aus dem Netz --
sie muss auf einem Rechner ohne Internet aufgehen, dort wird sie am
dringendsten gebraucht. Ein Test haelt das fest.
--still und --ohne-assistent gehen weiterhin den stummen Weg. Geht das
Fenster nicht auf, wird mit den Vorgaben installiert und gesagt warum -- ein
Setup, das gar nichts tut, weil sein Fenster nicht aufging, waere schlimmer
als eines, das nicht fragt.
fix(tools): %ProgramFiles(x86)% wurde auf echten Rechnern NIE aufgeloest
Beim Bauen der Pruefung aufgefallen, und es ist ein Lehrstueck:
Testumgebung {"ProgramFiles(x86)": ...} -> C:\Program Files (x86)\...
echtes Windows {"PROGRAMFILES(X86)": ...} -> ''
Windows behandelt Umgebungsvariablen ohne Ruecksicht auf die Schreibweise und
legt sie in os.environ GROSS ab. Der Vergleich in _entfalten war exakt, fand
nie eine Uebereinstimmung, und die bekannten Installationsorte fielen aus der
Kandidatenliste. MakeMKV wurde also NUR ueber die Registry-Rueckfallebene
gefunden -- wo die fehlt oder anders aussieht, fand Rippy nichts.
Aufgefallen ist es nur, weil hier zum ersten Mal eine ECHTE Umgebung benutzt
wurde. Der Test spritzte genau die Schreibweise ein, die im Muster stand: Er
war gruener als die Wirklichkeit. Beide Tests nehmen jetzt die echte
Schreibweise, und einer prueft direkt gegen os.environ. Beim Zurueckdrehen
des Fehlers rot gesehen (5 Fehlschlaege).
Ampel lokal: 691 gruen, ruff sauber. Assistent im Bildschirmfoto belegt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
66642b8d93
commit
a4a045f303
@@ -0,0 +1,260 @@
|
||||
"""Der Einrichtungs-Assistent: prüfen, fragen, installieren.
|
||||
|
||||
## Der Befund des Commanders (28.08.2026)
|
||||
|
||||
> „Dann hätte ich beim ‚Setup' auch erwartet das nen echtes setup passiert —
|
||||
> wo will ich das hinspeichern, nen pre requirement check usw. Da kommt
|
||||
> garnichts Rippy geht einfach auf."
|
||||
|
||||
Er hat recht. Ein Doppelklick auf `RippySetup.exe` installierte stillschweigend
|
||||
nach `%LOCALAPPDATA%\\Rippy` und öffnete das Fenster. Der Nutzer wurde nichts
|
||||
gefragt und erfuhr nichts — auch nicht, ob sein Rechner überhaupt kann, was
|
||||
Rippy braucht. Wenn hinterher etwas fehlte, stand er davor und wusste nicht,
|
||||
seit wann.
|
||||
|
||||
## Was hier passiert und was NICHT
|
||||
|
||||
Dieses Modul enthält die **Prüfungen und die Entscheidungen** — keine
|
||||
Oberfläche. Das Fenster steckt in `setup_fenster.py`, damit hier alles ohne
|
||||
WebView2, ohne Fenster und ohne Installation prüfbar bleibt.
|
||||
|
||||
## Die drei Sorten Befund
|
||||
|
||||
ok geht
|
||||
warnung geht, aber der Nutzer sollte es wissen
|
||||
fehler geht NICHT — die Installation würde nichts nützen
|
||||
|
||||
Nur `fehler` blockiert. Eine Warnung, die den Knopf sperrt, ist eine
|
||||
Bevormundung; ein Fehler, der nur warnt, ist eine Falle. Ein Beispiel für
|
||||
jede Sorte:
|
||||
|
||||
* **Kein optisches Laufwerk** ist eine WARNUNG. Rippy ohne Laufwerk ist eine
|
||||
reine Komprimier-Maschine — ein vorgesehener Betriebsfall (`install.sh` sagt
|
||||
das seit Langem: „läuft trotzdem durch").
|
||||
* **Kein MakeMKV** ist eine WARNUNG, solange Rippy es holen kann.
|
||||
* **Kein Schreibrecht am Zielordner** ist ein FEHLER. Da hilft kein Zureden.
|
||||
|
||||
## Warum jede Prüfung sagt, was zu tun ist
|
||||
|
||||
Ein Befund ohne Abhilfe lässt den Nutzer ratlos zurück. Deshalb hat jeder
|
||||
Eintrag ein `abhilfe`-Feld — leer nur dann, wenn wirklich nichts zu tun ist.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
|
||||
OK = "ok"
|
||||
WARNUNG = "warnung"
|
||||
FEHLER = "fehler"
|
||||
|
||||
# Was Rippy mindestens braucht. Gemessen, nicht geschätzt: Das Paket allein ist
|
||||
# 56 MB, dazu die entpackte HandBrake-Fassung (~74 MB), die Datenbank und der
|
||||
# WebView2-Zwischenspeicher. 500 MB sind reichlich und lassen Luft.
|
||||
MINDESTPLATZ_MB = 500
|
||||
|
||||
STANDARD_PORT = 7788
|
||||
|
||||
|
||||
def _eintrag(name, zustand, text, abhilfe=""):
|
||||
return {"name": name, "zustand": zustand, "text": text, "abhilfe": abhilfe}
|
||||
|
||||
|
||||
# ── Die einzelnen Prüfungen ─────────────────────────────────────────────
|
||||
def pruefe_windows(version=None) -> dict:
|
||||
"""Windows 10 oder neuer. Darunter gibt es kein WebView2."""
|
||||
if os.name != "nt":
|
||||
return _eintrag("Betriebssystem", FEHLER, "Kein Windows",
|
||||
"Für Linux gibt es die Docker- und die Headless-Fassung.")
|
||||
bau = version if version is not None else sys.getwindowsversion().build
|
||||
if bau >= 22000:
|
||||
return _eintrag("Betriebssystem", OK, "Windows 11 (Build %d)" % bau)
|
||||
if bau >= 10240:
|
||||
return _eintrag("Betriebssystem", OK, "Windows 10 (Build %d)" % bau)
|
||||
return _eintrag("Betriebssystem", FEHLER, "Windows-Build %d ist zu alt" % bau,
|
||||
"Rippy braucht mindestens Windows 10.")
|
||||
|
||||
|
||||
def pruefe_webview(version=None) -> dict:
|
||||
"""Ohne WebView2 gibt es kein Fenster — aber Rippy läuft trotzdem.
|
||||
|
||||
Deshalb WARNUNG und nicht Fehler: Der Rückfall auf den Browser
|
||||
funktioniert, er ist nur weniger schön.
|
||||
"""
|
||||
if version is None:
|
||||
from rippy import fenster
|
||||
|
||||
version = fenster.webview2_version()
|
||||
if version:
|
||||
return _eintrag("Fenster (WebView2)", OK, version)
|
||||
return _eintrag(
|
||||
"Fenster (WebView2)", WARNUNG, "nicht gefunden",
|
||||
'Rippy öffnet sich dann im Browser. Die Laufzeit gibt es kostenlos '
|
||||
'bei Microsoft (Suchwort „WebView2 Evergreen Runtime").')
|
||||
|
||||
|
||||
def pruefe_platz(ziel: str, frei_mb=None) -> dict:
|
||||
"""Genug Platz am Zielort? Gemessen am nächsten vorhandenen Ordner."""
|
||||
from rippy import betrieb
|
||||
|
||||
if frei_mb is None:
|
||||
pfad = betrieb.naechster_vorhandener(ziel)
|
||||
if not pfad:
|
||||
return _eintrag("Speicherplatz", FEHLER,
|
||||
"Der Ort ist nicht erreichbar: %s" % ziel,
|
||||
"Einen anderen Zielordner wählen.")
|
||||
try:
|
||||
frei_mb = shutil.disk_usage(pfad).free // (1024 * 1024)
|
||||
except OSError as e:
|
||||
return _eintrag("Speicherplatz", FEHLER, "Nicht lesbar (%s)" % e,
|
||||
"Einen anderen Zielordner wählen.")
|
||||
if frei_mb >= MINDESTPLATZ_MB:
|
||||
return _eintrag("Speicherplatz", OK, "%.1f GB frei" % (frei_mb / 1024))
|
||||
return _eintrag("Speicherplatz", FEHLER,
|
||||
"nur %d MB frei, gebraucht werden %d MB"
|
||||
% (frei_mb, MINDESTPLATZ_MB),
|
||||
"Platz schaffen oder einen anderen Zielordner wählen.")
|
||||
|
||||
|
||||
def pruefe_schreibrecht(ziel: str, probe=None) -> dict:
|
||||
"""Darf Rippy dorthin schreiben? Wirklich probiert, nicht geraten.
|
||||
|
||||
Ein `os.access(..., W_OK)` lügt unter Windows: Es beantwortet die Frage
|
||||
nach dem Schreibrecht anhand des Nur-Lesen-Flags, nicht anhand der
|
||||
Zugriffsrechte. Also wird eine Datei angelegt und wieder gelöscht.
|
||||
"""
|
||||
if probe is not None:
|
||||
geklappt, grund = probe
|
||||
else:
|
||||
geklappt, grund = _schreibprobe(ziel)
|
||||
if geklappt:
|
||||
return _eintrag("Schreibrecht", OK, ziel)
|
||||
return _eintrag("Schreibrecht", FEHLER, "%s (%s)" % (ziel, grund),
|
||||
'Einen Ordner unterhalb von „Benutzer" wählen — dort '
|
||||
'braucht Rippy keine Administratorrechte.')
|
||||
|
||||
|
||||
def _schreibprobe(ziel: str):
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
os.makedirs(ziel, exist_ok=True)
|
||||
with tempfile.NamedTemporaryFile(dir=ziel, prefix=".rippy-probe",
|
||||
delete=True):
|
||||
pass
|
||||
return True, ""
|
||||
except OSError as e:
|
||||
return False, e.strerror or str(e)
|
||||
|
||||
|
||||
def pruefe_port(port: int = STANDARD_PORT, belegt=None) -> dict:
|
||||
"""Ist der Port frei? Wenn nicht, kann Rippy nicht bedienen."""
|
||||
if belegt is None:
|
||||
belegt = _port_belegt(port)
|
||||
if not belegt:
|
||||
return _eintrag("Netzwerk-Port", OK, "%d ist frei" % port)
|
||||
# Kein Fehler: Sehr oft ist es ein bereits laufendes Rippy, und dann ist
|
||||
# eine Neuinstallation genau richtig.
|
||||
return _eintrag("Netzwerk-Port", WARNUNG, "%d ist belegt" % port,
|
||||
"Läuft Rippy schon? Sonst im Setup einen anderen Port "
|
||||
"wählen.")
|
||||
|
||||
|
||||
def _port_belegt(port: int) -> bool:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.settimeout(0.5)
|
||||
try:
|
||||
s.bind(("127.0.0.1", port))
|
||||
return False
|
||||
except OSError:
|
||||
return True
|
||||
|
||||
|
||||
def pruefe_laufwerk(laufwerke=None) -> dict:
|
||||
"""Ein optisches Laufwerk. WARNUNG, kein Fehler.
|
||||
|
||||
Rippy ohne Laufwerk ist eine reine Komprimier-Maschine — ein vorgesehener
|
||||
Betriebsfall, den `install.sh` seit Langem ausdrücklich zulässt.
|
||||
"""
|
||||
if laufwerke is None:
|
||||
try:
|
||||
from rippy import drives
|
||||
|
||||
laufwerke = drives.treiber().list_optical_devices()
|
||||
except Exception: # noqa: BLE001
|
||||
laufwerke = []
|
||||
if laufwerke:
|
||||
return _eintrag("Optisches Laufwerk", OK,
|
||||
", ".join(str(x) for x in laufwerke))
|
||||
return _eintrag("Optisches Laufwerk", WARNUNG, "keins gefunden",
|
||||
"Ohne Laufwerk kann Rippy nur komprimieren, nicht rippen. "
|
||||
"Ein USB-Brenner reicht.")
|
||||
|
||||
|
||||
def pruefe_werkzeuge(bestand=None) -> list:
|
||||
"""MakeMKV und HandBrake — je ein eigener Eintrag.
|
||||
|
||||
MakeMKV fehlt => WARNUNG, weil das Setup es holen kann. Ein Fehler wäre
|
||||
es erst, wenn es danach immer noch fehlt (siehe `tools/einrichten.py`).
|
||||
"""
|
||||
from rippy.tools import katalog
|
||||
|
||||
bestand = katalog.bestand() if bestand is None else bestand
|
||||
ergebnis = []
|
||||
for name, eintrag in sorted(bestand.items()):
|
||||
titel = eintrag.get("titel", name)
|
||||
if eintrag.get("da"):
|
||||
ergebnis.append(_eintrag(titel, OK,
|
||||
eintrag.get("version") or "gefunden"))
|
||||
elif name == "handbrake":
|
||||
ergebnis.append(_eintrag(titel, OK, "wird mitgeliefert"))
|
||||
else:
|
||||
ergebnis.append(_eintrag(
|
||||
titel, WARNUNG, "nicht gefunden",
|
||||
"Das Setup lädt es von makemkv.com und startet den Installer "
|
||||
"des Herstellers. Ohne MakeMKV kann Rippy keine Disc lesen."))
|
||||
return ergebnis
|
||||
|
||||
|
||||
# ── Alles zusammen ──────────────────────────────────────────────────────
|
||||
def alle_pruefungen(ziel: str, port: int = STANDARD_PORT) -> list:
|
||||
"""Der vollständige Voraussetzungs-Bericht, in Anzeigereihenfolge."""
|
||||
return ([pruefe_windows(), pruefe_webview(), pruefe_laufwerk()]
|
||||
+ pruefe_werkzeuge()
|
||||
+ [pruefe_platz(ziel), pruefe_schreibrecht(ziel), pruefe_port(port)])
|
||||
|
||||
|
||||
def darf_installieren(pruefungen: list) -> bool:
|
||||
"""Blockiert NUR bei einem echten Fehler. (reine Funktion)
|
||||
|
||||
Eine Warnung, die den Knopf sperrt, ist eine Bevormundung; ein Fehler,
|
||||
der nur warnt, ist eine Falle.
|
||||
"""
|
||||
return not any(p["zustand"] == FEHLER for p in pruefungen)
|
||||
|
||||
|
||||
def zusammenfassung(pruefungen: list) -> str:
|
||||
"""Ein Satz über den Gesamtzustand. (reine Funktion)"""
|
||||
fehler = [p for p in pruefungen if p["zustand"] == FEHLER]
|
||||
warnungen = [p for p in pruefungen if p["zustand"] == WARNUNG]
|
||||
if fehler:
|
||||
return "Installation nicht möglich: " + ", ".join(p["name"] for p in fehler)
|
||||
if warnungen:
|
||||
return "Bereit — mit Hinweisen zu: " + ", ".join(p["name"] for p in warnungen)
|
||||
return "Alles bereit."
|
||||
|
||||
|
||||
def vorgaben() -> dict:
|
||||
"""Womit die Maske vorbelegt wird."""
|
||||
from rippy import betrieb
|
||||
from rippy.platform import win_registry as reg
|
||||
|
||||
return {
|
||||
"ziel": reg.standard_installationsordner(),
|
||||
"ablage": betrieb.ablage_vorgabe({}, container=False, system="windows"),
|
||||
"port": STANDARD_PORT,
|
||||
"autostart": True,
|
||||
"verknuepfungen": True,
|
||||
"werkzeuge_holen": True,
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
"""Das Fenster des Einrichtungs-Assistenten.
|
||||
|
||||
## Warum es das gibt
|
||||
|
||||
Commander am 28.08.2026:
|
||||
|
||||
> „Dann hätte ich beim ‚Setup' auch erwartet das nen echtes setup passiert —
|
||||
> wo will ich das hinspeichern, nen pre requirement check usw. Da kommt
|
||||
> garnichts Rippy geht einfach auf."
|
||||
|
||||
## Warum WebView2 und nicht ein Windows-Dialog
|
||||
|
||||
Weil es schon da ist. Rippy bringt WebView2 ohnehin für sein Hauptfenster mit
|
||||
(Entscheid 4 in `KONZEPT-V2.md` § 10) — der Assistent kostet damit **null
|
||||
zusätzliche Bytes**. Ein Dialog aus Win32-Bausteinen wäre hunderte Zeilen
|
||||
ctypes für etwas, das hier fünfzig Zeilen HTML sind; `tkinter` ist im Bau
|
||||
ausdrücklich ausgeschlossen.
|
||||
|
||||
## Warum die Prüfungen NICHT hier stehen
|
||||
|
||||
Sie stehen in `einrichtung.py`, und zwar vollständig ohne Fenster prüfbar.
|
||||
Was hier steht, lässt sich nicht testen — also steht hier so wenig wie
|
||||
möglich: Aufbau, Anzeige, und die Brücke zwischen beidem.
|
||||
|
||||
## Was passiert, wenn das Fenster nicht aufgeht
|
||||
|
||||
Dann wird ohne Assistent installiert, mit den Vorgaben, und gesagt warum. Ein
|
||||
Setup, das gar nichts tut, weil sein Fenster nicht aufging, wäre schlimmer
|
||||
als eines, das nicht fragt.
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
from rippy import einrichtung
|
||||
|
||||
TITEL = "Rippy einrichten"
|
||||
BREITE = 860
|
||||
HOEHE = 760
|
||||
|
||||
|
||||
class Bruecke:
|
||||
"""Was die Seite aufrufen darf. Bewusst klein und ohne Zustand im HTML.
|
||||
|
||||
Jede Methode gibt Daten zurück, keine Erfolgsmeldungen — dieselbe Regel
|
||||
wie überall sonst in diesem Projekt.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.ergebnis = None
|
||||
self._fenster = None
|
||||
self._fortschritt = []
|
||||
|
||||
# ── Auskunft ────────────────────────────────────────────────────────
|
||||
def vorgaben(self) -> dict:
|
||||
return einrichtung.vorgaben()
|
||||
|
||||
def pruefen(self, ziel: str = "", port: int = 0) -> dict:
|
||||
"""Der Voraussetzungs-Bericht für die angegebenen Werte."""
|
||||
vorgabe = einrichtung.vorgaben()
|
||||
befunde = einrichtung.alle_pruefungen(ziel or vorgabe["ziel"],
|
||||
int(port or vorgabe["port"]))
|
||||
return {
|
||||
"befunde": befunde,
|
||||
"darf": einrichtung.darf_installieren(befunde),
|
||||
"text": einrichtung.zusammenfassung(befunde),
|
||||
}
|
||||
|
||||
def ordner_waehlen(self, vorschlag: str = "") -> str:
|
||||
"""Ein echter Ordner-Dialog. Leer, wenn abgebrochen wurde."""
|
||||
import webview
|
||||
|
||||
try:
|
||||
gewaehlt = self._fenster.create_file_dialog(
|
||||
webview.FOLDER_DIALOG, directory=vorschlag or "")
|
||||
except Exception: # noqa: BLE001
|
||||
return ""
|
||||
if not gewaehlt:
|
||||
return ""
|
||||
return gewaehlt[0] if isinstance(gewaehlt, (list, tuple)) else str(gewaehlt)
|
||||
|
||||
def fortschritt(self) -> list:
|
||||
"""Was seit dem letzten Abruf passiert ist. Leert sich dabei."""
|
||||
zeilen, self._fortschritt = self._fortschritt, []
|
||||
return zeilen
|
||||
|
||||
# ── Handeln ─────────────────────────────────────────────────────────
|
||||
def installieren(self, optionen: dict) -> dict:
|
||||
"""Installiert im Hintergrund-Thread und gibt sofort zurück.
|
||||
|
||||
Sofort, weil das Holen von MakeMKV Minuten dauern kann: Ein blockierter
|
||||
Aufruf ließe das Fenster einfrieren, und ein eingefrorenes Fenster
|
||||
sieht aus wie ein Absturz.
|
||||
"""
|
||||
threading.Thread(target=self._installieren_wirklich,
|
||||
args=(optionen or {},), daemon=True).start()
|
||||
return {"gestartet": True}
|
||||
|
||||
def _installieren_wirklich(self, optionen: dict) -> None:
|
||||
from rippy import windows_app
|
||||
|
||||
def melden(text, anteil=None):
|
||||
self._fortschritt.append(str(text))
|
||||
|
||||
try:
|
||||
melden("Dateien werden abgelegt …")
|
||||
ziel = windows_app.installieren(
|
||||
ziel=optionen.get("ziel") or None,
|
||||
autostart=bool(optionen.get("autostart", True)),
|
||||
starten_danach=False,
|
||||
verknuepfen=bool(optionen.get("verknuepfungen", True)),
|
||||
)
|
||||
if optionen.get("ablage"):
|
||||
melden("Ablage wird gespeichert …")
|
||||
_ablage_speichern(optionen["ablage"])
|
||||
if optionen.get("werkzeuge_holen", True):
|
||||
melden("Werkzeuge werden geprüft …")
|
||||
from rippy.tools import einrichten as werkzeug_einrichtung
|
||||
|
||||
bericht = werkzeug_einrichtung.sicherstellen(fortschritt=melden)
|
||||
melden(werkzeug_einrichtung.bericht(bericht))
|
||||
bereit = bericht["bereit"]
|
||||
else:
|
||||
bereit = True
|
||||
self.ergebnis = {"ok": True, "ziel": ziel, "bereit": bereit}
|
||||
melden("Fertig.")
|
||||
except Exception as ex: # noqa: BLE001
|
||||
# Nicht verschlucken: Ohne Konsole waere ein Absturz hier voellig
|
||||
# unsichtbar, und der Nutzer saehe ein Fenster, in dem nichts mehr
|
||||
# passiert.
|
||||
self.ergebnis = {"ok": False,
|
||||
"fehler": "%s: %s" % (type(ex).__name__, ex)}
|
||||
melden("Fehlgeschlagen: %s" % ex)
|
||||
|
||||
def stand(self) -> dict:
|
||||
"""None, solange es läuft. Danach das Ergebnis."""
|
||||
return self.ergebnis or {}
|
||||
|
||||
def schliessen(self) -> None:
|
||||
try:
|
||||
self._fenster.destroy()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
|
||||
def _ablage_speichern(pfad: str) -> None:
|
||||
"""Die gewählte Ablage in die Einstellungen schreiben.
|
||||
|
||||
Über die laufende Datenbank, nicht über eine Konfigurationsdatei: Das UI
|
||||
liest `outputDir` aus den Einstellungen, und genau dort muss der Wert
|
||||
landen, damit die Wahl im Assistenten überhaupt etwas bewirkt.
|
||||
"""
|
||||
from rippy import config, daemon, store
|
||||
|
||||
werte = config.laden()
|
||||
store.verbinden(config.datenbank_url(werte))
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, daemon._api_pfad())
|
||||
import db as api_db # noqa: E402
|
||||
|
||||
api_db.init_db()
|
||||
aktuell = api_db.get_settings() or {}
|
||||
aktuell["outputDir"] = pfad
|
||||
api_db.save_settings(aktuell)
|
||||
|
||||
|
||||
def starten() -> dict:
|
||||
"""Öffnet den Assistenten. Gibt zurück, was daraus geworden ist.
|
||||
|
||||
`{}` heißt: abgebrochen oder nicht möglich. Der Aufrufer entscheidet dann,
|
||||
ob er ohne Assistent installiert.
|
||||
"""
|
||||
import webview
|
||||
|
||||
bruecke = Bruecke()
|
||||
fenster = webview.create_window(TITEL, html=SEITE, js_api=bruecke,
|
||||
width=BREITE, height=HOEHE,
|
||||
min_size=(720, 560), text_select=True)
|
||||
bruecke._fenster = fenster
|
||||
webview.start(gui="edgechromium", private_mode=False)
|
||||
return bruecke.ergebnis or {}
|
||||
|
||||
|
||||
def moeglich() -> tuple:
|
||||
"""Kann der Assistent überhaupt aufgehen? (ja/nein, Begründung)"""
|
||||
from rippy import fenster as fenster_modul
|
||||
|
||||
return fenster_modul.verfuegbar()
|
||||
|
||||
|
||||
# ── Die Seite ───────────────────────────────────────────────────────────
|
||||
# Bewusst EINE Datei ohne Abhängigkeiten: kein Vite-Bau, kein Nachladen, keine
|
||||
# Schriftart aus dem Netz. Der Assistent muss auch auf einem Rechner ohne
|
||||
# Internet aufgehen — dort wird er am dringendsten gebraucht.
|
||||
SEITE = r"""<!DOCTYPE html>
|
||||
<html lang="de"><head><meta charset="utf-8">
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; background:#0b0f19; color:#e2e8f0;
|
||||
font:14px/1.55 "Segoe UI",system-ui,sans-serif; }
|
||||
header { padding:22px 28px 16px; border-bottom:1px solid #1e293b; }
|
||||
h1 { margin:0; font-size:19px; font-weight:650; }
|
||||
h1 span { color:#f59e0b; }
|
||||
.unter { color:#94a3b8; font-size:12.5px; margin-top:4px; }
|
||||
main { padding:20px 28px 96px; }
|
||||
section { margin-bottom:22px; }
|
||||
h2 { font-size:12px; text-transform:uppercase; letter-spacing:.09em;
|
||||
color:#94a3b8; margin:0 0 10px; font-weight:600; }
|
||||
.zeile { display:flex; gap:10px; padding:7px 0;
|
||||
border-bottom:1px solid #131c2e; align-items:flex-start; }
|
||||
.zeile:last-child { border-bottom:0; }
|
||||
.marke { width:17px; flex:0 0 17px; font-weight:700; text-align:center; }
|
||||
.ok .marke { color:#34d399; } .warnung .marke { color:#fbbf24; }
|
||||
.fehler .marke { color:#f87171; }
|
||||
.inhalt { flex:1; min-width:0; }
|
||||
.name { font-weight:600; }
|
||||
.wert { color:#cbd5e1; font-family:ui-monospace,Consolas,monospace;
|
||||
font-size:12.5px; word-break:break-all; }
|
||||
.abhilfe { color:#94a3b8; font-size:12px; margin-top:2px; }
|
||||
label.feld { display:block; margin-bottom:14px; }
|
||||
label.feld > span { display:block; font-size:12.5px; color:#cbd5e1;
|
||||
margin-bottom:5px; }
|
||||
.mitknopf { display:flex; gap:8px; }
|
||||
input[type=text], input[type=number] {
|
||||
flex:1; background:#0f1626; border:1px solid #24324a; border-radius:7px;
|
||||
color:#e2e8f0; padding:9px 11px; font-size:13px; font-family:inherit; }
|
||||
input:focus { outline:2px solid #f59e0b55; border-color:#f59e0b; }
|
||||
button { background:#1c2740; border:1px solid #2e3d5c; color:#e2e8f0;
|
||||
border-radius:7px; padding:9px 15px; font-size:13px; cursor:pointer;
|
||||
font-family:inherit; }
|
||||
button:hover:not(:disabled) { background:#25324f; }
|
||||
button.los { background:#f59e0b; border-color:#f59e0b; color:#1a1206;
|
||||
font-weight:650; padding:11px 26px; }
|
||||
button.los:hover:not(:disabled) { background:#fbbf24; }
|
||||
button:disabled { opacity:.45; cursor:not-allowed; }
|
||||
.schalter { display:flex; gap:9px; align-items:flex-start; margin-bottom:11px;
|
||||
cursor:pointer; }
|
||||
.schalter input { margin:3px 0 0; accent-color:#f59e0b; }
|
||||
.schalter .hinweis { color:#94a3b8; font-size:12px; }
|
||||
footer { position:fixed; left:0; right:0; bottom:0; background:#0b0f19f2;
|
||||
border-top:1px solid #1e293b; padding:14px 28px;
|
||||
display:flex; align-items:center; gap:16px;
|
||||
backdrop-filter:blur(6px); }
|
||||
#urteil { flex:1; font-size:12.5px; color:#94a3b8; }
|
||||
#protokoll { background:#080c15; border:1px solid #1a2438; border-radius:8px;
|
||||
padding:11px 13px; font-family:ui-monospace,Consolas,monospace;
|
||||
font-size:12px; color:#94a3b8; max-height:190px; overflow:auto;
|
||||
white-space:pre-wrap; }
|
||||
.weg { display:none; }
|
||||
</style></head><body>
|
||||
<header>
|
||||
<h1><span>◉</span> Rippy einrichten</h1>
|
||||
<div class="unter">Wird geprüft, was dein Rechner mitbringt — danach entscheidest du, wohin.</div>
|
||||
</header>
|
||||
<main>
|
||||
<section id="teil-pruefung">
|
||||
<h2>Voraussetzungen</h2>
|
||||
<div id="pruefungen"><div class="zeile"><div class="inhalt wert">wird geprüft …</div></div></div>
|
||||
</section>
|
||||
|
||||
<section id="teil-orte">
|
||||
<h2>Wohin</h2>
|
||||
<label class="feld"><span>Rippy selbst (Programmordner)</span>
|
||||
<div class="mitknopf">
|
||||
<input type="text" id="ziel">
|
||||
<button onclick="waehlen('ziel')">Durchsuchen …</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="feld"><span>Fertige Filme (Ablage) — darf auch ein Netzwerkpfad sein</span>
|
||||
<div class="mitknopf">
|
||||
<input type="text" id="ablage">
|
||||
<button onclick="waehlen('ablage')">Durchsuchen …</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="feld" style="max-width:190px"><span>Port</span>
|
||||
<input type="number" id="port" min="1024" max="65535">
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section id="teil-optionen">
|
||||
<h2>Sonst noch</h2>
|
||||
<label class="schalter"><input type="checkbox" id="autostart" checked>
|
||||
<span>Mit Windows starten<div class="hinweis">Rippy läuft dann im Hintergrund und erkennt eingelegte Discs von allein.</div></span></label>
|
||||
<label class="schalter"><input type="checkbox" id="verknuepfungen" checked>
|
||||
<span>Symbol auf dem Desktop und im Startmenü<div class="hinweis">Ohne das ist Rippy praktisch unauffindbar.</div></span></label>
|
||||
<label class="schalter"><input type="checkbox" id="werkzeuge_holen" checked>
|
||||
<span>Fehlende Werkzeuge holen<div class="hinweis">HandBrake liegt bei. MakeMKV wird von makemkv.com geladen und mit dem Installer des Herstellers installiert — ohne MakeMKV kann Rippy keine Disc lesen.</div></span></label>
|
||||
</section>
|
||||
|
||||
<section id="teil-lauf" class="weg">
|
||||
<h2>Läuft</h2>
|
||||
<div id="protokoll"></div>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<div id="urteil">wird geprüft …</div>
|
||||
<button id="knopf-abbruch" onclick="pywebview.api.schliessen()">Abbrechen</button>
|
||||
<button id="knopf-los" class="los" onclick="losgehen()" disabled>Installieren</button>
|
||||
</footer>
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
let laeuft = false;
|
||||
|
||||
function zeichnePruefungen(bericht) {
|
||||
const marken = { ok: '✓', warnung: '!', fehler: '✗' };
|
||||
$('pruefungen').innerHTML = bericht.befunde.map(b =>
|
||||
'<div class="zeile ' + b.zustand + '">'
|
||||
+ '<div class="marke">' + (marken[b.zustand] || '?') + '</div>'
|
||||
+ '<div class="inhalt"><div class="name">' + b.name + '</div>'
|
||||
+ '<div class="wert">' + b.text + '</div>'
|
||||
+ (b.abhilfe ? '<div class="abhilfe">' + b.abhilfe + '</div>' : '')
|
||||
+ '</div></div>').join('');
|
||||
$('urteil').textContent = bericht.text;
|
||||
$('knopf-los').disabled = !bericht.darf || laeuft;
|
||||
}
|
||||
|
||||
async function pruefen() {
|
||||
if (laeuft) return;
|
||||
zeichnePruefungen(await pywebview.api.pruefen($('ziel').value, Number($('port').value)));
|
||||
}
|
||||
|
||||
async function waehlen(feld) {
|
||||
const ordner = await pywebview.api.ordner_waehlen($(feld).value);
|
||||
if (ordner) { $(feld).value = ordner; if (feld === 'ziel') pruefen(); }
|
||||
}
|
||||
|
||||
async function losgehen() {
|
||||
laeuft = true;
|
||||
$('knopf-los').disabled = true;
|
||||
$('knopf-los').textContent = 'Läuft …';
|
||||
$('teil-lauf').classList.remove('weg');
|
||||
$('urteil').textContent = 'Rippy wird eingerichtet …';
|
||||
await pywebview.api.installieren({
|
||||
ziel: $('ziel').value,
|
||||
ablage: $('ablage').value,
|
||||
port: Number($('port').value),
|
||||
autostart: $('autostart').checked,
|
||||
verknuepfungen: $('verknuepfungen').checked,
|
||||
werkzeuge_holen: $('werkzeuge_holen').checked,
|
||||
});
|
||||
verfolgen();
|
||||
}
|
||||
|
||||
async function verfolgen() {
|
||||
const neue = await pywebview.api.fortschritt();
|
||||
if (neue.length) {
|
||||
const p = $('protokoll');
|
||||
p.textContent += neue.join('\n') + '\n';
|
||||
p.scrollTop = p.scrollHeight;
|
||||
}
|
||||
const stand = await pywebview.api.stand();
|
||||
if (stand && stand.ok !== undefined) {
|
||||
$('knopf-abbruch').classList.add('weg');
|
||||
$('knopf-los').disabled = false;
|
||||
$('knopf-los').textContent = 'Rippy öffnen';
|
||||
$('knopf-los').onclick = () => pywebview.api.schliessen();
|
||||
$('urteil').textContent = stand.ok
|
||||
? (stand.bereit ? 'Fertig — Rippy ist einsatzbereit.'
|
||||
: 'Installiert, aber es fehlt noch ein Werkzeug (siehe oben).')
|
||||
: ('Fehlgeschlagen: ' + stand.fehler);
|
||||
return;
|
||||
}
|
||||
setTimeout(verfolgen, 700);
|
||||
}
|
||||
|
||||
window.addEventListener('pywebviewready', async () => {
|
||||
const v = await pywebview.api.vorgaben();
|
||||
$('ziel').value = v.ziel;
|
||||
$('ablage').value = v.ablage;
|
||||
$('port').value = v.port;
|
||||
$('autostart').checked = v.autostart;
|
||||
$('verknuepfungen').checked = v.verknuepfungen;
|
||||
$('werkzeuge_holen').checked = v.werkzeuge_holen;
|
||||
pruefen();
|
||||
$('port').addEventListener('change', pruefen);
|
||||
$('ziel').addEventListener('change', pruefen);
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
# Damit die Seite auch ohne laufendes Fenster prüfbar ist (siehe Tests).
|
||||
SEITE_LAENGE = len(SEITE)
|
||||
|
||||
|
||||
def seite_enthaelt(text: str) -> bool:
|
||||
"""Kleine Hilfe für Tests: Steht etwas in der Seite?"""
|
||||
return text in SEITE
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Der Einrichtungs-Assistent — die Pruefungen, ohne etwas zu installieren.
|
||||
|
||||
## Warum es diese Tests gibt
|
||||
|
||||
Commander am 28.08.2026: *„Dann haette ich beim ‚Setup' auch erwartet das nen
|
||||
echtes setup passiert — wo will ich das hinspeichern, nen pre requirement
|
||||
check usw. Da kommt garnichts Rippy geht einfach auf."*
|
||||
|
||||
Er hatte recht: Ein Doppelklick installierte stillschweigend und oeffnete das
|
||||
Fenster. Der Nutzer wurde nichts gefragt und erfuhr nichts.
|
||||
|
||||
Alle Pruefungen sind eingespritzt — kein Fenster, keine Installation, kein
|
||||
WebView2. Sonst waere „kein Laufwerk" nur auf einem Rechner ohne Laufwerk
|
||||
pruefbar, und genau dort sieht es niemand.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from rippy import einrichtung as e
|
||||
|
||||
|
||||
def _p(zustand, name="X"):
|
||||
return {"name": name, "zustand": zustand, "text": "", "abhilfe": ""}
|
||||
|
||||
|
||||
# ── Was blockiert und was nicht ─────────────────────────────────────────
|
||||
def test_ohne_fehler_darf_installiert_werden():
|
||||
assert e.darf_installieren([_p(e.OK), _p(e.WARNUNG)]) is True
|
||||
|
||||
|
||||
def test_ein_fehler_blockiert():
|
||||
assert e.darf_installieren([_p(e.OK), _p(e.FEHLER)]) is False
|
||||
|
||||
|
||||
def test_eine_warnung_blockiert_NICHT():
|
||||
"""Eine Warnung, die den Knopf sperrt, ist eine Bevormundung. Rippy ohne
|
||||
optisches Laufwerk ist ein vorgesehener Betriebsfall — eine reine
|
||||
Komprimier-Maschine."""
|
||||
assert e.darf_installieren([_p(e.WARNUNG), _p(e.WARNUNG)]) is True
|
||||
|
||||
|
||||
# ── Die einzelnen Pruefungen ────────────────────────────────────────────
|
||||
def test_windows_10_und_11_gehen():
|
||||
assert e.pruefe_windows(22631)["zustand"] == e.OK
|
||||
assert e.pruefe_windows(19045)["zustand"] == e.OK
|
||||
|
||||
|
||||
def test_zu_altes_windows_ist_ein_fehler():
|
||||
befund = e.pruefe_windows(7601) # Windows 7
|
||||
assert befund["zustand"] == e.FEHLER
|
||||
assert befund["abhilfe"], "ein Befund ohne Abhilfe laesst den Nutzer ratlos"
|
||||
|
||||
|
||||
def test_fehlendes_webview_ist_nur_eine_warnung():
|
||||
"""Ohne WebView2 faellt Rippy auf den Browser zurueck. Das ist weniger
|
||||
schoen, aber es LAEUFT — ein Fehler waere hier eine Luege."""
|
||||
befund = e.pruefe_webview(version="")
|
||||
assert befund["zustand"] == e.WARNUNG
|
||||
assert "Browser" in befund["abhilfe"]
|
||||
|
||||
|
||||
def test_vorhandenes_webview_ist_ok():
|
||||
assert e.pruefe_webview(version="151.0.4129.107")["zustand"] == e.OK
|
||||
|
||||
|
||||
def test_zu_wenig_platz_ist_ein_fehler():
|
||||
befund = e.pruefe_platz("C:\\egal", frei_mb=100)
|
||||
assert befund["zustand"] == e.FEHLER
|
||||
assert "500" in befund["text"]
|
||||
|
||||
|
||||
def test_genug_platz_ist_ok():
|
||||
assert e.pruefe_platz("C:\\egal", frei_mb=5000)["zustand"] == e.OK
|
||||
|
||||
|
||||
def test_fehlendes_schreibrecht_ist_ein_fehler():
|
||||
"""`os.access(..., W_OK)` luegt unter Windows — es beantwortet die Frage
|
||||
anhand des Nur-Lesen-Flags, nicht anhand der Zugriffsrechte. Deshalb wird
|
||||
wirklich geschrieben."""
|
||||
befund = e.pruefe_schreibrecht("C:\\Programme\\Rippy",
|
||||
probe=(False, "Zugriff verweigert"))
|
||||
assert befund["zustand"] == e.FEHLER
|
||||
assert "Benutzer" in befund["abhilfe"]
|
||||
|
||||
|
||||
def test_schreibprobe_am_echten_ordner(tmp_path):
|
||||
befund = e.pruefe_schreibrecht(str(tmp_path))
|
||||
assert befund["zustand"] == e.OK
|
||||
# Und die Probe darf nichts hinterlassen.
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
def test_belegter_port_ist_nur_eine_warnung():
|
||||
"""Sehr oft ist es ein bereits laufendes Rippy — und dann ist eine
|
||||
Neuinstallation genau richtig."""
|
||||
befund = e.pruefe_port(7788, belegt=True)
|
||||
assert befund["zustand"] == e.WARNUNG
|
||||
assert "Rippy" in befund["abhilfe"]
|
||||
|
||||
|
||||
def test_freier_port_ist_ok():
|
||||
assert e.pruefe_port(7788, belegt=False)["zustand"] == e.OK
|
||||
|
||||
|
||||
def test_kein_laufwerk_ist_nur_eine_warnung():
|
||||
"""install.sh sagt seit Langem: ohne Laufwerk „laeuft trotzdem durch, dann
|
||||
ist das eine reine KOMPRIMIER-Maschine"."""
|
||||
befund = e.pruefe_laufwerk(laufwerke=[])
|
||||
assert befund["zustand"] == e.WARNUNG
|
||||
assert "komprimieren" in befund["abhilfe"]
|
||||
|
||||
|
||||
def test_gefundenes_laufwerk_wird_genannt():
|
||||
befund = e.pruefe_laufwerk(laufwerke=["\\\\.\\G:"])
|
||||
assert befund["zustand"] == e.OK
|
||||
assert "G:" in befund["text"]
|
||||
|
||||
|
||||
# ── Die Werkzeuge ───────────────────────────────────────────────────────
|
||||
def _bestand(makemkv, handbrake):
|
||||
return {
|
||||
"makemkv": {"titel": "MakeMKV", "da": makemkv, "version": "1.18.4",
|
||||
"pflicht": True},
|
||||
"handbrake": {"titel": "HandBrakeCLI", "da": handbrake,
|
||||
"version": "1.11.2", "pflicht": False},
|
||||
}
|
||||
|
||||
|
||||
def test_fehlendes_makemkv_ist_eine_warnung_mit_ausweg():
|
||||
befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(False, True))}
|
||||
assert befunde["MakeMKV"]["zustand"] == e.WARNUNG
|
||||
assert "makemkv.com" in befunde["MakeMKV"]["abhilfe"]
|
||||
|
||||
|
||||
def test_fehlendes_handbrake_ist_kein_problem():
|
||||
"""Es liegt im Paket — das Setup legt es beim Installieren daneben."""
|
||||
befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(True, False))}
|
||||
assert befunde["HandBrakeCLI"]["zustand"] == e.OK
|
||||
assert "mitgeliefert" in befunde["HandBrakeCLI"]["text"]
|
||||
|
||||
|
||||
def test_vorhandene_werkzeuge_zeigen_ihre_version():
|
||||
befunde = {b["name"]: b for b in e.pruefe_werkzeuge(_bestand(True, True))}
|
||||
assert befunde["MakeMKV"]["text"] == "1.18.4"
|
||||
|
||||
|
||||
# ── Die Zusammenfassung ─────────────────────────────────────────────────
|
||||
def test_zusammenfassung_nennt_die_fehler_beim_namen():
|
||||
text = e.zusammenfassung([_p(e.FEHLER, "Speicherplatz"), _p(e.OK, "Port")])
|
||||
assert "nicht möglich" in text
|
||||
assert "Speicherplatz" in text
|
||||
|
||||
|
||||
def test_zusammenfassung_nennt_die_hinweise():
|
||||
text = e.zusammenfassung([_p(e.WARNUNG, "MakeMKV"), _p(e.OK, "Port")])
|
||||
assert "MakeMKV" in text
|
||||
assert "nicht möglich" not in text
|
||||
|
||||
|
||||
def test_alles_gruen_sagt_es_auch_so():
|
||||
assert e.zusammenfassung([_p(e.OK), _p(e.OK)]) == "Alles bereit."
|
||||
|
||||
|
||||
def test_jede_pruefung_hat_dieselbe_form():
|
||||
"""Ein fehlendes Feld waere in der Oberflaeche ein `undefined`."""
|
||||
for befund in ([e.pruefe_windows(22631), e.pruefe_webview(version=""),
|
||||
e.pruefe_laufwerk(laufwerke=[]),
|
||||
e.pruefe_platz("C:\\x", frei_mb=9999),
|
||||
e.pruefe_port(7788, belegt=False)]
|
||||
+ e.pruefe_werkzeuge(_bestand(False, False))):
|
||||
assert set(befund) == {"name", "zustand", "text", "abhilfe"}, befund
|
||||
assert befund["zustand"] in (e.OK, e.WARNUNG, e.FEHLER)
|
||||
if befund["zustand"] != e.OK:
|
||||
assert befund["abhilfe"], (
|
||||
"%s sagt nicht, was zu tun ist" % befund["name"])
|
||||
|
||||
|
||||
# ── Die Vorgaben ────────────────────────────────────────────────────────
|
||||
def test_vorgaben_sind_vollstaendig():
|
||||
if os.name != "nt":
|
||||
import pytest
|
||||
pytest.skip("Registry-Vorgaben gibt es nur unter Windows")
|
||||
v = e.vorgaben()
|
||||
assert set(v) == {"ziel", "ablage", "port", "autostart",
|
||||
"verknuepfungen", "werkzeuge_holen"}
|
||||
assert v["port"] == 7788
|
||||
assert v["ziel"] and v["ablage"]
|
||||
assert "/app/" not in v["ablage"], "Container-Pfad in einer Windows-Vorgabe"
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Der Einrichtungs-Assistent: Bruecke und Seite, ohne ein Fenster zu oeffnen.
|
||||
|
||||
Ein Fenster laesst sich im Test nicht bedienen. Geprueft wird deshalb das,
|
||||
was DAHINTER haengt: was die Seite abfragen darf, was beim Installieren
|
||||
passiert, und dass ein Fehlschlag sichtbar wird statt zu verschwinden.
|
||||
|
||||
Hinweis zu den Backslashes: Windows-Pfade stehen hier in ROHEN Zeichenketten
|
||||
(`r"..."`). In einer normalen waere `C:\\Rippy` die ungueltige Escape-Sequenz
|
||||
Backslash-R.
|
||||
"""
|
||||
|
||||
from rippy import setup_fenster
|
||||
|
||||
|
||||
class LeeresFenster:
|
||||
def __init__(self, antwort=None):
|
||||
self.antwort = antwort
|
||||
self.zerstoert = False
|
||||
|
||||
def create_file_dialog(self, *a, **k):
|
||||
if self.antwort is None:
|
||||
raise RuntimeError("kein Dialog")
|
||||
return self.antwort
|
||||
|
||||
def destroy(self):
|
||||
self.zerstoert = True
|
||||
|
||||
|
||||
# ── Die Seite ───────────────────────────────────────────────────────────
|
||||
def test_die_seite_fragt_nach_dem_ablageort():
|
||||
"""Die ausdrueckliche Frage des Commanders: „wo will ich das
|
||||
hinspeichern"."""
|
||||
assert setup_fenster.seite_enthaelt('id="ablage"')
|
||||
assert setup_fenster.seite_enthaelt("Fertige Filme")
|
||||
|
||||
|
||||
def test_die_seite_zeigt_die_voraussetzungen():
|
||||
"""„nen pre requirement check usw." — vorher kam da gar nichts."""
|
||||
assert setup_fenster.seite_enthaelt("Voraussetzungen")
|
||||
assert setup_fenster.seite_enthaelt("pywebview.api.pruefen")
|
||||
|
||||
|
||||
def test_die_seite_laedt_nichts_aus_dem_netz():
|
||||
"""Der Assistent muss auf einem Rechner OHNE Internet aufgehen — dort
|
||||
wird er am dringendsten gebraucht."""
|
||||
for muster in ("http://", "https://", "<script src", "<link "):
|
||||
assert not setup_fenster.seite_enthaelt(muster), muster
|
||||
|
||||
|
||||
def test_die_seite_kann_den_ordner_waehlen_lassen():
|
||||
assert setup_fenster.seite_enthaelt("pywebview.api.ordner_waehlen")
|
||||
|
||||
|
||||
# ── Die Bruecke ─────────────────────────────────────────────────────────
|
||||
def test_abgebrochener_ordner_dialog_gibt_leer_zurueck():
|
||||
b = setup_fenster.Bruecke()
|
||||
b._fenster = LeeresFenster(antwort=None)
|
||||
assert b.ordner_waehlen("C:") == ""
|
||||
|
||||
|
||||
def test_gewaehlter_ordner_kommt_als_text():
|
||||
b = setup_fenster.Bruecke()
|
||||
b._fenster = LeeresFenster(antwort=(r"D:\Filme",))
|
||||
assert b.ordner_waehlen() == r"D:\Filme"
|
||||
|
||||
|
||||
def test_fortschritt_leert_sich_beim_abholen():
|
||||
"""Sonst waechst das Protokoll bei jedem Abruf um alles Bisherige."""
|
||||
b = setup_fenster.Bruecke()
|
||||
b._fortschritt = ["eins", "zwei"]
|
||||
assert b.fortschritt() == ["eins", "zwei"]
|
||||
assert b.fortschritt() == []
|
||||
|
||||
|
||||
def test_stand_ist_leer_solange_nichts_fertig_ist():
|
||||
assert setup_fenster.Bruecke().stand() == {}
|
||||
|
||||
|
||||
def test_ein_fehlschlag_beim_installieren_wird_SICHTBAR(monkeypatch):
|
||||
"""Ohne Konsole waere ein Absturz hier voellig unsichtbar — der Nutzer
|
||||
saehe ein Fenster, in dem nichts mehr passiert."""
|
||||
from rippy import windows_app
|
||||
|
||||
def platzt(**k):
|
||||
raise OSError("Zugriff verweigert")
|
||||
|
||||
monkeypatch.setattr(windows_app, "installieren", platzt)
|
||||
b = setup_fenster.Bruecke()
|
||||
b._installieren_wirklich({"ziel": r"C:\x", "werkzeuge_holen": False})
|
||||
|
||||
assert b.ergebnis["ok"] is False
|
||||
assert "Zugriff verweigert" in b.ergebnis["fehler"]
|
||||
assert any("Fehlgeschlagen" in z for z in b._fortschritt)
|
||||
|
||||
|
||||
def test_erfolgreiche_installation_meldet_den_ordner(monkeypatch):
|
||||
from rippy import windows_app
|
||||
|
||||
monkeypatch.setattr(windows_app, "installieren", lambda **k: r"C:\Rippy")
|
||||
b = setup_fenster.Bruecke()
|
||||
b._installieren_wirklich({"ziel": r"C:\Rippy", "werkzeuge_holen": False})
|
||||
|
||||
assert b.ergebnis == {"ok": True, "ziel": r"C:\Rippy", "bereit": True}
|
||||
|
||||
|
||||
def test_werkzeuge_koennen_uebersprungen_werden(monkeypatch):
|
||||
"""Sonst wartete eine unbeaufsichtigte Installation auf einen Download."""
|
||||
from rippy import windows_app
|
||||
from rippy.tools import einrichten
|
||||
|
||||
def haette_nicht(**k):
|
||||
raise AssertionError("haette nicht laufen duerfen")
|
||||
|
||||
monkeypatch.setattr(windows_app, "installieren", lambda **k: r"C:\Rippy")
|
||||
monkeypatch.setattr(einrichten, "sicherstellen", haette_nicht)
|
||||
b = setup_fenster.Bruecke()
|
||||
b._installieren_wirklich({"werkzeuge_holen": False})
|
||||
assert b.ergebnis["ok"] is True
|
||||
|
||||
|
||||
def test_schliessen_ohne_fenster_wirft_nicht():
|
||||
setup_fenster.Bruecke().schliessen()
|
||||
@@ -121,10 +121,37 @@ def ordner_von(datei: str) -> str:
|
||||
|
||||
|
||||
def _entfalten(pfad: str, umgebung: dict) -> str:
|
||||
"""%ProgramFiles%\\X -> C:\\Program Files\\X (mit eigener Umgebung testbar)."""
|
||||
r"""%ProgramFiles%\X -> C:\Program Files\X (mit eigener Umgebung testbar).
|
||||
|
||||
## Warum das GROSS- UND KLEINSCHREIBUNG ignorieren muss (28.08.2026)
|
||||
|
||||
Windows behandelt Umgebungsvariablen ohne Rücksicht auf die Schreibweise,
|
||||
und Python speichert sie unter Windows deshalb GROSS in `os.environ`.
|
||||
Gemessen auf dem Rechner des Commanders:
|
||||
|
||||
os.environ PROGRAMFILES(X86) = C:\Program Files (x86)
|
||||
WERKZEUGE["orte"] %ProgramFiles(x86)%\MakeMKV
|
||||
|
||||
Der Vergleich schlug fehl, `%` blieb stehen, und die Funktion gab "" —
|
||||
der Kandidat fiel aus der Liste. **Die bekannten Installationsorte haben
|
||||
auf einer echten Maschine also nie funktioniert.** MakeMKV wurde nur über
|
||||
die Registry-Rückfallebene gefunden; wo die fehlt oder anders aussieht,
|
||||
fand Rippy nichts.
|
||||
|
||||
Aufgefallen ist es nur, weil hier eine ECHTE Umgebung eingesetzt wurde.
|
||||
Der Test spritzte `{"ProgramFiles(x86)": …}` in genau der Schreibweise
|
||||
ein, die im Muster steht — er war damit grüner als die Wirklichkeit.
|
||||
"""
|
||||
gross = {("%" + name + "%").upper(): wert for name, wert in umgebung.items()}
|
||||
ergebnis = pfad
|
||||
for name, wert in umgebung.items():
|
||||
ergebnis = ergebnis.replace("%" + name + "%", wert)
|
||||
for marke, wert in gross.items():
|
||||
# Suchen ohne Rücksicht auf die Schreibweise, ersetzen an der
|
||||
# gefundenen Stelle — `str.replace` kann das nicht von allein.
|
||||
while True:
|
||||
stelle = ergebnis.upper().find(marke)
|
||||
if stelle < 0:
|
||||
break
|
||||
ergebnis = ergebnis[:stelle] + wert + ergebnis[stelle + len(marke):]
|
||||
return ergebnis if "%" not in ergebnis else ""
|
||||
|
||||
|
||||
|
||||
@@ -17,9 +17,17 @@ from rippy.tools import katalog
|
||||
BACKSLASH = chr(92)
|
||||
|
||||
# Eine Umgebung ohne echte Systemvariablen, damit die Pfade vorhersagbar sind.
|
||||
#
|
||||
# ⚠️ Die Schluessel stehen GROSS — genau so, wie Windows sie in `os.environ`
|
||||
# ablegt (am 28.08.2026 nachgesehen: `PROGRAMFILES(X86)`). Vorher stand hier
|
||||
# die huebsche Schreibweise `ProgramFiles(x86)`, und damit war dieser Test
|
||||
# gruener als die Wirklichkeit: Die Entfaltung verglich Zeichen fuer Zeichen,
|
||||
# fand auf einer echten Maschine nie eine Uebereinstimmung und lieferte "".
|
||||
# Die bekannten Installationsorte haben deshalb NIE funktioniert; MakeMKV
|
||||
# wurde nur ueber die Registry-Rueckfallebene gefunden.
|
||||
UMGEBUNG = {
|
||||
"ProgramFiles": r"C:\Program Files",
|
||||
"ProgramFiles(x86)": r"C:\Program Files (x86)",
|
||||
"PROGRAMFILES": r"C:\Program Files",
|
||||
"PROGRAMFILES(X86)": r"C:\Program Files (x86)",
|
||||
}
|
||||
|
||||
|
||||
@@ -222,3 +230,30 @@ def test_programmdatei_aus_beiden_formen():
|
||||
assert p(r"C:\R\x.exe") == r"C:\R\x.exe"
|
||||
assert p("") == ""
|
||||
assert p("irgendwas ohne Programm") == ""
|
||||
|
||||
|
||||
def test_schreibweise_der_umgebungsvariablen_ist_egal():
|
||||
"""DER Fehler vom 28.08.2026. Windows legt Umgebungsvariablen GROSS ab,
|
||||
das Muster in WERKZEUGE steht huebsch geschrieben. Ohne diese Pruefung
|
||||
faellt der Unterschied nie auf — der alte Test spritzte genau die
|
||||
Schreibweise ein, die im Muster stand."""
|
||||
for schreibweise in ("ProgramFiles(x86)", "PROGRAMFILES(X86)",
|
||||
"programfiles(x86)"):
|
||||
entfaltet = katalog._entfalten(
|
||||
r"%ProgramFiles(x86)%\MakeMKV", {schreibweise: r"C:\PF86"})
|
||||
assert entfaltet == r"C:\PF86\MakeMKV", (schreibweise, entfaltet)
|
||||
|
||||
|
||||
def test_mit_der_ECHTEN_umgebung_kommen_die_systempfade_heraus():
|
||||
"""Die Gegenprobe an os.environ statt an einer gepflegten Attrappe.
|
||||
Genau die hat gefehlt."""
|
||||
import os
|
||||
|
||||
if os.name != "nt":
|
||||
import pytest
|
||||
pytest.skip("ProgramFiles gibt es nur unter Windows")
|
||||
pfade = katalog.kandidaten("makemkv", r"C:\Rippy ools", os.environ)
|
||||
assert any("Program Files" in p for p in pfade), (
|
||||
"Kein einziger Systempfad in der Kandidatenliste — dann findet Rippy "
|
||||
"eine normale MakeMKV-Installation nur noch ueber die Registry."
|
||||
)
|
||||
|
||||
@@ -593,6 +593,7 @@ Optionen:
|
||||
--kein-autostart
|
||||
--keine-verknuepfungen ohne Desktop-Symbol und Startmenue-Eintrag
|
||||
--ohne-werkzeuge MakeMKV/HandBrake NICHT pruefen und nicht holen
|
||||
--ohne-assistent ohne Einrichtungs-Fenster, mit den Vorgaben
|
||||
--im-browser im Browser statt im eigenen Fenster
|
||||
--still ohne Rückmeldungen
|
||||
"""
|
||||
@@ -636,7 +637,27 @@ def main(argv=None) -> int:
|
||||
if "--dienst" in argv:
|
||||
return dienst_starten(int(wert("--port", STANDARD_PORT)))
|
||||
|
||||
# Ohne Argument: installieren. Das ist der Doppelklick-Fall.
|
||||
# Ohne Argument: der Doppelklick-Fall — und der gehoert in den
|
||||
# Einrichtungs-Assistenten.
|
||||
#
|
||||
# Commander am 28.08.2026: "Dann haette ich beim 'Setup' auch erwartet das
|
||||
# nen echtes setup passiert - wo will ich das hinspeichern, nen pre
|
||||
# requirement check usw. Da kommt garnichts Rippy geht einfach auf."
|
||||
#
|
||||
# `--still` und `--ohne-assistent` gehen weiterhin den stummen Weg: fuer
|
||||
# unbeaufsichtigte Installationen und fuer die eigenen Tests.
|
||||
if "--still" not in argv and "--ohne-assistent" not in argv:
|
||||
ergebnis = assistent_starten()
|
||||
if ergebnis.get("ok"):
|
||||
return 0
|
||||
if ergebnis.get("abgebrochen"):
|
||||
melden("Abgebrochen — es wurde nichts veraendert.")
|
||||
return 1
|
||||
# Weder erledigt noch abgebrochen: Der Assistent ging gar nicht auf.
|
||||
# Dann wird ohne ihn installiert, statt gar nichts zu tun.
|
||||
melden("Der Einrichtungs-Assistent ging nicht auf (%s) — es wird mit "
|
||||
"den Vorgaben installiert." % (ergebnis.get("grund") or "?"))
|
||||
|
||||
try:
|
||||
ziel = installieren(
|
||||
ziel=wert("--ziel"),
|
||||
@@ -671,6 +692,31 @@ def main(argv=None) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def assistent_starten() -> dict:
|
||||
"""Oeffnet den Einrichtungs-Assistenten. Sagt, was daraus wurde.
|
||||
|
||||
Rueckgabe:
|
||||
{"ok": True, ...} installiert
|
||||
{"abgebrochen": True} Fenster zugemacht, nichts veraendert
|
||||
{"grund": "..."} ging nicht auf — der Aufrufer entscheidet
|
||||
|
||||
Wirft nicht. Ein Setup, das gar nichts tut, weil sein Fenster nicht
|
||||
aufging, waere schlimmer als eines, das nicht fragt.
|
||||
"""
|
||||
from rippy import setup_fenster
|
||||
|
||||
geht, grund = setup_fenster.moeglich()
|
||||
if not geht:
|
||||
return {"grund": grund}
|
||||
try:
|
||||
ergebnis = setup_fenster.starten()
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"grund": "%s: %s" % (type(e).__name__, e)}
|
||||
if not ergebnis:
|
||||
return {"abgebrochen": True}
|
||||
return ergebnis
|
||||
|
||||
|
||||
def werkzeuge_einrichten() -> str:
|
||||
"""Holt fehlende Werkzeuge und gibt den Bericht als Text zurück.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user