perf+cleanup(api): /capabilities war 1,0 s; vier tote Endpunkte entfernt
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
Beides in main.py, deshalb ein Commit.
## 1. Das "Laggen" hatte genau eine Ursache
Gemessen ueber alle 15 Endpunkte, die das UI beim Laden braucht:
/capabilities 1,010 s
/system/updates 0,491 s (haengt am Knopf, nicht am Seitenaufbau)
/metadata/status 0,412 s (dito)
die anderen 12 < 0,025 s
/capabilities ist der einzige langsame, der beim SEITENAUFBAU zuschlaegt - und
fuenf Stellen holen ihn (Dashboard, Einstellungen, Worker-Tab, Wizard,
Rip-Dialog). Jede Seite zahlte eine Sekunde.
Die Ursache ist kein Fehler, sondern das Wesen des Celery-Pings: er sammelt
Antworten bis zum Timeout und kann nicht frueher aufhoeren, weil er nicht
weiss, wie viele Worker noch antworten wollen. Den Timeout zu kuerzen wuerde
Antworten langsamer Remote-Worker verschlucken - also genau die Maschinen, um
die es beim externen Encoding geht.
Jetzt pingt eine Hintergrund-Schleife im 5-s-Takt (neben Disc-Watcher und
Key-Refresh, die es dort schon gibt), der Endpunkt liest nur ab. Vorrat aelter
als 30 s - Schleife noch nicht angelaufen oder gestorben - dann EINMAL synchron
pingen: lieber langsam als falsch ("alles offline", obwohl alles laeuft).
## 2. Vier tote Endpunkte raus
Jeder ein Ueberrest eines ersetzten Entwurfs, keiner mit Aufrufer (mechanisch
gegengeprueft: alle api.*-Aufrufe des UI gegen alle Routen):
POST /prescan Metadaten-Vorschau-Seite ist seit v3.4 weg.
Die PreScan-Klasse bleibt - sie hat 5 echte
Fundstellen, der Watcher ruft sie im Prozess.
POST /jellyfin/format Macht seit v3.2 der Worker (medien.py), und
zwar an der richtigen Stelle: er kennt den
Ausgabeordner und ist nach dem Rip am Zug.
Mit ihm fallen nfo_generator.py und
image_downloader.py weg (sonst unbenutzt).
GET /stream/jobs Der unangenehmste: erst Placebo, am 23.07.
"repariert" statt entfernt - aber ein
EventSource im UI gab es nie (das Dashboard
nutzt setInterval(..., 4000)). Also keine
harmlose Leiche, sondern eine Endlosschleife
je Verbindung, die jeder aufmachen konnte.
GET /worker-setup/windows-gui Ohne Aufrufer seit die .exe den .bat-Umweg
ersetzt hat (v3.9). install-gui.ps1 selbst
lebt weiter, sie steckt in der .exe.
main.py: 1726 -> 1682 Zeilen, dazu 279 Zeilen in zwei geloeschten Modulen.
Tests halten beide Seiten fest: die vier Routen muessen WEG bleiben, und die
drei, an denen die Worker-Installation haengt (/worker-setup/paket, /windows,
/windows-exe), muessen DA sein. Ausserdem eine Doppelung entfernt - mein
eigener _sicherer_dateiname-Test aus dem Vorcommit pruefte dasselbe wie der
bestehende test_dateiname_validierung_blockt_pfad_tricks, und der war die
ganze Zeit korrekt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ def test_main_importierbar_und_routen_verdrahtet():
|
||||
|
||||
routen = {route.path for route in app.routes}
|
||||
for pfad in (
|
||||
"/health", "/jobs", "/devices", "/logs", "/settings", "/prescan",
|
||||
"/health", "/jobs", "/devices", "/logs", "/settings",
|
||||
# KEYDB.cfg + AACS-Dumps: der Platz für die selbst mitgebrachte
|
||||
# Schlüsseldatei (Befund 25.07.2026 — MakeMKV holt UHD-Schlüssel nicht
|
||||
# mehr online nach). Ohne diese Routen ist die Seite im UI tot.
|
||||
@@ -85,14 +85,75 @@ def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
|
||||
assert unter_wurzel("/app/media/movies", "/app/media/") is True
|
||||
|
||||
|
||||
def test_sicherer_dateiname_blockt_pfad_tricks():
|
||||
from main import _sicherer_dateiname
|
||||
def test_ping_vorrat_verhindert_die_wartesekunde(monkeypatch):
|
||||
"""Gemessen 25.07.2026: /capabilities brauchte 1,010 s - jedes Mal. Der
|
||||
Celery-Ping sammelt Antworten bis zum Timeout und kann nicht frueher
|
||||
aufhoeren. Fuenf UI-Stellen holen /capabilities, also zahlte jede Seite
|
||||
eine Sekunde, waehrend alle anderen Endpunkte unter 25 ms lagen.
|
||||
|
||||
assert _sicherer_dateiname("film.mkv") is True
|
||||
assert _sicherer_dateiname("../../etc/passwd") is False
|
||||
assert _sicherer_dateiname("a/b.mkv") is False
|
||||
# Raw-String: "a\b.mkv" waere ein Backspace-Zeichen und gar kein Backslash —
|
||||
# der Test war deshalb erst gruen, obwohl er nichts prueft (Ampel 19:10).
|
||||
assert _sicherer_dateiname(r"a\b.mkv") is False
|
||||
assert _sicherer_dateiname(".versteckt") is False
|
||||
assert _sicherer_dateiname("") is False
|
||||
Der Vorrat muss deshalb abgelesen und NICHT neu gepingt werden, solange er
|
||||
frisch ist - und bei altem Vorrat lieber einmal langsam als falsch.
|
||||
"""
|
||||
import time as _t
|
||||
|
||||
import main
|
||||
|
||||
pings = []
|
||||
monkeypatch.setattr(main, "_ping_jetzt", lambda: pings.append(1) or ["celery@neu"])
|
||||
|
||||
# Frischer Vorrat -> ablesen, kein Ping
|
||||
monkeypatch.setitem(main._PING, "knoten", ["celery@alt"])
|
||||
monkeypatch.setitem(main._PING, "stand", _t.monotonic())
|
||||
assert main._ping_knoten() == ["celery@alt"]
|
||||
assert pings == [], "bei frischem Vorrat darf NICHT gepingt werden"
|
||||
|
||||
# Zu alter Vorrat -> einmal synchron pingen
|
||||
monkeypatch.setitem(main._PING, "stand", _t.monotonic() - main.PING_ALTER_MAX_SEKUNDEN - 1)
|
||||
assert main._ping_knoten() == ["celery@neu"]
|
||||
assert len(pings) == 1
|
||||
|
||||
# Kalter Start (nie gepingt) -> ebenfalls pingen, nicht "alles offline" melden
|
||||
monkeypatch.setitem(main._PING, "stand", -1e9)
|
||||
main._ping_knoten()
|
||||
assert len(pings) == 2
|
||||
|
||||
|
||||
def test_ping_takt_ist_kuerzer_als_die_haltbarkeit():
|
||||
"""Sonst laeuft der Vorrat zwischen zwei Hintergrund-Laeufen ab und der
|
||||
Endpunkt pingt doch wieder synchron."""
|
||||
import main
|
||||
|
||||
assert main.PING_INTERVALL_SEKUNDEN < main.PING_ALTER_MAX_SEKUNDEN
|
||||
|
||||
|
||||
def test_tote_routen_sind_und_bleiben_weg():
|
||||
"""Entfernt am 25.07.2026, jede ein Ueberrest eines ersetzten Entwurfs und
|
||||
ohne einen einzigen Aufrufer (mechanisch gegengeprueft: alle api.*-Aufrufe
|
||||
des UI gegen alle Routen).
|
||||
|
||||
Der Test haelt sie draussen. /stream/jobs ist der Grund fuer diese
|
||||
Absicherung: er war schon einmal ein Placebo, wurde dann "repariert" statt
|
||||
entfernt - und war danach eine Endlosschleife je Verbindung ohne jeden
|
||||
Verbraucher. Wer echtes Push will, braucht BEIDE Seiten (Server UND ein
|
||||
EventSource im UI).
|
||||
"""
|
||||
from main import app
|
||||
|
||||
routen = {route.path for route in app.routes}
|
||||
for pfad in ("/prescan", "/jellyfin/format", "/stream/jobs",
|
||||
"/worker-setup/windows-gui"):
|
||||
assert pfad not in routen, (
|
||||
f"{pfad} ist wieder da — entweder mit Verbraucher (dann diesen Test "
|
||||
"anpassen) oder versehentlich (dann wieder raus)"
|
||||
)
|
||||
|
||||
|
||||
def test_worker_setup_routen_die_gebraucht_werden_sind_da():
|
||||
"""Die Installer holen sich Code und .exe hierueber — /worker-setup/paket
|
||||
ruft install.ps1 UND install-gui.ps1 auf, /windows-exe der UI-Knopf."""
|
||||
from main import app
|
||||
|
||||
routen = {route.path for route in app.routes}
|
||||
for pfad in ("/worker-setup/paket", "/worker-setup/windows",
|
||||
"/worker-setup/windows-exe"):
|
||||
assert pfad in routen, f"Route {pfad} fehlt — Worker-Installation kaputt"
|
||||
|
||||
Reference in New Issue
Block a user