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
|
||||
|
||||
|
||||
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:
|
||||
"""Ein Meldungsfenster. True, wenn es wirklich gezeigt wurde.
|
||||
|
||||
|
||||
+206
-19
@@ -38,6 +38,66 @@ BREITE = 860
|
||||
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:
|
||||
"""Was die Seite aufrufen darf. Bewusst klein und ohne Zustand im HTML.
|
||||
|
||||
@@ -48,7 +108,7 @@ class Bruecke:
|
||||
def __init__(self):
|
||||
self.ergebnis = None
|
||||
self._fenster = None
|
||||
self._fortschritt = []
|
||||
self._lauf = Fortschritt()
|
||||
|
||||
# ── Auskunft ────────────────────────────────────────────────────────
|
||||
def vorgaben(self) -> dict:
|
||||
@@ -65,6 +125,38 @@ class Bruecke:
|
||||
"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:
|
||||
"""Ein echter Ordner-Dialog. Leer, wenn abgebrochen wurde."""
|
||||
import webview
|
||||
@@ -78,10 +170,14 @@ class Bruecke:
|
||||
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
|
||||
def fortschritt(self) -> dict:
|
||||
"""Was seit dem letzten Abruf passiert ist — Zeilen UND Stand.
|
||||
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
def installieren(self, optionen: dict) -> dict:
|
||||
@@ -98,10 +194,10 @@ class Bruecke:
|
||||
def _installieren_wirklich(self, optionen: dict) -> None:
|
||||
from rippy import windows_app
|
||||
|
||||
def melden(text, anteil=None):
|
||||
self._fortschritt.append(str(text))
|
||||
melden = self._lauf.melde
|
||||
|
||||
try:
|
||||
self._lauf.beginne("dateien")
|
||||
melden("Dateien werden abgelegt …")
|
||||
ziel = windows_app.installieren(
|
||||
ziel=optionen.get("ziel") or None,
|
||||
@@ -110,19 +206,24 @@ class Bruecke:
|
||||
verknuepfen=bool(optionen.get("verknuepfungen", True)),
|
||||
)
|
||||
if optionen.get("ablage"):
|
||||
self._lauf.beginne("ablage")
|
||||
melden("Ablage wird gespeichert …")
|
||||
_ablage_speichern(optionen["ablage"])
|
||||
self._lauf.beginne("werkzeuge")
|
||||
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)
|
||||
bericht = werkzeug_einrichtung.sicherstellen(
|
||||
fortschritt=melden,
|
||||
aktualisieren=bool(optionen.get("aktualisieren", False)))
|
||||
melden(werkzeug_einrichtung.bericht(bericht))
|
||||
bereit = bericht["bereit"]
|
||||
else:
|
||||
bereit = True
|
||||
self._lauf.beginne("fertig")
|
||||
self.ergebnis = {"ok": True, "ziel": ziel, "bereit": bereit}
|
||||
melden("Fertig.")
|
||||
melden("Fertig.", 1.0)
|
||||
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
|
||||
@@ -135,6 +236,26 @@ class Bruecke:
|
||||
"""None, solange es läuft. Danach das Ergebnis."""
|
||||
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:
|
||||
try:
|
||||
self._fenster.destroy()
|
||||
@@ -251,6 +372,14 @@ SEITE = r"""<!DOCTYPE html>
|
||||
display:flex; align-items:center; gap:16px;
|
||||
backdrop-filter:blur(6px); }
|
||||
#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;
|
||||
padding:11px 13px; font-family:ui-monospace,Consolas,monospace;
|
||||
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>
|
||||
<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>
|
||||
<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 id="teil-lauf" class="weg">
|
||||
<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>
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<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-los" class="los" onclick="losgehen()" disabled>Installieren</button>
|
||||
</footer>
|
||||
@@ -321,6 +458,43 @@ SEITE = r"""<!DOCTYPE html>
|
||||
+ '</div></div>').join('');
|
||||
$('urteil').textContent = bericht.text;
|
||||
$('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() {
|
||||
@@ -346,27 +520,39 @@ SEITE = r"""<!DOCTYPE html>
|
||||
autostart: $('autostart').checked,
|
||||
verknuepfungen: $('verknuepfungen').checked,
|
||||
werkzeuge_holen: $('werkzeuge_holen').checked,
|
||||
aktualisieren: $('aktualisieren').checked,
|
||||
});
|
||||
verfolgen();
|
||||
}
|
||||
|
||||
async function verfolgen() {
|
||||
const neue = await pywebview.api.fortschritt();
|
||||
if (neue.length) {
|
||||
const takt = await pywebview.api.fortschritt();
|
||||
if (takt.zeilen && takt.zeilen.length) {
|
||||
const p = $('protokoll');
|
||||
p.textContent += neue.join('\n') + '\n';
|
||||
p.textContent += takt.zeilen.join('\n') + '\n';
|
||||
p.scrollTop = p.scrollHeight;
|
||||
}
|
||||
const stand = await pywebview.api.stand();
|
||||
if (stand && stand.ok !== undefined) {
|
||||
if (typeof takt.prozent === 'number') {
|
||||
$('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-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);
|
||||
$('urteil').textContent = ergebnis.ok
|
||||
? (ergebnis.bereit ? 'Fertig — Rippy ist einsatzbereit.'
|
||||
: 'Installiert, aber es fehlt noch ein Werkzeug (siehe oben).')
|
||||
: ('Fehlgeschlagen: ' + ergebnis.fehler);
|
||||
return;
|
||||
}
|
||||
setTimeout(verfolgen, 700);
|
||||
@@ -381,6 +567,7 @@ SEITE = r"""<!DOCTYPE html>
|
||||
$('verknuepfungen').checked = v.verknuepfungen;
|
||||
$('werkzeuge_holen').checked = v.werkzeuge_holen;
|
||||
pruefen();
|
||||
updatesNachtragen();
|
||||
$('port').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():
|
||||
"""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() == []
|
||||
b._lauf.melde("eins")
|
||||
b._lauf.melde("zwei")
|
||||
assert b.fortschritt()["zeilen"] == ["eins", "zwei"]
|
||||
assert b.fortschritt()["zeilen"] == []
|
||||
|
||||
|
||||
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 "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):
|
||||
@@ -120,3 +121,166 @@ def test_werkzeuge_koennen_uebersprungen_werden(monkeypatch):
|
||||
|
||||
def test_schliessen_ohne_fenster_wirft_nicht():
|
||||
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.")
|
||||
|
||||
|
||||
def starte_installer(pfad: str, still: bool = True) -> None:
|
||||
"""Startet eine Installationsdatei — auf Wunsch ohne Rückfragen.
|
||||
def starte_installer(pfad: str, still: bool = True, starter=None) -> None:
|
||||
r"""Startet eine Installationsdatei — auf Wunsch ohne Rückfragen.
|
||||
|
||||
`/S` ist der stille Modus von NSIS, mit dem MakeMKVs Windows-Installer
|
||||
gebaut ist. Ohne Kennzeichen erscheint der gewohnte Dialog; das ist der
|
||||
ehrlichere Vorgabewert, wenn jemand zusieht.
|
||||
"""
|
||||
from rippy.platform.winlauf import OHNE_FENSTER
|
||||
|
||||
befehl = [pfad] + (["/S"] if still else [])
|
||||
try:
|
||||
subprocess.Popen(befehl, creationflags=OHNE_FENSTER, close_fds=True)
|
||||
except OSError as e:
|
||||
## 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
|
||||
|
||||
try:
|
||||
subprocess.Popen([pfad] + (["/S"] if still else []),
|
||||
creationflags=OHNE_FENSTER, close_fds=True)
|
||||
except OSError as e:
|
||||
raise BeschaffungsFehler(
|
||||
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(
|
||||
f"Der Installer ließ sich nicht starten: {e}") from e
|
||||
'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 ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -56,9 +56,31 @@ def einsatzbereit(bestand: dict) -> bool:
|
||||
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,
|
||||
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.
|
||||
|
||||
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()
|
||||
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)
|
||||
|
||||
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)
|
||||
try:
|
||||
if name == "handbrake":
|
||||
handbrake_holen(fortschritt=fortschritt)
|
||||
handbrake_holen(fortschritt=teil_melden)
|
||||
elif name == "makemkv":
|
||||
# still=False: Der Nutzer SOLL den Installer von MakeMKV
|
||||
# sehen. Ein fremdes Programm still im Hintergrund zu
|
||||
# installieren, ist ein Ubergriff — und wenn es hakt, sieht
|
||||
# er wenigstens, woran.
|
||||
makemkv_holen(fortschritt=fortschritt, still=False)
|
||||
makemkv_holen(fortschritt=teil_melden, still=False)
|
||||
else:
|
||||
continue
|
||||
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):
|
||||
assert einrichten.mitgelieferte_werkzeuge(
|
||||
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