feat(tools): Ausweichquellen fuer MakeMKV — recherchiert und gemessen
Ampel / ampel (push) Successful in 1m1s

Commander: "bitte baue fuer MakeMKV fallback seiten ein."

Anlass war ein echter Ausfall. Am 28.08.2026 gemessen:

    https://www.makemkv.com/download/   HTTP 525   (dauerhaft)
    https://makemkv.com/download/       HTTP 525
    http://www.makemkv.com/download/    HTTP 403
    https://forum.makemkv.com/          200 / 522 / Zeitablauf (wechselnd)
    web.archive.org/web/2025id_/...exe  200, 16.432.607 Bytes

525/522 heissen: Cloudflare erreicht den Ursprungsserver nicht. Eine Stoerung
beim Hersteller -- dieselbe, die im Projekt schon einmal jeden Worker-Build
lahmgelegt hat.

## Die Kette

Version:  Hersteller-Seite (massgeblich) -> Forum-Ankuendigungen -> Archiv
Datei:    eigene Quelle -> Hersteller -> Internet Archive

Der Hersteller zuerst, immer. Das Archiv ist Rueckfallebene, und Rippy nennt
hinterher die Quelle, aus der die Datei kam.

Zwei Feinheiten, beide aus Messungen statt aus dem Kopf:

* Die HOECHSTE Nummer gewinnt, nicht die erste. Antwortet das Forum nicht,
  meldet das Archiv einen aelteren Stand (1.18.2 statt 1.18.4) -- "neueste
  Fassung" waere dann eine falsche Aussage.
* Ein zweiter Anlauf, mit knapper Zeitgrenze. Drei Abrufe am Forum:
  TimeoutError, HTTP 522, dann Erfolg. Ohne Wiederholung fiel die Quelle in
  zwei von drei Faellen aus; mit 20-Sekunden-Grenzen dauerte der schlimmste
  Fall 46 Sekunden, in denen das Setup eingefroren aussah. Jetzt acht
  Sekunden je Versuch, schlimmstenfalls gut zwanzig.

## Warum eine fremde Quelle trotzdem sicher ist

Der naheliegende Schutz geht NICHT: MakeMKV signiert seinen Installer nicht
(Get-AuthenticodeSignature -> NotSigned, an der echten Datei gemessen). Eine
Signaturpruefung waere eine, die immer fehlschlaegt -- schlimmer als keine,
weil sie Sicherheit vortaeuscht.

Geprueft wird die Versions-Ressource, ebenfalls gemessen:

    CompanyName      GuinpinSoft inc
    FileDescription  MakeMKV installer
    FileVersion      v1.18.4

Das ersetzt keine Signatur, faengt aber ab, was hier wirklich droht: eine
Fehlerseite mit .exe-Namen, ein abgebrochener Download, eine falsche Fassung.

Ende zu Ende nachgewiesen, ohne etwas zu installieren:

    Version laut Kette: 1.18.4
    Hersteller:         HTTP 525 -> uebersprungen
    Internet Archive:   15,7 MB in 6,1s
    Pruefung:           BESTANDEN (MakeMKV v1.18.4)

Die Grenze aus KONZEPT.md 6 bleibt: Rippy liefert MakeMKV weiterhin NICHT
mit. Es holt die Datei des Herstellers -- im Rueckfall aus einem Archiv, das
genau diese Datei aufbewahrt.

Ampel lokal: 733 gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-28 15:19:46 +02:00
co-authored by Claude Opus 5
parent 480b658294
commit 95beb91364
5 changed files with 653 additions and 43 deletions
+50
View File
@@ -1415,6 +1415,56 @@ WebView2 statt Win32-Dialog: Es ist wegen Entscheid 4 ohnehin da — der
Assistent kostet **null zusätzliche Bytes**. Die Seite lädt nichts aus dem
Netz; sie muss auf einem Rechner ohne Internet aufgehen.
#### 5a — Ausweichquellen für MakeMKV (28.08.2026, nachgetragen)
Der Commander: *„bitte baue für MakeMKV fallback seiten ein."* Anlass war ein
echter Ausfall. Gemessen:
https://www.makemkv.com/download/ HTTP 525 (dauerhaft)
https://forum.makemkv.com/ Zeitablauf / 522 / OK (wechselnd)
web.archive.org/web/2025id_/…exe HTTP 200, 16.432.607 Bytes
525/522 heißen: Cloudflare erreicht den Ursprungsserver nicht. Eine Störung
beim Hersteller, nichts, was Rippy reparieren könnte.
**Die Kette, in dieser Reihenfolge:**
| | Version | Datei |
|---|---|---|
| eigene Quelle (`basis`) | — | zuerst, wer sie einträgt hat sich etwas dabei gedacht |
| Hersteller-Seite | **maßgeblich** — wer sie erreicht, sucht nicht weiter | ja |
| Hersteller-Forum | ja (Ankündigungs-Bereich) | — |
| Internet Archive | ja (Rückfall) | ja |
Zwei Feinheiten, beide aus Messungen:
* **Die höchste Nummer gewinnt, nicht die erste.** Antwortet das Forum nicht,
meldet das Archiv einen älteren Stand (1.18.2 statt 1.18.4) — „neueste
Fassung" wäre dann eine falsche Aussage.
* **Ein zweiter Anlauf, mit knapper Zeitgrenze.** Drei Abrufe am Forum:
Zeitablauf, HTTP 522, dann Erfolg. Ohne Wiederholung fiel die Quelle in
zwei von drei Fällen aus; mit 20-Sekunden-Grenzen dauerte der schlimmste
Fall 46 Sekunden, in denen das Setup eingefroren aussah. Jetzt: acht
Sekunden je Versuch, schlimmstenfalls gut zwanzig insgesamt.
**Und warum das trotzdem sicher ist.** Der naheliegende Schutz geht nicht:
MakeMKV **signiert seinen Installer nicht** (`Get-AuthenticodeSignature` →
`NotSigned`, an der echten Datei gemessen). Eine Signaturprüfung wäre eine,
die immer fehlschlägt. Geprüft wird stattdessen die Versions-Ressource:
CompanyName GuinpinSoft inc
FileDescription MakeMKV installer
FileVersion v1.18.4
Das ersetzt keine Signatur — wer die Datei fälscht, fälscht auch die
Ressource. Es fängt aber zuverlässig ab, was hier wirklich droht: eine
Fehlerseite mit `.exe`-Namen, ein abgebrochener Download, eine falsche
Fassung. Und Rippy nennt hinterher die Quelle, aus der die Datei kam.
**Die Grenze aus `KONZEPT.md` § 6 bleibt unangetastet:** Rippy liefert
MakeMKV weiterhin NICHT mit. Es holt die Datei des Herstellers — im
Rückfall aus einem Archiv, das genau diese Datei aufbewahrt.
---
+124
View File
@@ -0,0 +1,124 @@
"""Was eine Windows-Datei über sich selbst sagt — und warum das hier zählt.
## Wozu (28.08.2026)
Rippy lädt den MakeMKV-Installer notfalls aus einer Ausweichquelle, weil
makemkv.com tagelang mit HTTP 525 antwortet. Eine Datei von woanders zu
holen und ungeprüft auszuführen wäre leichtsinnig.
Der naheliegende Schutz — die digitale Signatur — geht NICHT:
Get-AuthenticodeSignature Setup_MakeMKV_v1.18.4.exe
Status: NotSigned
Am 28.08.2026 an der echten Datei gemessen: **MakeMKV signiert seinen
Installer nicht.** Eine Signaturprüfung wäre also eine Prüfung, die immer
fehlschlägt — schlimmer als keine, weil sie Sicherheit vortäuscht und dann
den richtigen Weg blockiert.
Prüfbar ist die **Versions-Ressource**. An derselben Datei gemessen:
CompanyName GuinpinSoft inc
FileDescription MakeMKV installer
FileVersion v1.18.4
ProductName MakeMKV
Das ist kein Ersatz für eine Signatur — wer die Datei fälscht, kann auch die
Ressource fälschen. Aber es fängt zuverlässig ab, was hier wirklich droht:
eine Fehlerseite, ein umbenanntes Archiv, eine falsche Version, ein
abgebrochener Download. Und es sagt dem Nutzer, WAS er da bekommen hat.
"""
import os
FELDER = ("CompanyName", "FileDescription", "FileVersion", "ProductName",
"ProductVersion")
def versionsangaben(pfad: str) -> dict:
"""Die Versions-Ressource einer Windows-Datei. Leer, wenn keine da ist.
Wirft nicht: Eine Datei ohne Ressource ist ein gültiges Ergebnis (und
genau das, was eine Fehlerseite mit `.exe`-Namen hätte).
"""
if os.name != "nt" or not os.path.isfile(pfad):
return {}
import ctypes
try:
version = ctypes.WinDLL("version")
groesse = version.GetFileVersionInfoSizeW(pfad, None)
if not groesse:
return {}
puffer = ctypes.create_string_buffer(groesse)
if not version.GetFileVersionInfoW(pfad, 0, groesse, puffer):
return {}
zeiger = ctypes.c_void_p()
laenge = ctypes.c_uint()
if not version.VerQueryValueW(puffer, r"\VarFileInfo\Translation",
ctypes.byref(zeiger), ctypes.byref(laenge)):
return {}
paar = ctypes.cast(zeiger, ctypes.POINTER(ctypes.c_uint16))
sprache = "%04x%04x" % (paar[0], paar[1])
ergebnis = {}
for feld in FELDER:
z = ctypes.c_void_p()
n = ctypes.c_uint()
pfad_im_block = "\\StringFileInfo\\%s\\%s" % (sprache, feld)
if version.VerQueryValueW(puffer, pfad_im_block,
ctypes.byref(z), ctypes.byref(n)) and n.value:
ergebnis[feld] = ctypes.wstring_at(z.value, n.value - 1).strip()
return ergebnis
except (AttributeError, OSError, ValueError):
return {}
def ist_programm(pfad: str) -> bool:
"""Fängt die Datei mit `MZ` an? (Das tut jedes Windows-Programm.)
Der billigste und wirksamste Test gegen das, was wirklich passiert: eine
HTML-Fehlerseite, die unter dem Namen `Setup_MakeMKV_v1.18.4.exe`
gespeichert wurde.
"""
try:
with open(pfad, "rb") as f:
return f.read(2) == b"MZ"
except OSError:
return False
# ── Die Prüfung als reine Funktion ──────────────────────────────────────
def passt(angaben: dict, firma: str = "", produkt: str = "",
version: str = "") -> tuple:
"""Passen die Angaben zu dem, was erwartet wird? (ja/nein, Begründung)
Reine Funktion — damit jeder Fall prüfbar ist, ohne eine Datei zu haben.
Leere Erwartungen werden nicht geprüft: Wer nichts fordert, bekommt kein
Urteil vorgesetzt.
"""
if not angaben:
return False, "Die Datei hat keine Versionsangaben — das ist kein " \
"Installationsprogramm, sondern vermutlich eine Fehlerseite."
gefunden_firma = angaben.get("CompanyName", "")
if firma and firma.lower() not in gefunden_firma.lower():
return False, ("Die Datei stammt laut ihren eigenen Angaben von "
"%r, erwartet war %r." % (gefunden_firma or "niemandem", firma))
text = " ".join(angaben.get(f, "") for f in
("ProductName", "FileDescription")).lower()
if produkt and produkt.lower() not in text:
return False, ("Die Datei bezeichnet sich als %r, erwartet war %r."
% (text.strip() or "nichts", produkt))
if version:
gefunden = (angaben.get("FileVersion") or
angaben.get("ProductVersion") or "").lstrip("vV")
if gefunden and not gefunden.startswith(version):
return False, ("Die Datei ist Fassung %s, erwartet war %s."
% (gefunden, version))
return True, "%s %s" % (angaben.get("ProductName", "?"),
angaben.get("FileVersion", "?"))
+98
View File
@@ -0,0 +1,98 @@
"""Was eine Windows-Datei ueber sich selbst sagt.
## Warum das geprueft wird
Rippy laedt den MakeMKV-Installer notfalls aus einer Ausweichquelle, weil
makemkv.com tagelang mit HTTP 525 antwortet. Eine Datei von woanders zu holen
und ungeprueft auszufuehren waere leichtsinnig.
Der naheliegende Schutz -- die digitale Signatur -- geht NICHT: Am 28.08.2026
an der echten Datei gemessen, `Get-AuthenticodeSignature` sagt `NotSigned`.
MakeMKV signiert seinen Installer nicht. Eine Signaturpruefung waere also eine
Pruefung, die immer fehlschlaegt.
Geprueft wird deshalb die Versions-Ressource. An derselben Datei gemessen:
CompanyName GuinpinSoft inc
FileDescription MakeMKV installer
FileVersion v1.18.4
Alle Pruefungen hier sind rein -- keine Datei noetig, laeuft auf jeder
Plattform.
"""
import os
from rippy.platform import dateiangaben as d
ECHT = {
"CompanyName": "GuinpinSoft inc",
"FileDescription": "MakeMKV installer",
"FileVersion": "v1.18.4",
"ProductName": "MakeMKV",
}
def test_die_echte_datei_wird_erkannt():
gut, grund = d.passt(ECHT, firma="GuinpinSoft", produkt="MakeMKV",
version="1.18.4")
assert gut is True, grund
assert "1.18.4" in grund
def test_ohne_versionsangaben_ist_es_keine_programmdatei():
"""Das ist der haeufigste Fall: eine Fehlerseite mit .exe-Namen."""
gut, grund = d.passt({}, firma="GuinpinSoft")
assert gut is False
assert "Fehlerseite" in grund
def test_fremde_firma_wird_abgelehnt():
fremd = dict(ECHT, CompanyName="Irgendwer GmbH")
gut, grund = d.passt(fremd, firma="GuinpinSoft")
assert gut is False
assert "Irgendwer" in grund
def test_falsche_version_wird_abgelehnt():
"""Ein Archiv-Schnappschuss koennte eine aeltere Fassung liefern."""
gut, grund = d.passt(ECHT, firma="GuinpinSoft", version="1.19.0")
assert gut is False
assert "1.18.4" in grund and "1.19.0" in grund
def test_fremdes_produkt_wird_abgelehnt():
fremd = dict(ECHT, ProductName="Notepad", FileDescription="Editor")
gut, _ = d.passt(fremd, firma="GuinpinSoft", produkt="MakeMKV")
assert gut is False
def test_ohne_erwartung_wird_nichts_geurteilt():
"""Wer nichts fordert, bekommt kein Urteil vorgesetzt."""
gut, _ = d.passt(ECHT)
assert gut is True
def test_das_v_vor_der_version_stoert_nicht():
"""Die Datei meldet 'v1.18.4', erwartet wird '1.18.4'."""
gut, _ = d.passt(ECHT, version="1.18.4")
assert gut is True
def test_mz_pruefung_an_einer_echten_datei(tmp_path):
programm = tmp_path / "p.exe"
programm.write_bytes(b"MZ" + b"\x00" * 100)
assert d.ist_programm(str(programm)) is True
seite = tmp_path / "fehler.exe"
seite.write_bytes(b"<html>525</html>")
assert d.ist_programm(str(seite)) is False
assert d.ist_programm(str(tmp_path / "gibt-es-nicht.exe")) is False
def test_versionsangaben_ohne_windows_sind_leer():
if os.name == "nt":
import pytest
pytest.skip("prueft das Verhalten auf Nicht-Windows")
assert d.versionsangaben("/bin/sh") == {}
+264 -39
View File
@@ -51,6 +51,7 @@ import os
import shutil
import subprocess
import tempfile
import time
import urllib.error
import urllib.request
import zipfile
@@ -58,13 +59,24 @@ import zipfile
from rippy.tools import katalog
HANDBRAKE_API = "https://api.github.com/repos/HandBrake/HandBrake/releases/latest"
MAKEMKV_SEITE = "https://www.makemkv.com/download/"
# Wie lange auf eine Antwort gewartet wird. Großzügig für den Download,
# knapp für die Versionsabfrage — die soll das UI nicht aufhalten.
ABFRAGE_TIMEOUT = 20
DOWNLOAD_TIMEOUT = 600
# Wackelige Quellen bekommen einen zweiten Anlauf. Gemessen am 28.08.2026:
# forum.makemkv.com scheiterte zweimal und antwortete beim dritten Versuch.
VERSUCHE = 2
WARTEN_ZWISCHEN_VERSUCHEN = 2.0
# Fuer die Versionsabfrage: knapp. Eine Seite zu lesen dauert bei einem
# gesunden Server Millisekunden; 20 s Zeitgrenze mal zwei Versuche mal zwei
# wacklige Quellen waren am 28.08.2026 SECHSUNDVIERZIG Sekunden, in denen
# das Setup eingefroren aussah. Acht Sekunden sind fuer eine Seite reichlich
# und begrenzen den schlimmsten Fall auf gut zwanzig.
VERSIONS_TIMEOUT = 8
# Eine HandBrakeCLI-ZIP ist rund 25 MB. Alles unter einem MB ist keine —
# sondern eine Fehlerseite, die mit HTTP 200 ausgeliefert wurde.
MINDESTGROESSE_BYTES = 1024 * 1024
@@ -74,14 +86,38 @@ class BeschaffungsFehler(RuntimeError):
"""Es hat nicht geklappt — mit einer Begründung, die man lesen kann."""
def _laden(url: str, timeout: int = ABFRAGE_TIMEOUT) -> bytes:
def _laden(url: str, timeout: int = ABFRAGE_TIMEOUT,
versuche: int = VERSUCHE) -> bytes:
"""Eine Seite holen — mit Wiederholung bei wackligen Servern.
## Warum wiederholt wird (gemessen 28.08.2026)
Drei Abrufe von `forum.makemkv.com` hintereinander:
1: TimeoutError (20,1 s)
2: HTTP 522 (19,7 s) Cloudflare erreicht den Ursprung nicht
3: OK, 127.311 Bytes (0,2 s)
Der Server ist nicht tot, er wackelt. Ohne Wiederholung fiel die Quelle
in zwei von drei Fällen aus — und dann gewann der ältere Archiv-Stand,
also 1.18.2 statt 1.18.4. Ein zweiter Anlauf kostet Sekunden und ändert
das Ergebnis.
"""
anfrage = urllib.request.Request(url, headers={
# GitHub verlangt eine Kennung; ohne sie kommt 403.
"User-Agent": "Rippy/2.0 (+https://github.com/)",
"Accept": "*/*",
})
letzter = None
for nummer in range(max(1, versuche)):
try:
with urllib.request.urlopen(anfrage, timeout=timeout) as antwort:
return antwort.read()
except (urllib.error.URLError, OSError) as e:
letzter = e
if nummer + 1 < max(1, versuche):
time.sleep(WARTEN_ZWISCHEN_VERSUCHEN)
raise letzter
# ── HandBrake ───────────────────────────────────────────────────────────
@@ -210,49 +246,214 @@ def _datei_laden(url: str, ziel: str, erwartet: int, melden) -> None:
# ── MakeMKV ─────────────────────────────────────────────────────────────
def makemkv_neueste_version(seite: str = None) -> str:
"""Die neueste Fassung laut makemkv.com. "" wenn die Seite nicht antwortet.
#
# ## Warum es hier eine Quellenkette gibt (28.08.2026)
#
# Der Commander: „bitte baue für MakeMKV fallback seiten ein."
#
# Anlass war ein echter Ausfall. Am 28.08.2026 gemessen:
#
# https://www.makemkv.com/download/ HTTP 525
# https://makemkv.com/download/ HTTP 525
# http://www.makemkv.com/download/ HTTP 403
# https://www.makemkv.com/ HTTP 525
# https://forum.makemkv.com/ HTTP 200 <- lebt
#
# 525 heißt: Cloudflare erreicht den Ursprungsserver nicht. Das ist eine
# Störung beim Hersteller, nichts, was Rippy reparieren könnte — und
# dieselbe, die im Projekt schon einmal jeden Worker-Build lahmgelegt hat.
#
# ## Woher die Version kommt, wenn die Seite schweigt
#
# Das Forum liegt auf einem ANDEREN Host und war erreichbar. Im
# Ankündigungs-Bereich steht die aktuelle Fassung; am 28.08.2026 gemessen:
# `1.18.4` — dieselbe, die auf dem Rechner des Commanders installiert war.
#
# ## Woher die DATEI kommt
#
# Aus dem Internet Archive, das eine Kopie der Datei DES HERSTELLERS hält.
# Gemessen:
#
# web/2024id_/…/Setup_MakeMKV_v1.18.4.exe 200, 16.432.607 Bytes
# web/2025id_/… 200, dieselbe Datei
# web/3000id_/… 525 <- leitet auf die tote Seite
# web/id_/… HTML statt Programm
#
# Ein Zeitstempel in der VERGANGENHEIT ist nötig: Ohne ihn holt das Archiv
# die Datei live nach — und die Quelle ist ja gerade tot.
#
# ## Warum das trotzdem sicher ist
#
# Weil nachgesehen wird, WAS ankommt. Der naheliegende Schutz — die digitale
# Signatur — geht nicht: MakeMKV signiert seinen Installer nicht
# (`Get-AuthenticodeSignature` → `NotSigned`, an der echten Datei gemessen).
# Geprüft wird deshalb die Versions-Ressource, ebenfalls gemessen:
#
# CompanyName GuinpinSoft inc
# FileDescription MakeMKV installer
# FileVersion v1.18.4
#
# Das ersetzt keine Signatur — wer die Datei fälscht, fälscht auch die
# Ressource. Es fängt aber zuverlässig ab, was hier wirklich droht: eine
# Fehlerseite, ein abgebrochener Download, eine falsche Version.
#
# ## Und die Reihenfolge ist Absicht
#
# Der Hersteller zuerst, immer. Das Archiv ist die Rückfallebene, nicht der
# Normalfall — und Rippy sagt hinterher, woher die Datei kam.
⚠️ Am 28.08.2026 antwortete makemkv.com mit HTTP 525 (Cloudflare), auch
mit Browser-Kennung. Das ist kein Fehler auf unserer Seite und auch nicht
reparierbar — deshalb gibt diese Funktion "" zurück statt zu werfen: Ein
nicht erreichbarer Update-Server ist kein Grund, warum Rippy nicht laufen
sollte.
MAKEMKV_SEITE = "https://www.makemkv.com/download/"
MAKEMKV_FORUM = "https://forum.makemkv.com/forum/viewforum.php?f=3"
# Der Pfad beim Hersteller — an EINER Stelle, damit die Archiv-Adresse ihn
# nicht abschreiben muss.
MAKEMKV_DATEI = "https://www.makemkv.com/download/Setup_MakeMKV_v%s.exe"
# Ein Zeitstempel in der Vergangenheit. Ohne ihn holt das Archiv live nach.
ARCHIV_STAND = "2025"
MAKEMKV_FIRMA = "GuinpinSoft"
MAKEMKV_PRODUKT = "MakeMKV"
VERSION_RE = r"MakeMKV\s+v?(\d+\.\d+\.\d+)"
def hoechste_version(text: str) -> str:
"""Die höchste Fassung, die in einem Text vorkommt. (reine Funktion)
Höchste und nicht erste: Das Forum listet alte Ankündigungen mit; die
neueste Fassung ist die mit der größten Nummer. Und verglichen wird nach
Zahlen, nicht nach Zeichen — sonst wäre „1.9.2" größer als „1.18.4".
"""
import re
try:
text = (seite if seite is not None
else _laden(MAKEMKV_SEITE).decode("utf-8", "replace"))
except (urllib.error.URLError, OSError):
funde = re.findall(VERSION_RE, text or "")
if not funde:
return ""
treffer = re.search(r"MakeMKV\s+v?(\d+\.\d+\.\d+)", text or "")
return treffer.group(1) if treffer else ""
return max(funde, key=lambda v: [int(t) for t in v.split(".")])
def versionsquellen() -> list:
"""Wo nach der aktuellen Fassung gesucht wird.
`massgeblich` heißt: Diese Quelle IST die Wahrheit — wer sie erreicht,
braucht nicht weiterzusuchen. Das gilt nur für den Hersteller selbst.
"""
return [
{"name": "Hersteller-Seite", "url": MAKEMKV_SEITE, "massgeblich": True},
{"name": "Hersteller-Forum", "url": MAKEMKV_FORUM, "massgeblich": False},
{"name": "Archiv der Hersteller-Seite", "massgeblich": False,
"url": "https://web.archive.org/web/%s/%s" % (ARCHIV_STAND, MAKEMKV_SEITE)},
]
def makemkv_neueste_version(seite: str = None, quellen: list = None) -> str:
"""Die neueste Fassung. "" wenn KEINE Quelle antwortet.
## Warum die HÖCHSTE gewinnt und nicht die erste (gemessen 28.08.2026)
Vier Läufe hintereinander:
Lauf 1: 1.18.4 (0,3 s) Forum hat geantwortet
Lauf 2: 1.18.4 (0,4 s)
Lauf 3: 1.18.2 (21,3 s) Forum lief in die Zeitgrenze
Lauf 4: 1.18.2 (21,0 s)
`forum.makemkv.com` antwortet unzuverlässig. Fällt es aus, gewinnt sonst
der ältere Archiv-Schnappschuss — und Rippy holt 1.18.2, obwohl 1.18.4
aktuell ist. „Neueste Fassung" wäre dann eine falsche Aussage.
Deshalb: Antwortet der HERSTELLER, ist die Sache entschieden. Sonst wird
zusammengetragen, was die übrigen Quellen melden, und die höchste Nummer
gewinnt — eine Quelle kann nur eine Fassung nennen, die es wirklich gab.
`seite` bleibt als Einspritzpunkt für Tests (fertiger HTML-Text).
"""
if seite is not None:
return hoechste_version(seite)
gefunden = []
for quelle in (quellen if quellen is not None else versionsquellen()):
try:
text = _laden(quelle["url"],
timeout=VERSIONS_TIMEOUT).decode("utf-8", "replace")
except (urllib.error.URLError, OSError):
continue
version = hoechste_version(text)
if not version:
continue
if quelle.get("massgeblich"):
return version
gefunden.append(version)
if not gefunden:
return ""
return max(gefunden, key=lambda v: [int(t) for t in v.split(".")])
def makemkv_setup_url(version: str, basis: str = None) -> str:
"""Die Adresse der Windows-Installationsdatei.
"""Die Adresse beim Hersteller (oder unter einer eigenen Basis).
Das Namensmuster stammt von makemkv.com selbst
(`Setup_MakeMKV_v<version>.exe`). `basis` ist übersteuerbar — aus
demselben Grund wie `MAKEMKV_URL_BASE` beim Docker-Bau: Wenn die
Hauptquelle klemmt, soll ein Ausweichweg möglich sein, ohne den Code
anzufassen.
`basis` ist übersteuerbar — aus demselben Grund wie `MAKEMKV_URL_BASE`
beim Docker-Bau: Wer eine eigene Kopie im Netz liegen hat, soll sie
benutzen können, ohne den Code anzufassen.
"""
basis = (basis or "https://www.makemkv.com/download").rstrip("/")
return f"{basis}/Setup_MakeMKV_v{version}.exe"
if basis:
return "%s/Setup_MakeMKV_v%s.exe" % (basis.rstrip("/"), version)
return MAKEMKV_DATEI % version
def makemkv_quellen(version: str, basis: str = None) -> list:
"""Alle Bezugsquellen für den Installer, in Reihenfolge.
Der Hersteller zuerst, IMMER. Eine eigene Basis steht davor — wer sie
einträgt, hat sich etwas dabei gedacht.
"""
quellen = []
if basis:
quellen.append(("eigene Quelle", makemkv_setup_url(version, basis)))
quellen.append(("Hersteller", MAKEMKV_DATEI % version))
quellen.append(("Internet Archive",
"https://web.archive.org/web/%sid_/%s"
% (ARCHIV_STAND, MAKEMKV_DATEI % version)))
return quellen
def pruefe_makemkv_datei(pfad: str, version: str = "") -> tuple:
"""Ist das WIRKLICH der MakeMKV-Installer? (ja/nein, Begründung)
Ohne diese Prüfung wäre eine Ausweichquelle leichtsinnig. Mit ihr wird
aus „von woanders geladen" ein „nachweislich die Datei des Herstellers".
Begründung vollständig in `rippy/platform/dateiangaben.py`.
"""
from rippy.platform import dateiangaben
if not dateiangaben.ist_programm(pfad):
return False, ('Die geladene Datei ist kein Windows-Programm '
'(sie beginnt nicht mit „MZ") — vermutlich eine '
'Fehlerseite.')
if os.name != "nt":
# Auf Linux gibt es keine Versions-Ressourcen. Der MZ-Test hat schon
# gegriffen; mehr ist dort nicht zu holen, und eine Prüfung, die man
# nicht durchführen kann, darf nicht als bestanden gelten — aber auch
# nicht als Fehlschlag.
return True, "Windows-Programm (Versionsangaben nur unter Windows lesbar)"
return dateiangaben.passt(dateiangaben.versionsangaben(pfad),
firma=MAKEMKV_FIRMA, produkt=MAKEMKV_PRODUKT,
version=version)
def makemkv_holen(version: str = "", basis: str = None, fortschritt=None,
still: bool = True) -> str:
still: bool = True, quellen: list = None) -> str:
"""Lädt MakeMKV und startet den offiziellen Installer.
**Rippy liefert MakeMKV NICHT mit** — es lädt die offizielle Datei vom
Hersteller und startet sie. Das ist dieselbe Black-Box-Trennung, die
`KONZEPT.md` § 6 für den Container festhält: MakeMKV bleibt ein fremdes
Programm, das der Nutzer bezieht; Rippy nimmt ihm nur die Handgriffe ab.
**Rippy liefert MakeMKV NICHT mit** — es lädt die offizielle Datei und
startet sie. Das ist dieselbe Black-Box-Trennung, die `KONZEPT.md` § 6
für den Container festhält: MakeMKV bleibt ein fremdes Programm, das der
Nutzer bezieht; Rippy nimmt ihm nur die Handgriffe ab.
Gibt den Pfad der heruntergeladenen Datei zurück.
Gibt den Pfad der heruntergeladenen Datei zurück. Wirft mit dem Bericht
ALLER Versuche, wenn keine Quelle geliefert hat — eine Meldung, die nur
„ging nicht" sagt, lässt den Nutzer ratlos zurück.
"""
def melden(text, anteil=None):
if fortschritt:
@@ -261,24 +462,48 @@ def makemkv_holen(version: str = "", basis: str = None, fortschritt=None,
version = version or makemkv_neueste_version()
if not version:
raise BeschaffungsFehler(
"Die Download-Seite von MakeMKV war nicht erreichbar, deshalb ist "
"unbekannt, welche Fassung aktuell ist. (Am 28.08.2026 antwortete "
"makemkv.com mit HTTP 525 — eine Störung auf deren Seite.) "
"Ausweg: MakeMKV von Hand von makemkv.com laden und installieren; "
"Rippy findet es danach von allein.")
"Keine der Quellen konnte sagen, welche Fassung aktuell ist "
"(Hersteller-Seite, Forum, Archiv). Ausweg: MakeMKV von Hand von "
"makemkv.com laden und installieren; Rippy findet es danach von "
"allein.")
ordner = os.path.join(katalog.werkzeug_ordner(), "downloads")
os.makedirs(ordner, exist_ok=True)
ziel = os.path.join(ordner, f"Setup_MakeMKV_v{version}.exe")
ziel = os.path.join(ordner, "Setup_MakeMKV_v%s.exe" % version)
vorlaeufig = ziel + ".neu"
melden(f"MakeMKV {version} wird geladen …", 0.0)
_datei_laden(makemkv_setup_url(version, basis), ziel, 0, melden)
fehlschlaege = []
for name, url in (quellen if quellen is not None
else makemkv_quellen(version, basis)):
melden("MakeMKV %s wird geladen (%s) …" % (version, name), 0.0)
try:
_datei_laden(url, vorlaeufig, 0, melden)
gut, grund = pruefe_makemkv_datei(vorlaeufig, version)
if not gut:
raise BeschaffungsFehler(grund)
except Exception as e: # noqa: BLE001
fehlschlaege.append("%s: %s" % (name, e))
melden(" %s hat nicht geliefert." % name)
try:
os.remove(vorlaeufig)
except OSError:
pass
continue
melden("Der Installer von MakeMKV wird gestartet …", 0.9)
# Erst tauschen, wenn die Datei geprüft ist. Ein halber Download darf
# eine gute Datei nicht ersetzen.
os.replace(vorlaeufig, ziel)
melden("Der Installer von MakeMKV wird gestartet (Quelle: %s) …" % name, 0.9)
starte_installer(ziel, still=still)
melden(f"MakeMKV {version} wird installiert.", 1.0)
melden("MakeMKV %s wird installiert." % version, 1.0)
return ziel
raise BeschaffungsFehler(
"Keine Quelle hat den MakeMKV-Installer geliefert:\n "
+ "\n ".join(fehlschlaege)
+ "\nAusweg: MakeMKV von Hand von makemkv.com laden und installieren; "
"Rippy findet es danach von allein.")
def starte_installer(pfad: str, still: bool = True) -> None:
"""Startet eine Installationsdatei — auf Wunsch ohne Rückfragen.
+113
View File
@@ -200,3 +200,116 @@ def test_werkzeug_ordner_liegt_dort_wo_geschrieben_werden_darf():
assert "tools" in ordner
if os.name == "nt":
assert "Program Files" not in ordner
# ── Die MakeMKV-Quellenkette ────────────────────────────────────────────
#
# Anlass: Commander am 28.08.2026 -- "bitte baue fuer MakeMKV fallback seiten
# ein." Gemessen an dem Tag:
#
# https://www.makemkv.com/download/ HTTP 525
# https://forum.makemkv.com/ HTTP 200 <- lebt
# web.archive.org/web/2025id_/...exe HTTP 200, 16.432.607 Bytes
def test_hoechste_version_statt_der_ersten():
"""Das Forum listet alte Ankuendigungen mit. Die neueste Fassung ist die
mit der GROESSTEN Nummer -- und verglichen wird nach Zahlen, nicht nach
Zeichen, sonst waere 1.9.2 groesser als 1.18.4."""
text = "MakeMKV v1.9.2 ... MakeMKV v1.18.4 ... MakeMKV v1.17.0"
assert beschaffen.hoechste_version(text) == "1.18.4"
def test_ohne_version_im_text_kommt_leer():
assert beschaffen.hoechste_version("nichts hier") == ""
assert beschaffen.hoechste_version("") == ""
assert beschaffen.hoechste_version(None) == ""
def test_der_hersteller_kommt_immer_zuerst():
"""Das Archiv ist die Rueckfallebene, nicht der Normalfall."""
quellen = beschaffen.makemkv_quellen("1.18.4")
assert [n for n, _ in quellen] == ["Hersteller", "Internet Archive"]
assert quellen[0][1].startswith("https://www.makemkv.com/")
def test_eine_eigene_quelle_steht_ganz_vorn():
"""Wer sie eintraegt, hat sich etwas dabei gedacht."""
quellen = beschaffen.makemkv_quellen("1.18.4", basis="https://nas/mkv")
assert quellen[0][0] == "eigene Quelle"
assert quellen[0][1] == "https://nas/mkv/Setup_MakeMKV_v1.18.4.exe"
def test_die_archiv_adresse_hat_einen_zeitstempel():
"""web/3000id_/ und web/id_/ holen die Datei LIVE nach -- und die Quelle
ist ja gerade tot. Am 28.08.2026 gemessen: 3000id_ gab 525, id_ gab HTML
statt Programm."""
_, url = beschaffen.makemkv_quellen("1.18.4")[-1]
assert "web.archive.org" in url
assert "id_/" in url
assert beschaffen.ARCHIV_STAND in url
assert url.endswith("Setup_MakeMKV_v1.18.4.exe")
def test_versionsquellen_haben_alle_drei_wege():
quellen = beschaffen.versionsquellen()
assert len(quellen) == 3
urls = [q["url"] for q in quellen]
assert any("forum.makemkv.com" in u for u in urls), "das Forum lebte als einziges"
assert any("web.archive.org" in u for u in urls)
# Genau EINE Quelle ist massgeblich: der Hersteller selbst.
assert [q["massgeblich"] for q in quellen] == [True, False, False]
def test_eine_tote_quelle_wird_uebersprungen(monkeypatch):
"""Der eigentliche Zweck der Kette."""
gefragt = []
def laden(url, timeout=20):
gefragt.append(url)
if "makemkv.com/download" in url and "archive" not in url:
raise OSError("HTTP 525")
return b"MakeMKV v1.18.4"
monkeypatch.setattr(beschaffen, "_laden", laden)
assert beschaffen.makemkv_neueste_version() == "1.18.4"
assert len(gefragt) >= 2, "die zweite Quelle wurde nie gefragt"
def test_ein_alter_archiv_stand_gewinnt_NICHT(monkeypatch):
"""DER gemessene Fall vom 28.08.2026: forum.makemkv.com antwortet
unzuverlaessig. Vier Laeufe hintereinander:
1.18.4 (0,3s) 1.18.4 (0,4s) 1.18.2 (21,3s) 1.18.2 (21,0s)
Lief das Forum in die Zeitgrenze, gewann der aeltere Archiv-Schnappschuss
-- und Rippy haette 1.18.2 geholt, obwohl 1.18.4 aktuell ist."""
def laden(url, timeout=20):
if "forum" in url:
return b"MakeMKV v1.18.4"
if "archive" in url:
return b"MakeMKV v1.18.2 MakeMKV v1.18.1"
raise OSError("HTTP 525")
monkeypatch.setattr(beschaffen, "_laden", laden)
assert beschaffen.makemkv_neueste_version() == "1.18.4"
def test_der_hersteller_beendet_die_suche(monkeypatch):
"""Antwortet er, ist die Sache entschieden -- auch wenn irgendwo eine
hoehere Nummer herumliegt. Er IST die Wahrheit."""
gefragt = []
def laden(url, timeout=20):
gefragt.append(url)
return b"MakeMKV v1.18.4"
monkeypatch.setattr(beschaffen, "_laden", laden)
assert beschaffen.makemkv_neueste_version() == "1.18.4"
assert len(gefragt) == 1, "nach der massgeblichen Quelle wurde weitergesucht"
def test_wenn_KEINE_quelle_antwortet_kommt_leer(monkeypatch):
def platzt(url, timeout=20):
raise OSError("weg")
monkeypatch.setattr(beschaffen, "_laden", platzt)
assert beschaffen.makemkv_neueste_version() == ""