fix(windows): Drueberinstallieren, Linux-Reste im Windows-Betrieb, Ordner-Waehler
Ampel / ampel (push) Successful in 1m27s
Ampel / ampel (push) Successful in 1m27s
Vier Fragen des Commanders vom 29.08.2026, drei davon mit Codefolge.
## 1. „Was passiert wenn man die Setup.exe einfach drueber installiert?"
Bis hierher: nicht zuverlaessig. Rippy startet mit Windows, laeuft also fast
immer — dann ist `Rippy.exe` gesperrt, `shutil.copy2` warf PermissionError,
und die neue Fassung landete als `Rippy.exe.neu` daneben. Dazu die Meldung
„wird beim naechsten Start uebernommen".
**Diese Zusage hat niemand eingeloest.** `.neu` kam im ganzen Projekt genau
einmal vor: an der Stelle, die es schrieb. Wer drueberinstallierte, behielt
still die alte Fassung, und das Setup meldete Erfolg.
Jetzt wird der laufende Rippy vorher beendet (`dienst_beenden` gibt es seit
der Deinstallation und wartet auch die zwei Sekunden ab, die Windows fuer die
Dateihandles braucht). Eine von einer aelteren Setup-Fassung liegengelassene
`.neu` wird dabei uebernommen. Bleibt die Datei DANN noch gesperrt, gibt es
einen klaren Fehler statt einer Zusage — Rippy im Infobereich beenden und das
Setup erneut starten.
Der Tausch laeuft bewusst im SETUP und nicht beim Dienststart: Windows sperrt
eine laufende .exe, und `Rippy.exe` waere genau die zu ersetzende Datei.
## 3. Linux-Reste im Windows-Betrieb (Docker/Headless unveraendert)
**`caps.py`: `os.path.isdir("/app")`.** Damit hielt sich der eigenstaendige
Windows-Rippy fuer einen FREMDEN Worker — und das UI warnte vor fehlender
Pfad-Uebersetzung auf einer Maschine ohne Container und ohne Freigabe.
„Extern" heisst jetzt, was es meint: Rippy laeuft woanders als dieser Worker.
**`caps.py`: `shutil.which("makemkvcon")` + `os.path.ismount(daten_dir)`.**
Beide unter Windows immer falsch (Programme liegen nicht im PATH, ein
normaler Ordner ist kein Mount). Die Schluessel-Auskunft blieb dauerhaft
„unbekannt", obwohl MakeMKV samt Datenverzeichnis da war. Der Mount-Test
bleibt fuer den Container, wo er einen Zweck hat.
**`rohdaten.py`: `/app/temp/raw` und `/app/media` fest.** Dieses Modul findet
die Rohdaten eines Jobs wieder — fuer den Wiederholen-Dialog und fuer
„Rohdaten mitloeschen". Unter Windows fand es NIE etwas: Der Dialog meldete
„keine Rohdaten", das Aufraeumen loeschte nichts, und die Bruchstuecke eines
abgebrochenen Rips blieben liegen (bei 4K-UHD bis 100 GB).
Sieben Tests wurden dabei rot, und zwar zu Recht: Sie pruefen Container-Regeln,
liefen aber unter Windows. Die Wurzeln sind jetzt einspritzbar — beide
Betriebsfaelle auf jedem Rechner pruefbar statt vom laufenden abhaengig.
## 4. „Der Durchsuchen button fehlt. Wie es der Installer auch macht"
Neu: `OrdnerWaehler` — Pfadfeld plus „Durchsuchen …", benutzt fuer Ablage und
Arbeitsverzeichnis. Es waere der DRITTE fest eingebaute Ordner-Browser
geworden (RipTargetModal, StorageMounts); dieser hier ist wiederverwendbar.
`/browse` weiss seit dem 28.08. selbst, in welchem Betrieb es laeuft.
⚠️ Beim Einbau fiel der Import unter den Tisch. `vite` pruefte das NICHT — das
Buendel blieb byte-gleich gross, und zur Laufzeit waere es der naechste leere
Bildschirm gewesen. Aufgefallen nur, weil die erwartete Anzahl Ersetzungen
nicht stimmte. Im Browser gegengeprueft: alle sieben Laufwerke, Navigation in
D:\, keine Konsolenfehler.
843 Tests gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
21b1757aa4
commit
b2acddbdfa
+51
-13
@@ -41,15 +41,44 @@ Job-ID (vollständige UUID), fertige Ablagen heißen `Titel (Jahr) [kurz-id]`.
|
||||
import posixpath
|
||||
import subprocess
|
||||
|
||||
# Container-Standard für Roh-Rips (RAW_DIR im Worker).
|
||||
# Container-Standard für Roh-Rips (RAW_DIR im Worker). Bleibt als Rueckfall
|
||||
# stehen — die WURZELN dieses Betriebs liefert `wurzeln()`.
|
||||
RAW_STANDARD = "/app/temp/raw"
|
||||
MEDIA_ROOT = "/app/media"
|
||||
|
||||
|
||||
def wurzeln(werte=None) -> tuple:
|
||||
"""`(roh_standard, medien_wurzel, frei)` fuer DIESEN Betrieb.
|
||||
|
||||
## Warum das nicht fest sein darf (Befund 29.08.2026)
|
||||
|
||||
Dieses Modul findet die Rohdaten eines Jobs wieder — fuer den
|
||||
Wiederholen-Dialog („auf der Platte liegen X GB Rohdaten") und fuer
|
||||
„Rohdaten mitloeschen". Es suchte fest unter `/app/temp/raw` und
|
||||
`/app/media`.
|
||||
|
||||
Auf einem Windows-PC gibt es beides nicht. Also fand es NIE etwas: Der
|
||||
Dialog meldete „keine Rohdaten", das Aufraeumen loeschte nichts, und die
|
||||
Bruchstuecke eines abgebrochenen Rips blieben unbemerkt liegen — bei einer
|
||||
4K-UHD bis zu 100 GB.
|
||||
"""
|
||||
from rippy import betrieb, config
|
||||
|
||||
if werte is None:
|
||||
try:
|
||||
werte = config.laden()
|
||||
except Exception: # noqa: BLE001
|
||||
werte = {}
|
||||
return (betrieb.arbeits_vorgabe(werte) or RAW_STANDARD,
|
||||
betrieb.medien_wurzel(werte) or MEDIA_ROOT,
|
||||
betrieb.frei_blaettern(werte))
|
||||
|
||||
# Harte Obergrenze für EINE Verzeichnis-Prüfung. Siehe verzeichnis_da().
|
||||
PRUEF_TIMEOUT_SEKUNDEN = 4
|
||||
|
||||
|
||||
def kandidaten(job_id: str, work_dir: str, media_unterordner) -> list:
|
||||
def kandidaten(job_id: str, work_dir: str, media_unterordner,
|
||||
orte_wurzeln=None) -> list:
|
||||
"""Alle Orte, an denen die Roh-MKVs dieses Jobs liegen KÖNNTEN (pure).
|
||||
|
||||
`media_unterordner` sind die Namen der obersten Ebene unter /app/media
|
||||
@@ -65,13 +94,18 @@ def kandidaten(job_id: str, work_dir: str, media_unterordner) -> list:
|
||||
"""
|
||||
if not job_id:
|
||||
return []
|
||||
orte = [posixpath.join(RAW_STANDARD, job_id)]
|
||||
wahl = (work_dir or "").strip().rstrip("/")
|
||||
if wahl and (wahl == MEDIA_ROOT or wahl.startswith(MEDIA_ROOT + "/")):
|
||||
orte.append(posixpath.join(wahl, job_id))
|
||||
roh, medien, frei = orte_wurzeln or (RAW_STANDARD, MEDIA_ROOT, False)
|
||||
from rippy import pfade
|
||||
|
||||
orte = [pfade.verbinden(roh, job_id)]
|
||||
wahl = (work_dir or "").strip().rstrip("/\\")
|
||||
# Nativ zaehlt jede Wahl — dort liegt der Arbeitsordner oft auf einem
|
||||
# ganz anderen Laufwerk und damit unter gar keiner Wurzel.
|
||||
if wahl and (frei or wahl == medien or wahl.startswith(medien + "/")):
|
||||
orte.append(pfade.verbinden(wahl, job_id))
|
||||
for name in media_unterordner or []:
|
||||
if name:
|
||||
orte.append(posixpath.join(MEDIA_ROOT, name, job_id))
|
||||
orte.append(pfade.verbinden(pfade.verbinden(medien, name), job_id))
|
||||
gesehen, eindeutig = set(), []
|
||||
for ort in orte:
|
||||
if ort not in gesehen:
|
||||
@@ -145,7 +179,8 @@ def verzeichnis_da(pfad: str, laufen=None) -> bool:
|
||||
return pruefen(pfad, laufen) == "da"
|
||||
|
||||
|
||||
def suche(job_id: str, work_dir: str, listdir, isdir) -> list:
|
||||
def suche(job_id: str, work_dir: str, listdir, isdir,
|
||||
orte_wurzeln=None) -> list:
|
||||
"""Die Orte, an denen wirklich etwas liegt.
|
||||
|
||||
`listdir` und `isdir` werden übergeben statt importiert — so ist die Suche
|
||||
@@ -156,12 +191,13 @@ def suche(job_id: str, work_dir: str, listdir, isdir) -> list:
|
||||
`listdir` darf os.listdir bleiben: Gelistet wird nur /app/media selbst, und
|
||||
das ist ein lokales Verzeichnis — die Freigaben sind Unterordner davon.
|
||||
"""
|
||||
orte_wurzeln = orte_wurzeln or wurzeln()
|
||||
try:
|
||||
unterordner = sorted(listdir(MEDIA_ROOT))
|
||||
unterordner = sorted(listdir(orte_wurzeln[1]))
|
||||
except OSError:
|
||||
unterordner = []
|
||||
gefunden = []
|
||||
for ort in kandidaten(job_id, work_dir, unterordner):
|
||||
for ort in kandidaten(job_id, work_dir, unterordner, orte_wurzeln):
|
||||
try:
|
||||
if isdir(ort):
|
||||
gefunden.append(ort)
|
||||
@@ -172,7 +208,8 @@ def suche(job_id: str, work_dir: str, listdir, isdir) -> list:
|
||||
return gefunden
|
||||
|
||||
|
||||
def suche_mit_status(job_id: str, work_dir: str, listdir, pruefer=None) -> dict:
|
||||
def suche_mit_status(job_id: str, work_dir: str, listdir, pruefer=None,
|
||||
orte_wurzeln=None) -> dict:
|
||||
"""Wie suche(), aber sagt auch, ob etwas UNGEPRÜFT geblieben ist.
|
||||
|
||||
Rückgabe: {"pfade": [...], "unklar": bool}. `unklar` heißt: Mindestens ein
|
||||
@@ -181,12 +218,13 @@ def suche_mit_status(job_id: str, work_dir: str, listdir, pruefer=None) -> dict:
|
||||
behalten, statt Abwesenheit zu behaupten (siehe pruefen()).
|
||||
"""
|
||||
pruefe = pruefer or pruefen
|
||||
orte_wurzeln = orte_wurzeln or wurzeln()
|
||||
try:
|
||||
unterordner = sorted(listdir(MEDIA_ROOT))
|
||||
unterordner = sorted(listdir(orte_wurzeln[1]))
|
||||
except OSError:
|
||||
unterordner = []
|
||||
gefunden, unklar = [], False
|
||||
for ort in kandidaten(job_id, work_dir, unterordner):
|
||||
for ort in kandidaten(job_id, work_dir, unterordner, orte_wurzeln):
|
||||
antwort = pruefe(ort)
|
||||
if antwort == "da":
|
||||
gefunden.append(ort)
|
||||
|
||||
Reference in New Issue
Block a user