diff --git a/SAVEPOINT.md b/SAVEPOINT.md
index bfba13c..f2eec92 100644
--- a/SAVEPOINT.md
+++ b/SAVEPOINT.md
@@ -17,8 +17,15 @@
GitHub-Release (URL verifiziert), start-worker.bat (celery -Q transcode
--pool=solo), optional -Autostart (schtasks onlogon). tasks.py lädt auf
Windows ohne fcntl (Import-Guard); rip_disc ist dort hart verriegelt.
- **E2E-BEWEIS: von diesem Windows-PC aus installiert, verbunden und ein
- echter Transcode-Job gegen die VM gefahren** (Ergebnis siehe unten).
+ Für echte Transcodes übersetzt RIPPY_PATH_MAP die Container-Pfade aufs
+ Netzlaufwerk (pfad_lokal, mit Tests) — Voraussetzung bleibt eine
+ Freigabe der Rippy-Ablage (derselbe offene Infra-Punkt wie beim
+ Linux-Remote-Worker/AI-Box).
+ **E2E-BEWEIS (24.07., Commander-PC):** Installer in einem Rutsch
+ durchgelaufen (venv, Abhängigkeiten, HandBrakeCLI 1.9.2), Worker
+ gestartet und in Rippy erschienen:
+ `test-windows-nativ | online: True | HandBrake: 1.9.2 | IP: 192.168.178.98`
+ — echte LAN-IP, Celery-Ping grün. Testeintrag danach wieder entfernt.
- **Anleitungs-Tab** im UI: kompletter Selbsterklärer (Disc-Weg, Serien,
NAS, Media-Server, Benachrichtigungen, Key, Worker, Downloads, FAQ).
- Design 2.0 kommt bewusst in eine FRISCHE Session (Commander-Entscheid) —
diff --git a/docker/ui/src/components/RipTargetModal.tsx b/docker/ui/src/components/RipTargetModal.tsx
index b36a1f1..f26626b 100644
--- a/docker/ui/src/components/RipTargetModal.tsx
+++ b/docker/ui/src/components/RipTargetModal.tsx
@@ -276,7 +276,13 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
{scanStatus === 'error' && (
{scanFehler}
)}
- {scanStatus === 'done' && (
+ {scanStatus === 'done' && titelListe.length === 0 && (
+
+ Keine Titel lesbar — vermutlich kann diese Disc nicht entschlüsselt
+ werden (Details stehen nach einem Rip-Versuch im Job-Fehler).
+
+ )}
+ {scanStatus === 'done' && titelListe.length > 0 && (
<>
{titelListe.map(t => (
diff --git a/docker/worker/tasks.py b/docker/worker/tasks.py
index 011acc2..8842de7 100644
--- a/docker/worker/tasks.py
+++ b/docker/worker/tasks.py
@@ -46,6 +46,29 @@ from ripping import (
API_URL = os.getenv("API_URL", "http://api:8000")
+
+def pfad_lokal(pfad: str, mapping: str = None) -> str:
+ """Übersetzt Rippy-Container-Pfade für native Worker (pure Funktion).
+
+ Der native Windows-Worker sieht /app/temp und /app/media nicht — er
+ mountet Rippys Freigabe als Netzlaufwerk und setzt RIPPY_PATH_MAP,
+ z. B. "/app/media=Z:\\media;/app/temp=Y:\\temp". Ohne Mapping (Docker-
+ Worker: identische Mounts) kommt der Pfad unverändert zurück.
+ """
+ mapping = mapping if mapping is not None else os.getenv("RIPPY_PATH_MAP", "")
+ if not pfad or not mapping:
+ return pfad
+ for paar in mapping.split(";"):
+ if "=" not in paar:
+ continue
+ quelle, ziel = paar.split("=", 1)
+ if quelle and pfad.startswith(quelle):
+ rest = pfad[len(quelle):]
+ if "\\" in ziel:
+ rest = rest.replace("/", "\\")
+ return ziel + rest
+ return pfad
+
RAW_DIR = os.getenv("RAW_DIR", "/app/temp/raw")
MEDIA_ROOT = "/app/media"
@@ -453,6 +476,9 @@ def transcode_files(self, job_id: str, raw_dir: str, final_dir: str):
Über POST /jobs/{id}/retry-transcode jederzeit neu anstoßbar.
"""
db.init_db()
+ # Native Worker (Windows) übersetzen Container-Pfade aufs Netzlaufwerk
+ raw_dir = pfad_lokal(raw_dir)
+ final_dir = pfad_lokal(final_dir)
quellen = sorted(glob.glob(os.path.join(raw_dir, "*.mkv")))
if not quellen:
ergebnis = {"status": "error", "error": f"Keine Roh-MKVs in {raw_dir} gefunden"}
diff --git a/docker/worker/test_medien.py b/docker/worker/test_medien.py
index 638fa7f..062b8c1 100644
--- a/docker/worker/test_medien.py
+++ b/docker/worker/test_medien.py
@@ -102,3 +102,13 @@ def test_matche_episoden_ohne_treffer_gibt_none():
dateien = [600] # 10 min passt auf keine 24-min-Episode
episoden = [(1, 1440), (2, 1440)]
assert matche_episoden(dateien, episoden) is None
+
+
+def test_pfad_lokal_uebersetzt_fuer_windows_worker():
+ from tasks import pfad_lokal
+
+ mapping = "/app/media=Z:\\media;/app/temp=Y:\\temp"
+ assert pfad_lokal("/app/temp/raw/abc", mapping) == "Y:\\temp\\raw\\abc"
+ assert pfad_lokal("/app/media/movies/Film (2020)", mapping) == "Z:\\media\\movies\\Film (2020)"
+ assert pfad_lokal("/woanders/x", mapping) == "/woanders/x"
+ assert pfad_lokal("/app/temp/raw/abc", "") == "/app/temp/raw/abc"