feat(tools): Ausweichquellen fuer MakeMKV — recherchiert und gemessen
Ampel / ampel (push) Successful in 1m1s
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:
co-authored by
Claude Opus 5
parent
480b658294
commit
95beb91364
@@ -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.
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -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", "?"))
|
||||
@@ -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") == {}
|
||||
+268
-43
@@ -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": "*/*",
|
||||
})
|
||||
with urllib.request.urlopen(anfrage, timeout=timeout) as antwort:
|
||||
return antwort.read()
|
||||
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,23 +462,47 @@ 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)
|
||||
starte_installer(ziel, still=still)
|
||||
melden(f"MakeMKV {version} wird installiert.", 1.0)
|
||||
return ziel
|
||||
# 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("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:
|
||||
|
||||
@@ -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() == ""
|
||||
|
||||
Reference in New Issue
Block a user