feat(setup): Fortschrittsbalken, MakeMKV-Installer mit Rechteabfrage, Updates
Ampel / ampel (push) Successful in 55s
Ampel / ampel (push) Successful in 55s
Drei Punkte des Commanders vom 28.08.2026.
## 1. "bei der installation eine progress bar waere gut"
Die Daten dafuer waren schon da -- der Fortschritts-Rueckruf traegt seit
jeher einen Anteil, die Bruecke warf ihn nur weg.
Jetzt rechnet `Fortschritt` Abschnitt + Teil-Anteil in einen Gesamtstand um.
Die Gewichte sind gemessen: Dateien ablegen 0-8 % (rund drei Sekunden),
Ablage 8-10 %, Werkzeuge 10-97 % (sechs Sekunden bis mehrere Minuten). Ein
Balken, der bei 90 % minutenlang steht, ist schlimmer als gar keiner.
Zwei Regeln, beide mit Test:
* Er laeuft NIE zurueck. Der Werkzeug-Abschnitt kann mehrere Downloads
enthalten, jeder mit eigenem 0-bis-1 -- ohne die Regel spraenge der Balken
bei jedem neuen Werkzeug an den Abschnittsanfang. Zusaetzlich rechnet
`sicherstellen` den Anteil eines Werkzeugs auf die ganze Strecke um.
* Eine Textmeldung ohne Zahl bewegt ihn nicht. Sie ist keine Aussage ueber
den Fortschritt.
Am laufenden Fenster nachgewiesen: "Werkzeuge werden geprueft -- 36 %",
Balken bernsteinfarben, am Ende gruen bei 100 %.
## 2. "make MKV laesst sich nicht installieren weil der vorgang erhoehte
## rechte benoetigt"
Der Kern, und der Fehler lag bei uns: `subprocess.Popen` benutzt
`CreateProcess`, und das ZEIGT KEINE UAC-Abfrage -- es bricht mit
ERROR_ELEVATION_REQUIRED (740) ab. MakeMKV installiert nach Program Files und
fordert im Manifest Administratorrechte an.
`ShellExecuteW` ist der dokumentierte Weg, der die Abfrage ausloest. Damit
braucht NUR dieser eine Aufruf die Erhoehung.
Und deshalb laeuft das Setup NICHT dauerhaft erhoeht (dazu die Antwort im
Chat): Ein erhoehter Prozess sieht die eingebundenen Netzlaufwerke nicht --
auf diesem Rechner gemessen, X:, Y:, Z: ohne Erhoehung sichtbar,
EnableLinkedConnections nicht gesetzt. Genau das hatte der Commander vorher
selbst als Fehler gemeldet. Wer trotzdem erhoeht starten will, bekommt einen
Knopf -- aber nur, wenn das Schreibrecht am gewaehlten Ziel wirklich fehlt.
## 3. "wenn MakeMKV oder Handbrake schon installiert sind ein update
## verfuegbar ist. Das kann ja direkt im setup abgehandelt werden."
`veraltete()` vergleicht nach ZAHLEN (ein Zeichenvergleich hielte 1.9.2 fuer
neuer als 1.11.2 -- und HandBrake ist genau dort). `sicherstellen` holt sie
auf Wunsch mit, aber IMMER nach den fehlenden: Ein Update ist Komfort, ein
fehlendes Pflichtwerkzeug ist ein Hindernis.
Der Assistent zeigt es als eigenen Schalter mit Klartext ("MakeMKV 1.18.2 ->
1.18.4"). Der Update-Check laeuft als GETRENNTER Aufruf nach der
Voraussetzungs-Pruefung: Er kostet Netz, und makemkv.com braucht dafuer im
schlimmsten Fall gut zwanzig Sekunden -- in der Pruefung bliebe das Fenster
so lange leer. Sind die Quellen nicht erreichbar, schaltet sich der Haken ab
und sagt warum; ein Haken, der nichts tun kann, ist eine Falle.
Ampel lokal: 758 gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
95beb91364
commit
ea8a043643
@@ -133,6 +133,69 @@ def ausgabe_umleiten(pfad: str = None) -> str:
|
|||||||
return pfad
|
return pfad
|
||||||
|
|
||||||
|
|
||||||
|
def mit_rechteabfrage_starten(programm: str, argumente: str = "") -> bool:
|
||||||
|
r"""Startet ein Programm über die Shell — mit UAC-Abfrage, falls nötig.
|
||||||
|
|
||||||
|
Der Unterschied zu `subprocess.Popen` ist der ganze Punkt: `Popen` benutzt
|
||||||
|
`CreateProcess`, und das **zeigt keine UAC-Abfrage**. Verlangt das Ziel im
|
||||||
|
Manifest Administratorrechte, bricht es mit `ERROR_ELEVATION_REQUIRED`
|
||||||
|
(740) ab. Genau daran ist am 28.08.2026 der MakeMKV-Installer gescheitert.
|
||||||
|
|
||||||
|
Ohne Verb: Windows entscheidet selbst, ob eine Abfrage nötig ist. Ein
|
||||||
|
festes „runas" würde auch dort erhöhen, wo es nicht gebraucht wird.
|
||||||
|
"""
|
||||||
|
if os.name != "nt":
|
||||||
|
return False
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
SW_SHOWNORMAL = 1
|
||||||
|
try:
|
||||||
|
ergebnis = ctypes.WinDLL("shell32").ShellExecuteW(
|
||||||
|
None, None, programm, argumente or None, None, SW_SHOWNORMAL)
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
return False
|
||||||
|
# Über 32 heißt Erfolg (dokumentierter Rückgabewert von ShellExecute).
|
||||||
|
# 5 = ACCESS_DENIED: Der Nutzer hat die UAC-Abfrage weggeklickt.
|
||||||
|
return int(ergebnis) > 32
|
||||||
|
|
||||||
|
|
||||||
|
def als_admin_neu_starten(programm: str, argumente: str = "") -> bool:
|
||||||
|
r"""Startet ein Programm mit Administratorrechten neu (löst UAC aus).
|
||||||
|
|
||||||
|
True heißt: Windows hat den Start angenommen. False heißt: Der Nutzer hat
|
||||||
|
die Abfrage abgebrochen oder es ging nicht — dann läuft der aufrufende
|
||||||
|
Prozess einfach weiter, statt sich zu beenden.
|
||||||
|
|
||||||
|
## Warum das NICHT der Normalfall ist (gemessen 28.08.2026)
|
||||||
|
|
||||||
|
Ein erhöhter Prozess bekommt ein anderes Zugriffstoken, und eingebundene
|
||||||
|
Netzlaufwerke hängen am Token der SITZUNG. Auf diesem Rechner gemessen:
|
||||||
|
|
||||||
|
ohne Erhöhung X:, Y:, Z: sichtbar
|
||||||
|
EnableLinkedConnections nicht gesetzt (Windows-Standard)
|
||||||
|
|
||||||
|
Erhöht wären die drei also weg — genau der Befund, den der Commander
|
||||||
|
gemeldet hat („als Admin kann man kein netzlaufwerk wählen"). Deshalb ist
|
||||||
|
das hier ein Knopf und keine Voreinstellung: Wer nach `Programme`
|
||||||
|
installieren will, braucht ihn; wer eine Freigabe als Ablage will, darf
|
||||||
|
ihn NICHT drücken.
|
||||||
|
"""
|
||||||
|
if os.name != "nt":
|
||||||
|
return False
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
SW_SHOWNORMAL = 1
|
||||||
|
try:
|
||||||
|
# ShellExecuteW mit dem Verb „runas" ist der dokumentierte Weg. Ein
|
||||||
|
# Rückgabewert über 32 heißt Erfolg; 5 (ACCESS_DENIED) heißt: Der
|
||||||
|
# Nutzer hat die UAC-Abfrage weggeklickt.
|
||||||
|
ergebnis = ctypes.WinDLL("shell32").ShellExecuteW(
|
||||||
|
None, "runas", programm, argumente or None, None, SW_SHOWNORMAL)
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
return False
|
||||||
|
return int(ergebnis) > 32
|
||||||
|
|
||||||
|
|
||||||
def meldung_zeigen(text: str, titel: str = "Rippy", fehler: bool = False) -> bool:
|
def meldung_zeigen(text: str, titel: str = "Rippy", fehler: bool = False) -> bool:
|
||||||
"""Ein Meldungsfenster. True, wenn es wirklich gezeigt wurde.
|
"""Ein Meldungsfenster. True, wenn es wirklich gezeigt wurde.
|
||||||
|
|
||||||
|
|||||||
+205
-18
@@ -38,6 +38,66 @@ BREITE = 860
|
|||||||
HOEHE = 760
|
HOEHE = 760
|
||||||
|
|
||||||
|
|
||||||
|
# Die Abschnitte der Installation und was sie am Balken belegen.
|
||||||
|
#
|
||||||
|
# Die Gewichte sind gemessen, nicht geraten: Das Ablegen der Dateien dauert
|
||||||
|
# rund drei Sekunden (56 MB kopieren, UI und HandBrake auspacken), das Holen
|
||||||
|
# von MakeMKV je nach Quelle sechs Sekunden bis mehrere Minuten. Der
|
||||||
|
# Werkzeug-Abschnitt bekommt deshalb den Löwenanteil — ein Balken, der bei
|
||||||
|
# 90 % minutenlang steht, ist schlimmer als gar keiner.
|
||||||
|
ABSCHNITTE = {
|
||||||
|
"dateien": (0.00, 0.08, "Dateien werden abgelegt"),
|
||||||
|
"ablage": (0.08, 0.10, "Ablage wird gespeichert"),
|
||||||
|
"werkzeuge": (0.10, 0.97, "Werkzeuge werden geprüft"),
|
||||||
|
"fertig": (0.97, 1.00, "Fertig"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Fortschritt:
|
||||||
|
"""Rechnet Abschnitt + Teil-Anteil in einen Gesamtstand um.
|
||||||
|
|
||||||
|
Reine Rechnerei, damit sie ohne Fenster prüfbar ist — und weil ein
|
||||||
|
Fortschrittsbalken, der springt oder zurückläuft, schlimmer aussieht als
|
||||||
|
keiner.
|
||||||
|
|
||||||
|
Zwei Regeln:
|
||||||
|
|
||||||
|
* **Er läuft nie zurück.** Der Werkzeug-Abschnitt kann mehrere Downloads
|
||||||
|
enthalten, jeder mit eigenem 0-bis-1. Ohne diese Regel spränge der
|
||||||
|
Balken bei jedem neuen Werkzeug zurück an den Abschnittsanfang.
|
||||||
|
* **Ohne Anteil bleibt er stehen.** Eine Textmeldung ohne Zahl ist keine
|
||||||
|
Aussage über den Fortschritt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.prozent = 0
|
||||||
|
self.abschnitt = ""
|
||||||
|
self.zeilen = []
|
||||||
|
|
||||||
|
def beginne(self, name: str) -> None:
|
||||||
|
von, _bis, titel = ABSCHNITTE[name]
|
||||||
|
self.abschnitt = titel
|
||||||
|
self._von, self._bis, _ = ABSCHNITTE[name]
|
||||||
|
self.setze(von)
|
||||||
|
|
||||||
|
def melde(self, text, anteil=None) -> None:
|
||||||
|
if text:
|
||||||
|
self.zeilen.append(str(text))
|
||||||
|
if anteil is None:
|
||||||
|
return
|
||||||
|
anteil = min(1.0, max(0.0, float(anteil)))
|
||||||
|
self.setze(self._von + (self._bis - self._von) * anteil)
|
||||||
|
|
||||||
|
def setze(self, anteil: float) -> None:
|
||||||
|
self.prozent = max(self.prozent, int(round(anteil * 100)))
|
||||||
|
|
||||||
|
def abholen(self) -> dict:
|
||||||
|
"""Was seit dem letzten Abruf passiert ist. Die Zeilen leeren sich."""
|
||||||
|
zeilen, self.zeilen = self.zeilen, []
|
||||||
|
return {"zeilen": zeilen, "prozent": self.prozent,
|
||||||
|
"abschnitt": self.abschnitt}
|
||||||
|
|
||||||
|
|
||||||
class Bruecke:
|
class Bruecke:
|
||||||
"""Was die Seite aufrufen darf. Bewusst klein und ohne Zustand im HTML.
|
"""Was die Seite aufrufen darf. Bewusst klein und ohne Zustand im HTML.
|
||||||
|
|
||||||
@@ -48,7 +108,7 @@ class Bruecke:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.ergebnis = None
|
self.ergebnis = None
|
||||||
self._fenster = None
|
self._fenster = None
|
||||||
self._fortschritt = []
|
self._lauf = Fortschritt()
|
||||||
|
|
||||||
# ── Auskunft ────────────────────────────────────────────────────────
|
# ── Auskunft ────────────────────────────────────────────────────────
|
||||||
def vorgaben(self) -> dict:
|
def vorgaben(self) -> dict:
|
||||||
@@ -65,6 +125,38 @@ class Bruecke:
|
|||||||
"text": einrichtung.zusammenfassung(befunde),
|
"text": einrichtung.zusammenfassung(befunde),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def updates(self) -> dict:
|
||||||
|
"""Gibt es für die installierten Werkzeuge etwas Neueres?
|
||||||
|
|
||||||
|
## Warum das ein EIGENER Aufruf ist (28.08.2026)
|
||||||
|
|
||||||
|
Commander: *„bitte baue ein, das wenn MakeMKV oder Handbrake schon
|
||||||
|
installiert sind ein update verfügbar ist."*
|
||||||
|
|
||||||
|
Die Auskunft kostet Netz — und makemkv.com braucht dafür im
|
||||||
|
schlimmsten Fall gut zwanzig Sekunden (gemessen). In der
|
||||||
|
Voraussetzungs-Prüfung würde das Fenster so lange leer bleiben.
|
||||||
|
Deshalb getrennt: Die Prüfung zeigt sofort, was installiert ist, und
|
||||||
|
diese Auskunft trägt die Update-Hinweise nach.
|
||||||
|
|
||||||
|
Leere Werte heißen „nicht erreichbar", nicht „kein Update" — eine
|
||||||
|
Nichtauskunft ist keine Aussage.
|
||||||
|
"""
|
||||||
|
from rippy.tools import beschaffen, einrichten as werkzeuge, katalog
|
||||||
|
|
||||||
|
try:
|
||||||
|
bestand = katalog.bestand()
|
||||||
|
neueste = beschaffen.neueste_versionen()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
return {"erreichbar": False, "offen": []}
|
||||||
|
|
||||||
|
offen = [{"name": name,
|
||||||
|
"titel": eintrag.get("titel", name),
|
||||||
|
"installiert": eintrag.get("version", ""),
|
||||||
|
"neueste": neu}
|
||||||
|
for name, eintrag, neu in werkzeuge.veraltete(bestand, neueste)]
|
||||||
|
return {"erreichbar": any(neueste.values()), "offen": offen}
|
||||||
|
|
||||||
def ordner_waehlen(self, vorschlag: str = "") -> str:
|
def ordner_waehlen(self, vorschlag: str = "") -> str:
|
||||||
"""Ein echter Ordner-Dialog. Leer, wenn abgebrochen wurde."""
|
"""Ein echter Ordner-Dialog. Leer, wenn abgebrochen wurde."""
|
||||||
import webview
|
import webview
|
||||||
@@ -78,10 +170,14 @@ class Bruecke:
|
|||||||
return ""
|
return ""
|
||||||
return gewaehlt[0] if isinstance(gewaehlt, (list, tuple)) else str(gewaehlt)
|
return gewaehlt[0] if isinstance(gewaehlt, (list, tuple)) else str(gewaehlt)
|
||||||
|
|
||||||
def fortschritt(self) -> list:
|
def fortschritt(self) -> dict:
|
||||||
"""Was seit dem letzten Abruf passiert ist. Leert sich dabei."""
|
"""Was seit dem letzten Abruf passiert ist — Zeilen UND Stand.
|
||||||
zeilen, self._fortschritt = self._fortschritt, []
|
|
||||||
return zeilen
|
Gibt ein Wörterbuch zurück, keine Liste: Der Balken braucht eine Zahl,
|
||||||
|
und zwei getrennte Abrufe dafür wären zwei Gelegenheiten, dass Text
|
||||||
|
und Stand auseinanderlaufen.
|
||||||
|
"""
|
||||||
|
return self._lauf.abholen()
|
||||||
|
|
||||||
# ── Handeln ─────────────────────────────────────────────────────────
|
# ── Handeln ─────────────────────────────────────────────────────────
|
||||||
def installieren(self, optionen: dict) -> dict:
|
def installieren(self, optionen: dict) -> dict:
|
||||||
@@ -98,10 +194,10 @@ class Bruecke:
|
|||||||
def _installieren_wirklich(self, optionen: dict) -> None:
|
def _installieren_wirklich(self, optionen: dict) -> None:
|
||||||
from rippy import windows_app
|
from rippy import windows_app
|
||||||
|
|
||||||
def melden(text, anteil=None):
|
melden = self._lauf.melde
|
||||||
self._fortschritt.append(str(text))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self._lauf.beginne("dateien")
|
||||||
melden("Dateien werden abgelegt …")
|
melden("Dateien werden abgelegt …")
|
||||||
ziel = windows_app.installieren(
|
ziel = windows_app.installieren(
|
||||||
ziel=optionen.get("ziel") or None,
|
ziel=optionen.get("ziel") or None,
|
||||||
@@ -110,19 +206,24 @@ class Bruecke:
|
|||||||
verknuepfen=bool(optionen.get("verknuepfungen", True)),
|
verknuepfen=bool(optionen.get("verknuepfungen", True)),
|
||||||
)
|
)
|
||||||
if optionen.get("ablage"):
|
if optionen.get("ablage"):
|
||||||
|
self._lauf.beginne("ablage")
|
||||||
melden("Ablage wird gespeichert …")
|
melden("Ablage wird gespeichert …")
|
||||||
_ablage_speichern(optionen["ablage"])
|
_ablage_speichern(optionen["ablage"])
|
||||||
|
self._lauf.beginne("werkzeuge")
|
||||||
if optionen.get("werkzeuge_holen", True):
|
if optionen.get("werkzeuge_holen", True):
|
||||||
melden("Werkzeuge werden geprüft …")
|
melden("Werkzeuge werden geprüft …")
|
||||||
from rippy.tools import einrichten as werkzeug_einrichtung
|
from rippy.tools import einrichten as werkzeug_einrichtung
|
||||||
|
|
||||||
bericht = werkzeug_einrichtung.sicherstellen(fortschritt=melden)
|
bericht = werkzeug_einrichtung.sicherstellen(
|
||||||
|
fortschritt=melden,
|
||||||
|
aktualisieren=bool(optionen.get("aktualisieren", False)))
|
||||||
melden(werkzeug_einrichtung.bericht(bericht))
|
melden(werkzeug_einrichtung.bericht(bericht))
|
||||||
bereit = bericht["bereit"]
|
bereit = bericht["bereit"]
|
||||||
else:
|
else:
|
||||||
bereit = True
|
bereit = True
|
||||||
|
self._lauf.beginne("fertig")
|
||||||
self.ergebnis = {"ok": True, "ziel": ziel, "bereit": bereit}
|
self.ergebnis = {"ok": True, "ziel": ziel, "bereit": bereit}
|
||||||
melden("Fertig.")
|
melden("Fertig.", 1.0)
|
||||||
except Exception as ex: # noqa: BLE001
|
except Exception as ex: # noqa: BLE001
|
||||||
# Nicht verschlucken: Ohne Konsole waere ein Absturz hier voellig
|
# Nicht verschlucken: Ohne Konsole waere ein Absturz hier voellig
|
||||||
# unsichtbar, und der Nutzer saehe ein Fenster, in dem nichts mehr
|
# unsichtbar, und der Nutzer saehe ein Fenster, in dem nichts mehr
|
||||||
@@ -135,6 +236,26 @@ class Bruecke:
|
|||||||
"""None, solange es läuft. Danach das Ergebnis."""
|
"""None, solange es läuft. Danach das Ergebnis."""
|
||||||
return self.ergebnis or {}
|
return self.ergebnis or {}
|
||||||
|
|
||||||
|
def als_admin(self) -> dict:
|
||||||
|
"""Startet das Setup mit Administratorrechten neu.
|
||||||
|
|
||||||
|
Nur auf Knopfdruck, nie von selbst. Der Grund steht in
|
||||||
|
`winlauf.als_admin_neu_starten`: Erhöht sieht Rippy die eingebundenen
|
||||||
|
Netzlaufwerke NICHT mehr — für eine Ablage auf einer Freigabe wäre das
|
||||||
|
genau der falsche Weg.
|
||||||
|
|
||||||
|
Gibt zurück, ob der Neustart angenommen wurde. Ein „nein" ist der
|
||||||
|
Normalfall, wenn der Nutzer die UAC-Abfrage wegklickt — dann bleibt
|
||||||
|
das Fenster einfach offen.
|
||||||
|
"""
|
||||||
|
from rippy import windows_app
|
||||||
|
from rippy.platform import winlauf
|
||||||
|
|
||||||
|
geklappt = winlauf.als_admin_neu_starten(windows_app._eigene_datei())
|
||||||
|
if geklappt:
|
||||||
|
self.schliessen()
|
||||||
|
return {"gestartet": geklappt}
|
||||||
|
|
||||||
def schliessen(self) -> None:
|
def schliessen(self) -> None:
|
||||||
try:
|
try:
|
||||||
self._fenster.destroy()
|
self._fenster.destroy()
|
||||||
@@ -251,6 +372,14 @@ SEITE = r"""<!DOCTYPE html>
|
|||||||
display:flex; align-items:center; gap:16px;
|
display:flex; align-items:center; gap:16px;
|
||||||
backdrop-filter:blur(6px); }
|
backdrop-filter:blur(6px); }
|
||||||
#urteil { flex:1; font-size:12.5px; color:#94a3b8; }
|
#urteil { flex:1; font-size:12.5px; color:#94a3b8; }
|
||||||
|
.balken-rahmen { height:9px; background:#111a2b; border-radius:99px;
|
||||||
|
overflow:hidden; margin:2px 0 10px; }
|
||||||
|
#balken { height:100%; width:0%; border-radius:99px; background:#f59e0b;
|
||||||
|
transition:width .35s ease; }
|
||||||
|
.balken-zeile { display:flex; justify-content:space-between;
|
||||||
|
font-size:12.5px; color:#cbd5e1; margin-bottom:4px; }
|
||||||
|
#prozent { font-family:ui-monospace,Consolas,monospace; color:#f59e0b;
|
||||||
|
font-variant-numeric:tabular-nums; }
|
||||||
#protokoll { background:#080c15; border:1px solid #1a2438; border-radius:8px;
|
#protokoll { background:#080c15; border:1px solid #1a2438; border-radius:8px;
|
||||||
padding:11px 13px; font-family:ui-monospace,Consolas,monospace;
|
padding:11px 13px; font-family:ui-monospace,Consolas,monospace;
|
||||||
font-size:12px; color:#94a3b8; max-height:190px; overflow:auto;
|
font-size:12px; color:#94a3b8; max-height:190px; overflow:auto;
|
||||||
@@ -293,16 +422,24 @@ SEITE = r"""<!DOCTYPE html>
|
|||||||
<label class="schalter"><input type="checkbox" id="verknuepfungen" checked>
|
<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>
|
<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>
|
<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>
|
<span>Fehlende Werkzeuge holen<div class="hinweis">HandBrake liegt bei. MakeMKV wird von makemkv.com geladen und mit dem Installer des Herstellers installiert — dafür fragt Windows einmal nach Administratorrechten. Ohne MakeMKV kann Rippy keine Disc lesen.</div></span></label>
|
||||||
|
<label class="schalter"><input type="checkbox" id="aktualisieren" checked>
|
||||||
|
<span>Vorhandene Werkzeuge aktualisieren<div class="hinweis" id="update-hinweis">wird geprüft …</div></span></label>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="teil-lauf" class="weg">
|
<section id="teil-lauf" class="weg">
|
||||||
<h2>Läuft</h2>
|
<h2>Läuft</h2>
|
||||||
|
<div class="balken-zeile">
|
||||||
|
<span id="abschnitt">wird vorbereitet …</span>
|
||||||
|
<span id="prozent">0 %</span>
|
||||||
|
</div>
|
||||||
|
<div class="balken-rahmen"><div id="balken"></div></div>
|
||||||
<div id="protokoll"></div>
|
<div id="protokoll"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<div id="urteil">wird geprüft …</div>
|
<div id="urteil">wird geprüft …</div>
|
||||||
|
<button id="knopf-admin" class="weg" onclick="alsAdmin()">Als Administrator neu starten</button>
|
||||||
<button id="knopf-abbruch" onclick="pywebview.api.schliessen()">Abbrechen</button>
|
<button id="knopf-abbruch" onclick="pywebview.api.schliessen()">Abbrechen</button>
|
||||||
<button id="knopf-los" class="los" onclick="losgehen()" disabled>Installieren</button>
|
<button id="knopf-los" class="los" onclick="losgehen()" disabled>Installieren</button>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -321,6 +458,43 @@ SEITE = r"""<!DOCTYPE html>
|
|||||||
+ '</div></div>').join('');
|
+ '</div></div>').join('');
|
||||||
$('urteil').textContent = bericht.text;
|
$('urteil').textContent = bericht.text;
|
||||||
$('knopf-los').disabled = !bericht.darf || laeuft;
|
$('knopf-los').disabled = !bericht.darf || laeuft;
|
||||||
|
|
||||||
|
/* Der Knopf für Administratorrechte erscheint NUR, wenn das Schreibrecht
|
||||||
|
am gewählten Ziel fehlt. Nicht dauerhaft: Erhöht sieht Rippy die
|
||||||
|
eingebundenen Netzlaufwerke nicht mehr — wer eine Freigabe als Ablage
|
||||||
|
will, darf ihn gerade NICHT drücken. */
|
||||||
|
const rechteProblem = bericht.befunde.some(
|
||||||
|
b => b.name === 'Schreibrecht' && b.zustand === 'fehler');
|
||||||
|
$('knopf-admin').classList.toggle('weg', !rechteProblem || laeuft);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatesNachtragen() {
|
||||||
|
/* Getrennt vom Voraussetzungs-Bericht: Die Auskunft kostet Netz, und
|
||||||
|
makemkv.com braucht dafuer im schlimmsten Fall gut zwanzig Sekunden.
|
||||||
|
Solange steht "wird geprueft" da, statt das Fenster leer zu lassen. */
|
||||||
|
const stand = await pywebview.api.updates();
|
||||||
|
const hinweis = $('update-hinweis');
|
||||||
|
if (!stand.erreichbar) {
|
||||||
|
hinweis.textContent = 'Update-Quellen nicht erreichbar — es wird nichts aktualisiert.';
|
||||||
|
$('aktualisieren').checked = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!stand.offen.length) {
|
||||||
|
hinweis.textContent = 'Alles auf dem neuesten Stand.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hinweis.textContent = stand.offen
|
||||||
|
.map(o => o.titel + ' ' + o.installiert + ' → ' + o.neueste)
|
||||||
|
.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function alsAdmin() {
|
||||||
|
$('knopf-admin').disabled = true;
|
||||||
|
const antwort = await pywebview.api.als_admin();
|
||||||
|
if (!antwort.gestartet) {
|
||||||
|
$('knopf-admin').disabled = false;
|
||||||
|
$('urteil').textContent = 'Der Neustart als Administrator wurde abgebrochen.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pruefen() {
|
async function pruefen() {
|
||||||
@@ -346,27 +520,39 @@ SEITE = r"""<!DOCTYPE html>
|
|||||||
autostart: $('autostart').checked,
|
autostart: $('autostart').checked,
|
||||||
verknuepfungen: $('verknuepfungen').checked,
|
verknuepfungen: $('verknuepfungen').checked,
|
||||||
werkzeuge_holen: $('werkzeuge_holen').checked,
|
werkzeuge_holen: $('werkzeuge_holen').checked,
|
||||||
|
aktualisieren: $('aktualisieren').checked,
|
||||||
});
|
});
|
||||||
verfolgen();
|
verfolgen();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verfolgen() {
|
async function verfolgen() {
|
||||||
const neue = await pywebview.api.fortschritt();
|
const takt = await pywebview.api.fortschritt();
|
||||||
if (neue.length) {
|
if (takt.zeilen && takt.zeilen.length) {
|
||||||
const p = $('protokoll');
|
const p = $('protokoll');
|
||||||
p.textContent += neue.join('\n') + '\n';
|
p.textContent += takt.zeilen.join('\n') + '\n';
|
||||||
p.scrollTop = p.scrollHeight;
|
p.scrollTop = p.scrollHeight;
|
||||||
}
|
}
|
||||||
const stand = await pywebview.api.stand();
|
if (typeof takt.prozent === 'number') {
|
||||||
if (stand && stand.ok !== undefined) {
|
$('balken').style.width = takt.prozent + '%';
|
||||||
|
$('prozent').textContent = takt.prozent + ' %';
|
||||||
|
}
|
||||||
|
if (takt.abschnitt) $('abschnitt').textContent = takt.abschnitt;
|
||||||
|
|
||||||
|
const ergebnis = await pywebview.api.stand();
|
||||||
|
if (ergebnis && ergebnis.ok !== undefined) {
|
||||||
|
// Am Ende ist der Balken voll — und die Farbe sagt, wie es ausging.
|
||||||
|
$('balken').style.width = '100%';
|
||||||
|
$('prozent').textContent = '100 %';
|
||||||
|
$('balken').style.background = ergebnis.ok ? '#34d399' : '#f87171';
|
||||||
|
$('abschnitt').textContent = ergebnis.ok ? 'Fertig' : 'Abgebrochen';
|
||||||
$('knopf-abbruch').classList.add('weg');
|
$('knopf-abbruch').classList.add('weg');
|
||||||
$('knopf-los').disabled = false;
|
$('knopf-los').disabled = false;
|
||||||
$('knopf-los').textContent = 'Rippy öffnen';
|
$('knopf-los').textContent = 'Rippy öffnen';
|
||||||
$('knopf-los').onclick = () => pywebview.api.schliessen();
|
$('knopf-los').onclick = () => pywebview.api.schliessen();
|
||||||
$('urteil').textContent = stand.ok
|
$('urteil').textContent = ergebnis.ok
|
||||||
? (stand.bereit ? 'Fertig — Rippy ist einsatzbereit.'
|
? (ergebnis.bereit ? 'Fertig — Rippy ist einsatzbereit.'
|
||||||
: 'Installiert, aber es fehlt noch ein Werkzeug (siehe oben).')
|
: 'Installiert, aber es fehlt noch ein Werkzeug (siehe oben).')
|
||||||
: ('Fehlgeschlagen: ' + stand.fehler);
|
: ('Fehlgeschlagen: ' + ergebnis.fehler);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTimeout(verfolgen, 700);
|
setTimeout(verfolgen, 700);
|
||||||
@@ -381,6 +567,7 @@ SEITE = r"""<!DOCTYPE html>
|
|||||||
$('verknuepfungen').checked = v.verknuepfungen;
|
$('verknuepfungen').checked = v.verknuepfungen;
|
||||||
$('werkzeuge_holen').checked = v.werkzeuge_holen;
|
$('werkzeuge_holen').checked = v.werkzeuge_holen;
|
||||||
pruefen();
|
pruefen();
|
||||||
|
updatesNachtragen();
|
||||||
$('port').addEventListener('change', pruefen);
|
$('port').addEventListener('change', pruefen);
|
||||||
$('ziel').addEventListener('change', pruefen);
|
$('ziel').addEventListener('change', pruefen);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,9 +67,10 @@ def test_gewaehlter_ordner_kommt_als_text():
|
|||||||
def test_fortschritt_leert_sich_beim_abholen():
|
def test_fortschritt_leert_sich_beim_abholen():
|
||||||
"""Sonst waechst das Protokoll bei jedem Abruf um alles Bisherige."""
|
"""Sonst waechst das Protokoll bei jedem Abruf um alles Bisherige."""
|
||||||
b = setup_fenster.Bruecke()
|
b = setup_fenster.Bruecke()
|
||||||
b._fortschritt = ["eins", "zwei"]
|
b._lauf.melde("eins")
|
||||||
assert b.fortschritt() == ["eins", "zwei"]
|
b._lauf.melde("zwei")
|
||||||
assert b.fortschritt() == []
|
assert b.fortschritt()["zeilen"] == ["eins", "zwei"]
|
||||||
|
assert b.fortschritt()["zeilen"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_stand_ist_leer_solange_nichts_fertig_ist():
|
def test_stand_ist_leer_solange_nichts_fertig_ist():
|
||||||
@@ -90,7 +91,7 @@ def test_ein_fehlschlag_beim_installieren_wird_SICHTBAR(monkeypatch):
|
|||||||
|
|
||||||
assert b.ergebnis["ok"] is False
|
assert b.ergebnis["ok"] is False
|
||||||
assert "Zugriff verweigert" in b.ergebnis["fehler"]
|
assert "Zugriff verweigert" in b.ergebnis["fehler"]
|
||||||
assert any("Fehlgeschlagen" in z for z in b._fortschritt)
|
assert any("Fehlgeschlagen" in z for z in b._lauf.zeilen)
|
||||||
|
|
||||||
|
|
||||||
def test_erfolgreiche_installation_meldet_den_ordner(monkeypatch):
|
def test_erfolgreiche_installation_meldet_den_ordner(monkeypatch):
|
||||||
@@ -120,3 +121,166 @@ def test_werkzeuge_koennen_uebersprungen_werden(monkeypatch):
|
|||||||
|
|
||||||
def test_schliessen_ohne_fenster_wirft_nicht():
|
def test_schliessen_ohne_fenster_wirft_nicht():
|
||||||
setup_fenster.Bruecke().schliessen()
|
setup_fenster.Bruecke().schliessen()
|
||||||
|
|
||||||
|
|
||||||
|
# -- Der Fortschrittsbalken ---------------------------------------------
|
||||||
|
def test_der_balken_faengt_bei_null_an():
|
||||||
|
f = setup_fenster.Fortschritt()
|
||||||
|
assert f.abholen() == {"zeilen": [], "prozent": 0, "abschnitt": ""}
|
||||||
|
|
||||||
|
|
||||||
|
def test_ein_abschnitt_setzt_den_anfang():
|
||||||
|
f = setup_fenster.Fortschritt()
|
||||||
|
f.beginne("werkzeuge")
|
||||||
|
stand = f.abholen()
|
||||||
|
assert stand["prozent"] == 10
|
||||||
|
assert "Werkzeuge" in stand["abschnitt"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_der_anteil_wird_in_den_abschnitt_gerechnet():
|
||||||
|
"""Werkzeuge belegen 10 bis 97 Prozent. Halb fertig heisst also rund 54."""
|
||||||
|
f = setup_fenster.Fortschritt()
|
||||||
|
f.beginne("werkzeuge")
|
||||||
|
f.melde("laeuft", 0.5)
|
||||||
|
assert 50 <= f.prozent <= 58, f.prozent
|
||||||
|
|
||||||
|
|
||||||
|
def test_der_balken_laeuft_NIE_zurueck():
|
||||||
|
"""Der Werkzeug-Abschnitt kann mehrere Downloads enthalten, jeder mit
|
||||||
|
eigenem 0-bis-1. Ohne diese Regel spraenge der Balken bei jedem neuen
|
||||||
|
Werkzeug zurueck an den Abschnittsanfang."""
|
||||||
|
f = setup_fenster.Fortschritt()
|
||||||
|
f.beginne("werkzeuge")
|
||||||
|
f.melde("erstes Werkzeug fertig", 0.9)
|
||||||
|
hoch = f.prozent
|
||||||
|
f.melde("zweites Werkzeug faengt an", 0.0)
|
||||||
|
assert f.prozent == hoch, "der Balken ist zurueckgesprungen"
|
||||||
|
|
||||||
|
|
||||||
|
def test_eine_meldung_ohne_zahl_bewegt_nichts():
|
||||||
|
"""Eine Textmeldung ist keine Aussage ueber den Fortschritt."""
|
||||||
|
f = setup_fenster.Fortschritt()
|
||||||
|
f.beginne("dateien")
|
||||||
|
f.melde("irgendwas passiert")
|
||||||
|
assert f.prozent == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_zeilen_leeren_sich_beim_abholen_der_stand_nicht():
|
||||||
|
f = setup_fenster.Fortschritt()
|
||||||
|
f.beginne("werkzeuge")
|
||||||
|
f.melde("eins", 0.5)
|
||||||
|
erst = f.abholen()
|
||||||
|
zweit = f.abholen()
|
||||||
|
assert erst["zeilen"] == ["eins"]
|
||||||
|
assert zweit["zeilen"] == []
|
||||||
|
assert zweit["prozent"] == erst["prozent"], "der Stand darf nicht vergessen werden"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ein_verrutschter_anteil_sprengt_nichts():
|
||||||
|
f = setup_fenster.Fortschritt()
|
||||||
|
f.beginne("dateien")
|
||||||
|
f.melde("x", 5.0)
|
||||||
|
assert f.prozent <= 100
|
||||||
|
f.melde("y", -1.0)
|
||||||
|
assert f.prozent <= 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_die_abschnitte_ueberlappen_nicht_und_enden_bei_hundert():
|
||||||
|
grenzen = sorted(setup_fenster.ABSCHNITTE.values())
|
||||||
|
assert grenzen[0][0] == 0.0
|
||||||
|
assert grenzen[-1][1] == 1.0
|
||||||
|
for (_, ende_a, _), (von_b, _, _) in zip(grenzen, grenzen[1:]):
|
||||||
|
assert abs(ende_a - von_b) < 1e-9, (ende_a, von_b)
|
||||||
|
|
||||||
|
|
||||||
|
def test_die_seite_zeigt_einen_balken():
|
||||||
|
assert setup_fenster.seite_enthaelt('id="balken"')
|
||||||
|
assert setup_fenster.seite_enthaelt("prozent")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Administratorrechte auf Abruf --------------------------------------
|
||||||
|
def test_der_admin_knopf_ist_da_aber_nicht_vorgewaehlt():
|
||||||
|
"""Commander: "lass das setup sonst immer mit erhoehten rechten starten".
|
||||||
|
|
||||||
|
Immer erhoeht waere genau der Fehler, den er selbst gemeldet hat: Ein
|
||||||
|
erhoehter Prozess sieht die eingebundenen Netzlaufwerke NICHT (auf diesem
|
||||||
|
Rechner gemessen: X:, Y:, Z: ohne Erhoehung sichtbar,
|
||||||
|
EnableLinkedConnections nicht gesetzt). Deshalb ein Knopf statt einer
|
||||||
|
Voreinstellung -- und der ist anfangs verborgen."""
|
||||||
|
assert setup_fenster.seite_enthaelt('id="knopf-admin"')
|
||||||
|
assert setup_fenster.seite_enthaelt('class="weg"')
|
||||||
|
assert setup_fenster.seite_enthaelt("pywebview.api.als_admin")
|
||||||
|
|
||||||
|
|
||||||
|
def test_der_admin_knopf_erscheint_nur_beim_rechteproblem():
|
||||||
|
"""Sonst laedt er dazu ein, sich die Netzlaufwerke wegzunehmen."""
|
||||||
|
assert setup_fenster.seite_enthaelt("b.name === 'Schreibrecht'")
|
||||||
|
assert setup_fenster.seite_enthaelt("b.zustand === 'fehler'")
|
||||||
|
|
||||||
|
|
||||||
|
def test_ein_abgebrochener_uac_dialog_schliesst_das_fenster_nicht(monkeypatch):
|
||||||
|
"""Wer die UAC-Abfrage wegklickt, will weitermachen -- nicht von vorn
|
||||||
|
anfangen."""
|
||||||
|
from rippy.platform import winlauf
|
||||||
|
|
||||||
|
monkeypatch.setattr(winlauf, "als_admin_neu_starten", lambda *a, **k: False)
|
||||||
|
b = setup_fenster.Bruecke()
|
||||||
|
b._fenster = LeeresFenster()
|
||||||
|
assert b.als_admin() == {"gestartet": False}
|
||||||
|
assert b._fenster.zerstoert is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_ein_angenommener_neustart_schliesst_das_fenster(monkeypatch):
|
||||||
|
from rippy.platform import winlauf
|
||||||
|
|
||||||
|
monkeypatch.setattr(winlauf, "als_admin_neu_starten", lambda *a, **k: True)
|
||||||
|
b = setup_fenster.Bruecke()
|
||||||
|
b._fenster = LeeresFenster()
|
||||||
|
assert b.als_admin() == {"gestartet": True}
|
||||||
|
assert b._fenster.zerstoert is True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Updates im Setup ----------------------------------------------------
|
||||||
|
def test_die_seite_bietet_updates_an():
|
||||||
|
"""Commander: "bitte baue ein, das wenn MakeMKV oder Handbrake schon
|
||||||
|
installiert sind ein update verfuegbar ist. Das kann ja theoretisch direkt
|
||||||
|
im setup abgehandelt werden.\""""
|
||||||
|
assert setup_fenster.seite_enthaelt('id="aktualisieren"')
|
||||||
|
assert setup_fenster.seite_enthaelt("pywebview.api.updates")
|
||||||
|
assert setup_fenster.seite_enthaelt("aktualisieren: $('aktualisieren').checked")
|
||||||
|
|
||||||
|
|
||||||
|
def test_nicht_erreichbare_quellen_schalten_das_update_AB():
|
||||||
|
"""Eine Nichtauskunft ist keine Aussage -- und ein Haken, der nichts tun
|
||||||
|
kann, ist eine Falle."""
|
||||||
|
assert setup_fenster.seite_enthaelt("stand.erreichbar")
|
||||||
|
assert setup_fenster.seite_enthaelt("$('aktualisieren').checked = false")
|
||||||
|
|
||||||
|
|
||||||
|
def test_updates_ohne_netz_melden_nicht_erreichbar(monkeypatch):
|
||||||
|
from rippy.tools import beschaffen
|
||||||
|
|
||||||
|
def platzt():
|
||||||
|
raise OSError("kein Netz")
|
||||||
|
|
||||||
|
monkeypatch.setattr(beschaffen, "neueste_versionen", platzt)
|
||||||
|
stand = setup_fenster.Bruecke().updates()
|
||||||
|
assert stand == {"erreichbar": False, "offen": []}
|
||||||
|
|
||||||
|
|
||||||
|
def test_updates_melden_was_offen_ist(monkeypatch):
|
||||||
|
from rippy.tools import beschaffen, katalog
|
||||||
|
|
||||||
|
monkeypatch.setattr(katalog, "bestand", lambda *a, **k: {
|
||||||
|
"makemkv": {"titel": "MakeMKV", "da": True, "version": "1.18.2",
|
||||||
|
"pflicht": True},
|
||||||
|
"handbrake": {"titel": "HandBrakeCLI", "da": True, "version": "1.11.2",
|
||||||
|
"pflicht": False}})
|
||||||
|
monkeypatch.setattr(beschaffen, "neueste_versionen",
|
||||||
|
lambda: {"makemkv": "1.18.4", "handbrake": "1.11.2"})
|
||||||
|
|
||||||
|
stand = setup_fenster.Bruecke().updates()
|
||||||
|
assert stand["erreichbar"] is True
|
||||||
|
assert [o["titel"] for o in stand["offen"]] == ["MakeMKV"]
|
||||||
|
assert stand["offen"][0]["installiert"] == "1.18.2"
|
||||||
|
assert stand["offen"][0]["neueste"] == "1.18.4"
|
||||||
|
|||||||
@@ -505,21 +505,62 @@ def makemkv_holen(version: str = "", basis: str = None, fortschritt=None,
|
|||||||
"Rippy findet es danach von allein.")
|
"Rippy findet es danach von allein.")
|
||||||
|
|
||||||
|
|
||||||
def starte_installer(pfad: str, still: bool = True) -> None:
|
def starte_installer(pfad: str, still: bool = True, starter=None) -> None:
|
||||||
"""Startet eine Installationsdatei — auf Wunsch ohne Rückfragen.
|
r"""Startet eine Installationsdatei — auf Wunsch ohne Rückfragen.
|
||||||
|
|
||||||
`/S` ist der stille Modus von NSIS, mit dem MakeMKVs Windows-Installer
|
`/S` ist der stille Modus von NSIS, mit dem MakeMKVs Windows-Installer
|
||||||
gebaut ist. Ohne Kennzeichen erscheint der gewohnte Dialog; das ist der
|
gebaut ist. Ohne Kennzeichen erscheint der gewohnte Dialog; das ist der
|
||||||
ehrlichere Vorgabewert, wenn jemand zusieht.
|
ehrlichere Vorgabewert, wenn jemand zusieht.
|
||||||
|
|
||||||
|
## Warum ShellExecute und NICHT subprocess.Popen (28.08.2026)
|
||||||
|
|
||||||
|
Der Commander: *„Das Problem ist, das make MKV sich nicht installieren
|
||||||
|
lässt weil der vorgang erhöhte rechte benötigt."*
|
||||||
|
|
||||||
|
Er hat den Kern getroffen, und der Fehler lag hier. MakeMKV installiert
|
||||||
|
nach `C:\Program Files (x86)` — sein Installer fordert im Manifest
|
||||||
|
Administratorrechte an. `subprocess.Popen` benutzt `CreateProcess`, und
|
||||||
|
das **zeigt keine UAC-Abfrage**: Es bricht mit `ERROR_ELEVATION_REQUIRED`
|
||||||
|
(740) ab. `ShellExecuteW` ist der dokumentierte Weg, der die Abfrage
|
||||||
|
auslöst.
|
||||||
|
|
||||||
|
## Warum trotzdem nicht das ganze Setup erhöht läuft
|
||||||
|
|
||||||
|
Weil ein erhöhter Prozess die eingebundenen Netzlaufwerke NICHT sieht —
|
||||||
|
auf diesem Rechner gemessen: X:, Y:, Z: ohne Erhöhung sichtbar,
|
||||||
|
`EnableLinkedConnections` nicht gesetzt. Der Commander hat genau das
|
||||||
|
vorher als Fehler gemeldet. Erhöht wird deshalb nur DIESER eine Aufruf:
|
||||||
|
eine UAC-Abfrage für MakeMKVs Installer, und Rippy behält seine
|
||||||
|
Laufwerke.
|
||||||
|
|
||||||
|
`starter` ist einspritzbar, damit der Weg prüfbar ist, ohne etwas zu
|
||||||
|
installieren.
|
||||||
"""
|
"""
|
||||||
|
if starter is not None:
|
||||||
|
if not starter(pfad, "/S" if still else ""):
|
||||||
|
raise BeschaffungsFehler(
|
||||||
|
"Der Installer wurde nicht gestartet (Abfrage abgebrochen?).")
|
||||||
|
return
|
||||||
|
|
||||||
|
if os.name != "nt":
|
||||||
from rippy.platform.winlauf import OHNE_FENSTER
|
from rippy.platform.winlauf import OHNE_FENSTER
|
||||||
|
|
||||||
befehl = [pfad] + (["/S"] if still else [])
|
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(befehl, creationflags=OHNE_FENSTER, close_fds=True)
|
subprocess.Popen([pfad] + (["/S"] if still else []),
|
||||||
|
creationflags=OHNE_FENSTER, close_fds=True)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
raise BeschaffungsFehler(
|
raise BeschaffungsFehler(
|
||||||
f"Der Installer ließ sich nicht starten: {e}") from e
|
f"Der Installer ließ sich nicht starten: {e}") from e
|
||||||
|
return
|
||||||
|
|
||||||
|
from rippy.platform import winlauf
|
||||||
|
|
||||||
|
if not winlauf.mit_rechteabfrage_starten(pfad, "/S" if still else ""):
|
||||||
|
raise BeschaffungsFehler(
|
||||||
|
'Der Installer von MakeMKV wurde nicht gestartet. Er braucht '
|
||||||
|
'Administratorrechte (er installiert nach „Programme") — die '
|
||||||
|
'Abfrage von Windows muss bestätigt werden. Rippy selbst braucht '
|
||||||
|
'sie nicht.')
|
||||||
|
|
||||||
|
|
||||||
# ── Gemeinsam ───────────────────────────────────────────────────────────
|
# ── Gemeinsam ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -56,9 +56,31 @@ def einsatzbereit(bestand: dict) -> bool:
|
|||||||
return all(e.get("da") for e in bestand.values() if e.get("pflicht"))
|
return all(e.get("da") for e in bestand.values() if e.get("pflicht"))
|
||||||
|
|
||||||
|
|
||||||
|
def veraltete(bestand: dict, neueste: dict) -> list:
|
||||||
|
"""Welche Werkzeuge sind DA, aber überholt? (reine Funktion)
|
||||||
|
|
||||||
|
Commander am 28.08.2026: *„bitte baue ein, das wenn MakeMKV oder Handbrake
|
||||||
|
schon installiert sind ein update verfügbar ist. Das kann ja theoretisch
|
||||||
|
direkt im setup abgehandelt werden."*
|
||||||
|
|
||||||
|
Verglichen wird über `beschaffen.ist_neuer` — also nach ZAHLEN. Ein
|
||||||
|
Zeichenvergleich hielte „1.9.2" für neuer als „1.18.4", und HandBrake ist
|
||||||
|
genau dort.
|
||||||
|
"""
|
||||||
|
offen = []
|
||||||
|
for name, eintrag in sorted((bestand or {}).items()):
|
||||||
|
if not eintrag.get("da"):
|
||||||
|
continue
|
||||||
|
neu = (neueste or {}).get(name, "")
|
||||||
|
if beschaffen.ist_neuer(neu, eintrag.get("version", "")):
|
||||||
|
offen.append((name, eintrag, neu))
|
||||||
|
return offen
|
||||||
|
|
||||||
|
|
||||||
def sicherstellen(fortschritt=None, eingestellt: dict = None,
|
def sicherstellen(fortschritt=None, eingestellt: dict = None,
|
||||||
handbrake_holen=None, makemkv_holen=None,
|
handbrake_holen=None, makemkv_holen=None,
|
||||||
bestand_lesen=None) -> dict:
|
bestand_lesen=None, aktualisieren: bool = False,
|
||||||
|
neueste_lesen=None) -> dict:
|
||||||
"""Holt, was fehlt. Gibt zurück, was DANACH wirklich da ist.
|
"""Holt, was fehlt. Gibt zurück, was DANACH wirklich da ist.
|
||||||
|
|
||||||
Die drei Funktionen sind einspritzbar — so ist der ganze Ablauf prüfbar,
|
Die drei Funktionen sind einspritzbar — so ist der ganze Ablauf prüfbar,
|
||||||
@@ -82,18 +104,46 @@ def sicherstellen(fortschritt=None, eingestellt: dict = None,
|
|||||||
vorher = bestand_lesen()
|
vorher = bestand_lesen()
|
||||||
geholt, fehler = [], {}
|
geholt, fehler = [], {}
|
||||||
|
|
||||||
for name, eintrag in fehlende(vorher):
|
offen = fehlende(vorher)
|
||||||
|
|
||||||
|
# Auf Wunsch auch das, was DA ist, aber überholt. Absichtlich hinter den
|
||||||
|
# fehlenden: Ein Update ist Komfort, ein fehlendes Pflichtwerkzeug ist ein
|
||||||
|
# Hindernis — bei wackliger Verbindung soll zuerst ankommen, was zählt.
|
||||||
|
if aktualisieren:
|
||||||
|
neueste_lesen = neueste_lesen or beschaffen.neueste_versionen
|
||||||
|
try:
|
||||||
|
neueste = neueste_lesen()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
# Ein Update-Check ist Komfort; er darf nichts blockieren.
|
||||||
|
neueste = {}
|
||||||
|
for name, eintrag, neu in veraltete(vorher, neueste):
|
||||||
|
melden("%s %s -> %s wird aktualisiert …"
|
||||||
|
% (eintrag.get("titel", name), eintrag.get("version", "?"), neu))
|
||||||
|
offen.append((name, eintrag))
|
||||||
|
for nummer, (name, eintrag) in enumerate(offen):
|
||||||
titel = eintrag.get("titel", name)
|
titel = eintrag.get("titel", name)
|
||||||
|
|
||||||
|
def teil_melden(text, anteil=None, _n=nummer, _gesamt=len(offen)):
|
||||||
|
"""Den Anteil EINES Werkzeugs auf die ganze Strecke umrechnen.
|
||||||
|
|
||||||
|
Ohne das meldet jeder Download 0 bis 1, und ein Fortschrittsbalken
|
||||||
|
spränge bei jedem neuen Werkzeug zurück an den Anfang. Zwei
|
||||||
|
Werkzeuge: das erste füllt 0–50 %, das zweite 50–100 %.
|
||||||
|
"""
|
||||||
|
if fortschritt:
|
||||||
|
fortschritt(text, None if anteil is None
|
||||||
|
else (_n + anteil) / max(1, _gesamt))
|
||||||
|
|
||||||
melden("%s fehlt — wird geholt …" % titel)
|
melden("%s fehlt — wird geholt …" % titel)
|
||||||
try:
|
try:
|
||||||
if name == "handbrake":
|
if name == "handbrake":
|
||||||
handbrake_holen(fortschritt=fortschritt)
|
handbrake_holen(fortschritt=teil_melden)
|
||||||
elif name == "makemkv":
|
elif name == "makemkv":
|
||||||
# still=False: Der Nutzer SOLL den Installer von MakeMKV
|
# still=False: Der Nutzer SOLL den Installer von MakeMKV
|
||||||
# sehen. Ein fremdes Programm still im Hintergrund zu
|
# sehen. Ein fremdes Programm still im Hintergrund zu
|
||||||
# installieren, ist ein Ubergriff — und wenn es hakt, sieht
|
# installieren, ist ein Ubergriff — und wenn es hakt, sieht
|
||||||
# er wenigstens, woran.
|
# er wenigstens, woran.
|
||||||
makemkv_holen(fortschritt=fortschritt, still=False)
|
makemkv_holen(fortschritt=teil_melden, still=False)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
|
|||||||
@@ -184,3 +184,88 @@ def test_eine_neuere_fassung_wird_nicht_zurueckgedreht(tmp_path):
|
|||||||
def test_ohne_mitgeliefertes_passiert_nichts(tmp_path):
|
def test_ohne_mitgeliefertes_passiert_nichts(tmp_path):
|
||||||
assert einrichten.mitgelieferte_werkzeuge(
|
assert einrichten.mitgelieferte_werkzeuge(
|
||||||
str(tmp_path / "gibt-es-nicht"), str(tmp_path)) == []
|
str(tmp_path / "gibt-es-nicht"), str(tmp_path)) == []
|
||||||
|
|
||||||
|
|
||||||
|
# ── Updates schon vorhandener Werkzeuge ─────────────────────────────────
|
||||||
|
#
|
||||||
|
# Commander am 28.08.2026: "bitte baue ein, das wenn MakeMKV oder Handbrake
|
||||||
|
# schon installiert sind ein update verfuegbar ist. Das kann ja theoretisch
|
||||||
|
# direkt im setup abgehandelt werden."
|
||||||
|
def test_ueberholtes_werkzeug_wird_erkannt():
|
||||||
|
veraltet = einrichten.veraltete(_bestand(),
|
||||||
|
{"makemkv": "1.19.0", "handbrake": "1.11.2"})
|
||||||
|
assert [n for n, _, _ in veraltet] == ["makemkv"]
|
||||||
|
assert veraltet[0][2] == "1.19.0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_zweistellige_versionen_kippen_nicht():
|
||||||
|
"""DER Klassiker: Als Zeichenkette waere 1.9.2 groesser als 1.11.2 --
|
||||||
|
und HandBrake ist genau dort."""
|
||||||
|
bestand = {"handbrake": {"titel": "HandBrakeCLI", "da": True,
|
||||||
|
"version": "1.11.2", "pflicht": False}}
|
||||||
|
assert einrichten.veraltete(bestand, {"handbrake": "1.9.2"}) == []
|
||||||
|
assert len(einrichten.veraltete(bestand, {"handbrake": "1.11.3"})) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_ohne_auskunft_gibt_es_kein_update():
|
||||||
|
"""Antwortet die Quelle nicht, darf NICHT 'Update verfuegbar' dastehen.
|
||||||
|
Eine Nichtauskunft ist keine Aussage."""
|
||||||
|
assert einrichten.veraltete(_bestand(), {}) == []
|
||||||
|
assert einrichten.veraltete(_bestand(), {"makemkv": ""}) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_fehlende_werkzeuge_zaehlen_hier_nicht_mit():
|
||||||
|
"""Die holt `fehlende()` -- sonst stuende ein fehlendes Werkzeug zweimal
|
||||||
|
in der Liste und wuerde zweimal geholt."""
|
||||||
|
assert einrichten.veraltete(_bestand(makemkv=False),
|
||||||
|
{"makemkv": "1.19.0"}) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_ohne_flag_wird_NICHT_aktualisiert():
|
||||||
|
"""Eine unbeaufsichtigte Installation soll nicht ploetzlich Downloads
|
||||||
|
anstossen."""
|
||||||
|
gerufen = []
|
||||||
|
einrichten.sicherstellen(
|
||||||
|
bestand_lesen=lambda: _bestand(),
|
||||||
|
neueste_lesen=lambda: {"makemkv": "1.19.0"},
|
||||||
|
handbrake_holen=lambda **k: gerufen.append("hb"),
|
||||||
|
makemkv_holen=lambda **k: gerufen.append("mkv"))
|
||||||
|
assert gerufen == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_mit_flag_wird_aktualisiert():
|
||||||
|
gerufen = []
|
||||||
|
einrichten.sicherstellen(
|
||||||
|
aktualisieren=True,
|
||||||
|
bestand_lesen=lambda: _bestand(),
|
||||||
|
neueste_lesen=lambda: {"makemkv": "1.19.0", "handbrake": "1.11.2"},
|
||||||
|
handbrake_holen=lambda **k: gerufen.append("hb"),
|
||||||
|
makemkv_holen=lambda **k: gerufen.append("mkv"))
|
||||||
|
assert gerufen == ["mkv"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_fehlendes_kommt_VOR_dem_update():
|
||||||
|
"""Bei wackliger Verbindung soll zuerst ankommen, was zaehlt: Ein
|
||||||
|
fehlendes Pflichtwerkzeug ist ein Hindernis, ein Update ist Komfort."""
|
||||||
|
reihenfolge = []
|
||||||
|
einrichten.sicherstellen(
|
||||||
|
aktualisieren=True,
|
||||||
|
bestand_lesen=lambda: _bestand(makemkv=False),
|
||||||
|
neueste_lesen=lambda: {"handbrake": "1.99.0"},
|
||||||
|
handbrake_holen=lambda **k: reihenfolge.append("handbrake"),
|
||||||
|
makemkv_holen=lambda **k: reihenfolge.append("makemkv"))
|
||||||
|
assert reihenfolge == ["makemkv", "handbrake"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ein_kaputter_update_check_bricht_nichts_ab():
|
||||||
|
"""Ein Update-Check ist Komfort; er darf nichts blockieren."""
|
||||||
|
def platzt():
|
||||||
|
raise OSError("kein Netz")
|
||||||
|
|
||||||
|
ergebnis = einrichten.sicherstellen(
|
||||||
|
aktualisieren=True,
|
||||||
|
bestand_lesen=lambda: _bestand(),
|
||||||
|
neueste_lesen=platzt,
|
||||||
|
handbrake_holen=lambda **k: None,
|
||||||
|
makemkv_holen=lambda **k: None)
|
||||||
|
assert ergebnis["bereit"] is True
|
||||||
|
|||||||
Reference in New Issue
Block a user