fix(ui,transcode): Dialog-Falle, Arbeitsverzeichnis waehlbar, Original-Aufheben entschaerft
Ampel / ampel (push) Successful in 28s

Drei Befunde aus dem ersten echten UHD-Durchlauf (Akira). Der Rip und die
Kompression liefen sauber durch - danach hat "Original behalten" die Platte
vollgeschrieben und den fertigen Job als fehlgeschlagen markiert.

1. ORIGINAL-AUFHEBEN DARF DEN JOB NICHT MEHR TOETEN (tasks.py)
   Vorher stand dort ein nacktes shutil.move(raw_dir, ziel). Arbeits-
   verzeichnis (/app/temp, Docker-Volume) und Ziel (/app/media, Bind-Mount)
   sind VERSCHIEDENE Dateisysteme - os.rename scheitert mit EXDEV, shutil.move
   faellt auf Kopieren zurueck. Ergebnis am 25.07.: 74-GB-Vollkopie auf
   dieselbe Platte, Abbruch bei 41 GB mit ENOSPC, Platte 100 % voll, Worker-
   Container startete nicht mehr ("failed to mount: no space left on device"),
   und der Job galt als FEHLGESCHLAGEN - obwohl die komprimierte Datei
   (4,8 GB) fertig und in Ordnung war. Der Nutzer sah nur eine leere Queue.
   Jetzt: _original_aufheben() prueft erst, ob ueberhaupt kopiert werden muss
   (gleiches Dateisystem -> reines Umhaengen), prueft sonst den freien Platz
   VORHER, faengt jeden OSError ab, raeumt eine halbe Kopie weg und meldet das
   als WARNUNG. Der Job bleibt erfolgreich, die Roh-Datei bleibt liegen.
   Zwei Tests decken beide Wege ab.

2. ARBEITSVERZEICHNIS IST JETZT WAEHLBAR (Settings.tsx)
   Es war ein freies Textfeld - man musste den Container-Pfad (/app/media/...)
   KENNEN, um eine Netzwerk-Freigabe zu treffen. Genau daran ist es
   gescheitert, weshalb der 74-GB-Rohschnitt ueberhaupt erst auf der VM-Platte
   landete. Jetzt eine Auswahl aus /storage-targets (dieselbe Liste wie bei
   den Speicherzielen), inklusive Kennzeichnung als Netzwerk-Freigabe und
   freiem Platz je Ziel.

3. DIALOGE KLEBTEN IM PANEL (ui/Modal.tsx)
   "Rippen starten" im Laufwerke-Tab oeffnete den Dialog INNERHALB des
   Bereichs, teils abgeschnitten. Ursache: .glass-panel in index.css setzt
   backdrop-filter: blur(16px), und ein Element mit backdrop-filter wird zum
   Bezugsrahmen fuer position: fixed seiner Nachfahren - das Modal war damit
   an der Card ausgerichtet statt am Fenster. Modal rendert jetzt per
   createPortal an document.body. Behebt es fuer ALLE Dialoge auf einmal.

Aufgeraeumt: die abgebrochene 41-GB-Teilkopie unter
"/app/media/movies/Akira (1988)/original/" geloescht (nachweislich
unvollstaendig - 41 GB gegen 79,6 GB Quelle, Quelle intakt). Platte wieder
bei 78 %, Container laufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 19:48:10 +02:00
parent e84afc1718
commit c065967f4d
4 changed files with 168 additions and 10 deletions
+59 -3
View File
@@ -172,6 +172,64 @@ def _makemkv_key_anwenden(einstellungen: dict) -> None:
db.add_log("warning", "worker", f"MakeMKV-Key konnte nicht gesetzt werden: {e}")
def _ordner_groesse(pfad: str) -> int:
"""Belegter Platz eines Ordners in Bytes (0, wenn nicht lesbar)."""
summe = 0
for wurzel, _, dateien in os.walk(pfad):
for name in dateien:
try:
summe += os.path.getsize(os.path.join(wurzel, name))
except OSError:
pass
return summe
def _original_aufheben(job_id: str, raw_dir: str, final_dir: str) -> None:
"""Roh-Rip zusätzlich aufheben — darf den Job NIEMALS scheitern lassen.
Befund 25.07.2026 (Akira-UHD, echter Schaden): Hier stand ein nacktes
shutil.move(). Arbeitsverzeichnis (/app/temp, Docker-Volume) und Ziel
(/app/media, Bind-Mount) sind VERSCHIEDENE Dateisysteme — os.rename
scheitert dort mit EXDEV, und shutil.move fällt auf Kopieren zurück. Damit
lief eine 74-GB-Vollkopie auf dieselbe Platte an, bis sie mit ENOSPC voll
war. Ergebnis: Platte 100 % voll, Worker-Container startete nicht mehr —
und der Job wurde als FEHLGESCHLAGEN markiert, obwohl die komprimierte
Datei längst fertig und in Ordnung war. Der Nutzer sah nur „nichts da".
Deshalb dreifach abgesichert: Platz vorher prüfen, Fehler abfangen, und im
Zweifel die Rohdatei einfach liegen lassen, wo sie ist.
"""
ziel_original = os.path.join(final_dir, "original")
try:
# Liegt beides auf demselben Dateisystem, ist es ein reines Umhängen —
# sofort fertig, kein zusätzlicher Platz nötig.
gleiches_dateisystem = os.stat(raw_dir).st_dev == os.stat(final_dir).st_dev
if not gleiches_dateisystem:
benoetigt = _ordner_groesse(raw_dir)
frei = _frei_bytes(final_dir)
if frei < benoetigt * 1.05:
db.add_log(
"warning", "worker",
f"Job {job_id}: Original NICHT aufgehoben — dafür wären "
f"{benoetigt / 1024**3:.1f} GB nötig, frei sind nur "
f"{frei / 1024**3:.1f} GB. Die Roh-Datei bleibt unter "
f"{raw_dir} liegen. Abhilfe: Arbeitsverzeichnis "
"(Einstellungen → Verarbeitung) auf dieselbe Freigabe legen "
"wie das Ziel — dann wird nur umgehängt statt kopiert."
)
return
shutil.move(raw_dir, ziel_original)
db.add_log("info", "worker", f"Job {job_id}: Original behalten unter {ziel_original}")
except OSError as e:
# Halb geschriebene Kopie wegräumen, sonst belegt sie für immer Platz.
shutil.rmtree(ziel_original, ignore_errors=True)
db.add_log(
"warning", "worker",
f"Job {job_id}: Original konnte nicht aufgehoben werden ({e}). "
f"Die komprimierte Datei ist fertig; die Roh-Datei bleibt unter {raw_dir}."
)
def _abbruch_angefordert(job_id: str) -> bool:
"""Kooperativer Abbruch: hat der Nutzer über die API abgebrochen?"""
return db.get_job_status(job_id) == "canceling"
@@ -610,9 +668,7 @@ def transcode_files(self, job_id: str, raw_dir: str, final_dir: str):
return ergebnis
if original_behalten:
ziel_original = os.path.join(final_dir, "original")
shutil.move(raw_dir, ziel_original)
db.add_log("info", "worker", f"Job {job_id}: Original behalten unter {ziel_original}")
_original_aufheben(job_id, raw_dir, final_dir)
else:
shutil.rmtree(raw_dir, ignore_errors=True)