Commit Graph

8 Commits

Author SHA1 Message Date
Hitonabi 11597eb00c perf+cleanup(api): /capabilities war 1,0 s; vier tote Endpunkte entfernt
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>
2026-07-25 21:41:48 +02:00
Hitonabi a1aabd591d fix(test): Ampel rot - der Backslash-Test prueft nichts
Ampel / ampel (push) Successful in 28s
Meine eigene Zeile aus dem vorigen Commit. Im Quelltext stand "a\b.mkv" mit
EINEM Backslash - Python liest \b als Backspace-Zeichen, der String enthaelt
also gar keinen Backslash, und _sicherer_dateiname gab korrekt True zurueck.
Der Test behauptete, den Backslash-Pfad zu pruefen, und tat es nicht.

Jetzt ein Raw-String r"a\b.mkv".

Warum das lokal nicht auffiel: test_api_smoke.py ueberspringt sich unter
Windows selbst (main.py -> detection.py -> fcntl). Genau die Luecke, die im
Savepoint als offener Punkt steht - hier hat sie sofort zugeschlagen. Lehre:
Tests, die nur in der Ampel laufen, sind erst nach dem Push bewiesen, und
Backslashes gehoeren in Raw-Strings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:12:39 +02:00
Hitonabi ef0a574a70 fix(worker,api): Platten-Schutz griff nicht, Fortschritt log, Auswurf tat nichts
Vier Funde aus der Durchsicht, alle auf der VM gemessen.

1. DER PLATTEN-SCHUTZ AUS c065967 WAR WIRKUNGSLOS

_original_aufheben() entschied per os.stat().st_dev, ob umgehaengt oder
kopiert werden muss. Im Worker-Container gemessen - beides gleichzeitig wahr:

    st_dev /app/temp  = 2050
    st_dev /app/media = 2050        → identisch
    os.rename(...)    → EXDEV, "Invalid cross-device link"

Der Kernel vergleicht bei rename() den MOUNT, nicht das Geraet. /app/temp
(Docker-Volume) und /app/media (Bind-Mount) sind zwei Mounts DERSELBEN
ext4-Partition. Die Pruefung sah "gleiches Dateisystem", uebersprang die
Platzpruefung, und shutil.move kopierte doch - 75 GB bei 37 GB frei. Der
Schutz haette genau den Schaden zugelassen, gegen den er gebaut wurde.

Jetzt wird os.rename VERSUCHT statt vorhergesagt: klappt es, ist es
umgehaengt und fertig; kommt EXDEV, steht die Kopie fest und ERST DANN wird
der Platz geprueft. Das ist keine Vermutung mehr, sondern die Antwort des
Kernels. Vier Tests in test_original_aufheben.py, darunter genau der Fall,
der die Platte fuellte. Die zwei alten Tests in test_medien.py sind dorthin
gewandert - sie taeuschten per gefaelschtem os.stat "verschiedene
Dateisysteme" vor, also genau die Annahme, an der der Schutz scheiterte.

2. DIE FORTSCHRITTSANZEIGE ZEIGTE DEN SCAN, NICHT DEN ENCODE

get_progress_from_line matchte jede Zahl vor einem Prozentzeichen. HandBrake
gibt Prozente aber in drei Phasen aus (Formatstrings aus dem Binary gelesen):

    Scanning title %d of %d, preview %d, %.2f %%          → laeuft VOR dem
                                                            Encode bis 100 %
    Encoding: task %d of %d, %.2f %%       (%.2f fps, avg  → der echte Wert
    Encoding: task %d of %d, Searching for start time, ... → Vorlauf

Dazu warf `if progress > 0` im Aufrufer jeden Wert unter 1,00 % weg. Live
beobachtet: Anzeige stand auf 99 %, der Encode bei 1,06 %; sie fiel erst auf
1, als der Encode die 1-%-Marke ueberschritt. Jetzt wird nur die
Encoding-Zeile gelesen, `task N of M` mitgerechnet (sonst springt die
Anzeige bei Zwei-Pass-Presets mitten in der Datei zurueck), und -1 heisst
"keine Angabe" - dasselbe Muster wie bei get_progress_from_prgv.

3. "AUTOMATISCHER AUSWURF" WURDE VON NIEMANDEM GELESEN

Die Einstellung (Standard: ein, "Disc nach erfolgreichem Ripping automatisch
auswerfen") kam in keiner Zeile Backend-Code vor. DVD/Blu-ray warfen deshalb
NIE aus, Audio-CDs IMMER, weil abcde `-x` fest verdrahtet bekam. Jetzt
entscheidet die Einstellung beides: wirf_disc_aus() per CDROMEJECT-ioctl
(fcntl-guarded, der native Windows-Worker laedt das Modul auch) und `-x` nur
noch, wenn gewuenscht.

4. PFAD-PRUEFUNG FIEL AUF PRAEFIX-NAMEN HEREIN

Elf Stellen prueften mit nacktem startswith(MEDIA_ROOT). "/app/media-boese/x"
beginnt mit "/app/media", liegt aber ausserhalb - betroffen waren auch
/browse und /browse/mkdir, wo der Pfad vom Nutzer kommt. Neuer
Zwillings-Helfer unter_wurzel() in api/main.py und worker/tasks.py, alle elf
Stellen umgestellt, Tests in beiden.

Nebenbefund: _zielbasis() benutzte os.path.normpath - unter Windows werden
daraus Backslashes, die MEDIA_ROOT-Pruefung greift nicht mehr, und das
gewaehlte Ziel faellt still auf den Standard zurueck. Genau die Falle, die
_arbeitsverzeichnis() drei Zeilen weiter dokumentiert und mit posixpath
vermeidet. Live war es nie (nur aus rip_disc, das auf Windows verriegelt
ist), jetzt konsistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:05:11 +02:00
Hitonabi f449c4ee34 fix(uhd): 4K-UHD geloest - makemkvcon holt Schluessel unter Linux nie
Ampel / ampel (push) Successful in 28s
Richtigstellung des Vortags-Befunds. Dort stand, MakeMKVs Schluessel-Kanal
sei abgeschaltet. Das war FALSCH: die Herleitung stuetzte sich auf zwei
Hostnamen aus alten Forumsbeitraegen (hkdata.fairuse.org,
hkdata.crabdance.com), die zwar wirklich nicht mehr aufloesen, von MakeMKV
aber laengst nicht mehr benutzt werden. Aufgedeckt durch den Einwand des
Commanders, unter Windows ginge es sofort.

Gegenprobe mit demselben Laufwerk und derselben Disc (Akira UHD, MKB v76):

                        Linux (Worker)      Windows
  Verbindungen          KEINE EINZIGE       185.84.108.20:443
  Meldung 3338          nie                 "Downloading latest HK"
  _private_data.tar     2048 B, 0 Keys      6,4 MB, 604 Keys
  Disc                  volume key unknown  TCOUNT:5, geht auf

Gegengeprueft mit leerem UND gefuelltem Speicher, mit und ohne --noscan,
mit dev:/dev/sr0 und disc:0, mit geloeschter update.conf. Linux fragt nie.
Die Meldungsvorlage "Downloading latest %1 to %2 ..." steckt sehr wohl im
Linux-Binary - sie loest nur nicht aus. Gleiches Symptom im MakeMKV-Forum,
seit Jahren offen (t=25782, t=34022). Der Dienst lebt; der Worker erreicht
185.84.108.20:443 sogar problemlos.

BEWIESEN: Nach Uebernahme des Windows-Schluesselspeichers oeffnet
makemkvcon auf der VM die Akira-UHD - "Operation successfully completed",
TCOUNT:5, fuenf Titel, identisch zum Windows-Ergebnis. Erster belegter
UHD-Disc-Zugriff auf der Rippy-Maschine.

- makemkv_daten.py (beide Zwillinge): zaehle_schluessel,
  private_data_pruefen, schluesselspeicher_status, private_data_schreiben.
  Die Pruefung lehnt einen Speicher OHNE hkd_*.bin ab - sonst laedt jemand
  den leeren Vorrat einer frischen Installation hoch, nichts aendert sich,
  und niemand versteht warum. Modulkopf komplett neu, inkl. der
  Fehldiagnose als Warnung fuer spaeter.
- API: GET/POST /system/keystore. Der Rohkoerper der Anfrage IST die Datei
  (binaer - JSON/Base64 waere Ballast, Multipart kann die API nicht).
  Groessengrenze 64 MB = client_max_body_size in nginx.conf.
- UI: neuer Block "Disc-Schluessel fuer 4K-UHD" UEBER dem KEYDB-Block, mit
  Schluessel-Anzahl, Upload und Anleitung fuer den Windows-Weg. KEYDB.cfg
  ist jetzt als Notnagel beschriftet. Worker-Plakette zeigt die Anzahl;
  0 heisst sichtbar "4K-UHD scheitert".
- tasks.py: UHD-Fehlertext sagt den Windows-Weg an und nennt die Anzahl
  bekannter Schluessel dieses Workers.
- caps.py meldet schluessel je Worker.
- Alle Falschaussagen korrigiert: UI (3), Anleitung (2), README (3),
  KONZEPT §8 + §10, Worker-Dockerfile, makemkv_key.py (dort stand "Den
  AACS-Schluessel zieht MakeMKV via LibreDrive ohnehin selbst aus dem
  Laufwerk" - gilt fuer Blu-ray, NICHT fuer UHD).
- SAVEPOINT v3.11, ROADMAP Etappe 18 (Etappe 17 mit Nachtrag), AGENTS.

Offen: voller UHD-Rip inkl. Transcode-E2E; und ob sich der Abruf unter
Linux doch anstossen laesst.

Quellen (AGENTS Regel D):
- Linux laedt keine Hashed Keys, gleiches Symptom:
  https://forum.makemkv.com/forum/viewtopic.php?t=25782
  https://forum.makemkv.com/forum/viewtopic.php?t=34022
- Schluessel als hkd_*.bin in _private_data.tar:
  https://forum.makemkv.com/forum/viewtopic.php?t=32675
- Meldungsformat: https://www.makemkv.com/developers/usage.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 11:18:09 +02:00
Hitonabi 0935766f61 feat(uhd): KEYDB.cfg-Unterstuetzung - MakeMKVs Schluessel-Kanal liefert nichts mehr
Ampel / ampel (push) Successful in 28s
4K-UHD scheiterte an "The volume key is unknown for this disc". Am 25.07.2026
im Worker-Container nachgemessen: Laufwerk und MakeMKV sind in Ordnung
(LibreDrive v06.3 / Meldung 1011, Disc wird gelesen, AACS-Dump geschrieben) -
MakeMKV versucht gar nicht erst, online einen Schluessel zu holen. Belege:
_private_data.tar enthaelt nur die Index-Datei und KEINE hkd_*.bin; auch mit
geloeschter update.conf (Meldung 5074 belegt den Web-Kontakt) und mit
app_UpdateEnable="1" kam keiner; hkdata.fairuse.org und hkdata.crabdance.com
loesen weltweit nicht mehr auf (NXDOMAIN gegen Fritz!Box, 8.8.8.8, 1.1.1.1).
Betroffen war Akira UHD (MKB v76, Pressung Dez. 2020) - also gerade KEINE
Neuerscheinung. Der bisherige Fehlertext ("Disc neuer als die
Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war falsch.

Einziger heute funktionierender Weg ist eine vom Nutzer selbst mitgebrachte
KEYDB.cfg. Rippy liefert KEINE Schluessel mit, laedt keine herunter und
verteilt keine - es stellt nur den Platz bereit und zeigt an, was dort liegt.

- Datenverzeichnis persistent gemountet (MAKEMKV_DATA_HOST, Default
  /srv/rippy/makemkv): KEYDB.cfg und AACS-Dumps ueberleben jeden Rebuild.
  Vorher loeschte jeder "up -d --build" beides - inklusive des Dumps, auf den
  die Fehlermeldung selbst verwies.
- entrypoint.sh und tasks.py schreiben settings.conf ergaenzend statt
  zerstoerend. Der entrypoint bricht bei nicht beschreibbarem Verzeichnis
  nicht mehr ab - mit "restart: unless-stopped" waere das ein Crashloop
  gewesen, in dem auch reines DVD-Rippen tot ist.
- Neues Zwillings-Modul makemkv_daten.py (docker/api + docker/worker,
  byteweise identisch; test_zwillinge_sind_byteweise_identisch wacht darueber
  und wurde durch absichtliches Verstellen als wirksam nachgewiesen).
- API: GET/POST/DELETE /system/keydb, GET /system/aacs-dumps(/{dateiname}).
  JSON-Body statt Multipart - python-multipart ist bewusst nicht installiert
  und wuerde die API beim Import toeten. nginx client_max_body_size 64m,
  sonst scheitert der Upload mit 413, bevor die API ihn sieht.
- UI (Einstellungen -> System): Status, Hochladen per Datei-Dialog, Entfernen,
  Dump-Download, KEYDB-Plakette je Worker (nur wo das Verzeichnis wirklich
  gemountet ist - ein Remote-Transcode-Worker truege sonst eine Warnung,
  die ihn nichts angeht).
- parse_msg() + log_cb: MakeMKV-Meldungen landen im Rippy-Log (gedrosselt:
  Code 1003 raus, keine Wiederholungen, max. 40 je Rip). Nebenbei behoben:
  der alte Parser (split(",", 4)[3]) schnitt jede Meldung am ersten Komma ab.
- Fuenf Stellen richtiggestellt, die behaupteten, MakeMKV-Updates braechten
  die neueste Disc-Schluessel-Datenbank mit (UI, Anleitung, README,
  Worker-Dockerfile, makemkv_key.py).

NICHT bewiesen: ein erfolgreicher UHD-Rip - es lag keine KEYDB.cfg mit dem
Akira-Schluessel vor. Belegt sind der Befund und die neue Mechanik. So steht
es auch im SAVEPOINT und in der ROADMAP.

Quellen (AGENTS Regel D):
- Datenverzeichnis + Dateiname GROSS/case-sensitiv:
  https://forum.makemkv.com/forum/viewtopic.php?t=30636
- hkd_*.bin in _private_data.tar:
  https://forum.makemkv.com/forum/viewtopic.php?t=32675
- headless settings.conf / app_UpdateEnable:
  https://forum.makemkv.com/forum/viewtopic.php?t=20364
- KEYDB.cfg-Zeilenformat (libaacs):
  https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
- MSG-/PRGV-Format: https://www.makemkv.com/developers/usage.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 01:26:08 +02:00
Hitonabi 46a8f50c34 fix(api): remount nicht-blockierend (Netz-Mount darf API-Start nicht haengen)
Ampel / ampel (push) Successful in 28s
Vorfall 24.07.: Beim API-Start blockierte der synchrone CIFS-Schreibtest in
alle_remounten()/mounten() im Kernel (wait_for_response), als der SMB-Server
langsam war -> ~5 min "Waiting for application startup", kein Endpoint bedient
(bis der soft-Mount per Timeout abbrach). startup_event() lief isoliert sauber,
also war es der blockierende Netz-Mount, nicht die App-Logik.

Fix: remount als Hintergrund-Task (asyncio.create_task) statt await -> die API
kommt sofort hoch, die Mounts stellen sich her sobald der Server antwortet.
Test (test_api_smoke.py): haelt den Nicht-blockierend-Vertrag per Quelltext-
Inspektion fest, im Stil der anderen Verdrahtungs-Tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:50:32 +02:00
Hitonabi ab75134931 feat: Download-Knopf fuer fertige Rips — Dateien direkt im Browser statt scp
Ampel / ampel (push) Successful in 29s
- GET /jobs/{id}/files: Dateiliste aus job.output_path (Name + Groesse)
- GET /jobs/{id}/files/{name}: FileResponse-Stream; Validierung strikt —
  output_path muss unter /app/media liegen, nackter Dateiname (kein
  Slash/.., kein Dotfile), realpath-Check gegen Symlink-Ausbrueche.
  Mit Test (test_dateiname_validierung_blockt_pfad_tricks).
- UI: 'Download'-Knopf in der Aktion-Spalte bei fertigen Jobs; die
  Dateiliste mit Groessen + Download-Links lebt im Job-Detail-Popup
  (ein Dropdown wuerde im overflow-x-auto-Tabellencontainer clippen).
- nginx: proxy_buffering off + proxy_read_timeout 3600s waren fuer SSE
  schon gesetzt — grosse Downloads brauchen keine Aenderung.

Wunsch aus der Uebernahme-Session (Commander-Sammelliste 24.07.).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:01:38 +02:00
Hitonabi 28a12b2e06 API: Die Job-Kette existiert jetzt — POST /jobs, Watcher, Postgres, /logs
Vorher gab es KEINEN Code-Pfad, der je einen Rip ausgelöst hat: kein
POST /jobs, kein udev-Daemon (udev_daemon.py existierte nirgends), GET /jobs
gab hart [] zurück, Postgres lag komplett brach, der SSE-Stream konnte
strukturell nie senden (sse_connections wurde nie befüllt), udevadm lieferte
ohne udevd nichts.

- POST /jobs: legt Job-Zeile an, schickt worker.tasks.rip_disc via Celery
- GET /jobs aus Postgres (running→processing fürs UI)
- Disc-Watcher: 3s-ioctl-Poll statt udev, protokolliert Einwurf/Auswurf
- /devices über /sys (vendor/model) + ioctl-Status — ehrlich statt leer
- /logs + /settings (Settings-Seite sprach vorher gegen 404)
- SSE-Fix, udev aus dem API-Image entfernt, Import-Smoke-Test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:02:13 +02:00