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
|
Assistent kostet **null zusätzliche Bytes**. Die Seite lädt nichts aus dem
|
||||||
Netz; sie muss auf einem Rechner ohne Internet aufgehen.
|
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 shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -58,13 +59,24 @@ import zipfile
|
|||||||
from rippy.tools import katalog
|
from rippy.tools import katalog
|
||||||
|
|
||||||
HANDBRAKE_API = "https://api.github.com/repos/HandBrake/HandBrake/releases/latest"
|
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,
|
# 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.
|
# knapp für die Versionsabfrage — die soll das UI nicht aufhalten.
|
||||||
ABFRAGE_TIMEOUT = 20
|
ABFRAGE_TIMEOUT = 20
|
||||||
DOWNLOAD_TIMEOUT = 600
|
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 —
|
# Eine HandBrakeCLI-ZIP ist rund 25 MB. Alles unter einem MB ist keine —
|
||||||
# sondern eine Fehlerseite, die mit HTTP 200 ausgeliefert wurde.
|
# sondern eine Fehlerseite, die mit HTTP 200 ausgeliefert wurde.
|
||||||
MINDESTGROESSE_BYTES = 1024 * 1024
|
MINDESTGROESSE_BYTES = 1024 * 1024
|
||||||
@@ -74,14 +86,38 @@ class BeschaffungsFehler(RuntimeError):
|
|||||||
"""Es hat nicht geklappt — mit einer Begründung, die man lesen kann."""
|
"""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={
|
anfrage = urllib.request.Request(url, headers={
|
||||||
# GitHub verlangt eine Kennung; ohne sie kommt 403.
|
# GitHub verlangt eine Kennung; ohne sie kommt 403.
|
||||||
"User-Agent": "Rippy/2.0 (+https://github.com/)",
|
"User-Agent": "Rippy/2.0 (+https://github.com/)",
|
||||||
"Accept": "*/*",
|
"Accept": "*/*",
|
||||||
})
|
})
|
||||||
with urllib.request.urlopen(anfrage, timeout=timeout) as antwort:
|
letzter = None
|
||||||
return antwort.read()
|
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 ───────────────────────────────────────────────────────────
|
# ── HandBrake ───────────────────────────────────────────────────────────
|
||||||
@@ -210,49 +246,214 @@ def _datei_laden(url: str, ziel: str, erwartet: int, melden) -> None:
|
|||||||
|
|
||||||
|
|
||||||
# ── MakeMKV ─────────────────────────────────────────────────────────────
|
# ── 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
|
MAKEMKV_SEITE = "https://www.makemkv.com/download/"
|
||||||
mit Browser-Kennung. Das ist kein Fehler auf unserer Seite und auch nicht
|
MAKEMKV_FORUM = "https://forum.makemkv.com/forum/viewforum.php?f=3"
|
||||||
reparierbar — deshalb gibt diese Funktion "" zurück statt zu werfen: Ein
|
|
||||||
nicht erreichbarer Update-Server ist kein Grund, warum Rippy nicht laufen
|
# Der Pfad beim Hersteller — an EINER Stelle, damit die Archiv-Adresse ihn
|
||||||
sollte.
|
# 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
|
import re
|
||||||
|
|
||||||
try:
|
funde = re.findall(VERSION_RE, text or "")
|
||||||
text = (seite if seite is not None
|
if not funde:
|
||||||
else _laden(MAKEMKV_SEITE).decode("utf-8", "replace"))
|
|
||||||
except (urllib.error.URLError, OSError):
|
|
||||||
return ""
|
return ""
|
||||||
treffer = re.search(r"MakeMKV\s+v?(\d+\.\d+\.\d+)", text or "")
|
return max(funde, key=lambda v: [int(t) for t in v.split(".")])
|
||||||
return treffer.group(1) if treffer else ""
|
|
||||||
|
|
||||||
|
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:
|
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
|
`basis` ist übersteuerbar — aus demselben Grund wie `MAKEMKV_URL_BASE`
|
||||||
(`Setup_MakeMKV_v<version>.exe`). `basis` ist übersteuerbar — aus
|
beim Docker-Bau: Wer eine eigene Kopie im Netz liegen hat, soll sie
|
||||||
demselben Grund wie `MAKEMKV_URL_BASE` beim Docker-Bau: Wenn die
|
benutzen können, ohne den Code anzufassen.
|
||||||
Hauptquelle klemmt, soll ein Ausweichweg möglich sein, ohne den Code
|
|
||||||
anzufassen.
|
|
||||||
"""
|
"""
|
||||||
basis = (basis or "https://www.makemkv.com/download").rstrip("/")
|
if basis:
|
||||||
return f"{basis}/Setup_MakeMKV_v{version}.exe"
|
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,
|
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.
|
"""Lädt MakeMKV und startet den offiziellen Installer.
|
||||||
|
|
||||||
**Rippy liefert MakeMKV NICHT mit** — es lädt die offizielle Datei vom
|
**Rippy liefert MakeMKV NICHT mit** — es lädt die offizielle Datei und
|
||||||
Hersteller und startet sie. Das ist dieselbe Black-Box-Trennung, die
|
startet sie. Das ist dieselbe Black-Box-Trennung, die `KONZEPT.md` § 6
|
||||||
`KONZEPT.md` § 6 für den Container festhält: MakeMKV bleibt ein fremdes
|
für den Container festhält: MakeMKV bleibt ein fremdes Programm, das der
|
||||||
Programm, das der Nutzer bezieht; Rippy nimmt ihm nur die Handgriffe ab.
|
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):
|
def melden(text, anteil=None):
|
||||||
if fortschritt:
|
if fortschritt:
|
||||||
@@ -261,23 +462,47 @@ def makemkv_holen(version: str = "", basis: str = None, fortschritt=None,
|
|||||||
version = version or makemkv_neueste_version()
|
version = version or makemkv_neueste_version()
|
||||||
if not version:
|
if not version:
|
||||||
raise BeschaffungsFehler(
|
raise BeschaffungsFehler(
|
||||||
"Die Download-Seite von MakeMKV war nicht erreichbar, deshalb ist "
|
"Keine der Quellen konnte sagen, welche Fassung aktuell ist "
|
||||||
"unbekannt, welche Fassung aktuell ist. (Am 28.08.2026 antwortete "
|
"(Hersteller-Seite, Forum, Archiv). Ausweg: MakeMKV von Hand von "
|
||||||
"makemkv.com mit HTTP 525 — eine Störung auf deren Seite.) "
|
"makemkv.com laden und installieren; Rippy findet es danach von "
|
||||||
"Ausweg: MakeMKV von Hand von makemkv.com laden und installieren; "
|
"allein.")
|
||||||
"Rippy findet es danach von allein.")
|
|
||||||
|
|
||||||
ordner = os.path.join(katalog.werkzeug_ordner(), "downloads")
|
ordner = os.path.join(katalog.werkzeug_ordner(), "downloads")
|
||||||
os.makedirs(ordner, exist_ok=True)
|
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)
|
fehlschlaege = []
|
||||||
_datei_laden(makemkv_setup_url(version, basis), ziel, 0, melden)
|
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
|
||||||
starte_installer(ziel, still=still)
|
# eine gute Datei nicht ersetzen.
|
||||||
melden(f"MakeMKV {version} wird installiert.", 1.0)
|
os.replace(vorlaeufig, ziel)
|
||||||
return 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:
|
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
|
assert "tools" in ordner
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
assert "Program Files" not in ordner
|
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