Der Grund, warum der erste Rip mit externem Encoder scheitern MUSSTE - und Commander-Vorgabe: "Wenn ein externes Ziel eingehaengt ist, soll Rippy das ausgewaehlte als Arbeitsziel verwenden. Das stellt man ja sowieso in den Einstellungen ein. Das muss auch fuer den Automatik-Modus so sein." Genau das ging nicht. outputDir stand in den Einstellungen, wurde von der Vollautomatik (main.py) und der Schnellwahl (RipTargetModal) gelesen - aber es gab NIRGENDS ein Eingabefeld. Die Ablage klebte auf /app/media, der Container-Platte. Das Arbeitsverzeichnis hatte eine Auswahlliste der eingehaengten Ziele, das Ziel nicht. Folge im Praxistest: Rohdaten auf der NAS (die der PC erreicht), Ziel auf der VM-Platte (die er nicht erreicht, kein Samba dort). Der externe Encoder konnte lesen, aber nicht schreiben - und das haette man mit den vorhandenen Bedienelementen gar nicht anders einstellen koennen. JETZT: Ablage-Auswahl in Einstellungen -> Ripping, mit derselben Liste eingehaengter Ziele wie das Arbeitsverzeichnis. Damit wirkt sie automatisch auch in der Vollautomatik und in der Schnellwahl, weil beide outputDir lesen - also genau so, wie der Commander es beschrieben hat. DAZU die Pruefung, die den Vorfall verhindert haette: Sobald ein EXTERNER Worker gemeldet ist, warnt das UI, wenn Ablage und Arbeitsverzeichnis nicht beide auf einer erreichbaren Freigabe liegen. Drei Faelle, jeweils mit der Konsequenz im Klartext: - Arbeitsverzeichnis auf der Container-Platte -> externer Encoder kann nicht lesen - Arbeit auf Freigabe, Ablage lokal -> kann lesen, nicht schreiben (der Vorfall) - beide auf VERSCHIEDENEN Freigaben -> laeuft, kostet aber eine Vollkopie "Extern" ist dabei keine Heuristik ueber IPs oder Namen: caps.py meldet es selbst (`extern`), weil /app im Rippy-Image immer existiert und ausserhalb nie. Zusaetzlich meldet der Worker jetzt seine `pfad_map` - damit kann das UI im naechsten Schritt sagen, ob die Uebersetzung ueberhaupt gesetzt ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,10 +77,12 @@ interface WorkerInfo {
|
|||||||
// Angaben war am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf der
|
// Angaben war am 25.07.2026 nicht zu sehen, dass ein 4K-Encode auf der
|
||||||
// Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne).
|
// Rippy-VM Tage statt Stunden braucht (kein AVX2, 4 Kerne).
|
||||||
// handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst.
|
// handbrake_encoder: die ungefilterte Encoder-Auskunft von HandBrake selbst.
|
||||||
|
// extern: "ja" = läuft AUSSERHALB des Rippy-Containers, sieht /app/... also
|
||||||
|
// nur über eine Freigabe plus pfad_map. Der Worker meldet das selbst.
|
||||||
info?: {
|
info?: {
|
||||||
makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string,
|
makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string,
|
||||||
cpu_modell?: string, cpu_kerne?: string, cpu_simd?: string,
|
cpu_modell?: string, cpu_kerne?: string, cpu_simd?: string,
|
||||||
handbrake_encoder?: string,
|
handbrake_encoder?: string, extern?: string, pfad_map?: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +347,40 @@ export default function SettingsPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Erreichbarkeit für EXTERNE Encoder. Ein Worker auf einem anderen Rechner
|
||||||
|
// sieht die Container-Platte nicht — nur eingehängte Netzwerk-Freigaben, die
|
||||||
|
// er selbst auch erreicht. Liegen Ablage und Arbeitsverzeichnis nicht BEIDE
|
||||||
|
// auf einer solchen Freigabe, scheitert die Kompression dort zwangsläufig.
|
||||||
|
//
|
||||||
|
// Genau das ist am 25.07.2026 passiert: Rohdaten auf der NAS (erreichbar),
|
||||||
|
// Ziel auf der VM-Platte (nicht erreichbar) — der Fehler kam erst nach einer
|
||||||
|
// Stunde Rippen. Diese Prüfung sagt es vorher.
|
||||||
|
// Der Worker sagt selbst, ob er außerhalb des Containers läuft (caps.py
|
||||||
|
// meldet `extern`) — keine Heuristik über IPs oder Namen nötig.
|
||||||
|
const externerWorkerVorhanden = workers.some(w => w.info?.extern === 'ja')
|
||||||
|
const istFreigabe = (pfad: string) =>
|
||||||
|
ziele.some(z => z.path === pfad && z.is_mount)
|
||||||
|
const ablageWarnung = (() => {
|
||||||
|
const ablage = settings.outputDir || '/app/media'
|
||||||
|
const arbeit = settings.workDir
|
||||||
|
if (!arbeit) {
|
||||||
|
return 'Das Arbeitsverzeichnis steht auf der Container-Platte. Ein Encoder auf '
|
||||||
|
+ 'einem anderen Rechner kann die Rohdaten dort nicht lesen — lege beides auf '
|
||||||
|
+ 'dieselbe Netzwerk-Freigabe (Einstellungen → Verarbeitung).'
|
||||||
|
}
|
||||||
|
if (!istFreigabe(ablage) && istFreigabe(arbeit)) {
|
||||||
|
return 'Arbeitsverzeichnis liegt auf einer Freigabe, die Ablage aber auf der '
|
||||||
|
+ 'Container-Platte. Ein externer Encoder kann dann lesen, aber nicht schreiben — '
|
||||||
|
+ 'genau daran scheiterte der erste Versuch. Stelle die Ablage auf dieselbe Freigabe.'
|
||||||
|
}
|
||||||
|
if (istFreigabe(ablage) && istFreigabe(arbeit) && ablage !== arbeit) {
|
||||||
|
return 'Ablage und Arbeitsverzeichnis liegen auf VERSCHIEDENEN Freigaben. Das '
|
||||||
|
+ 'funktioniert, kostet am Ende aber eine Vollkopie statt eines Umhängens. '
|
||||||
|
+ 'Dieselbe Freigabe für beides ist deutlich schneller.'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})()
|
||||||
|
|
||||||
const tabs: { id: SettingsTab; label: string; icon: any }[] = [
|
const tabs: { id: SettingsTab; label: string; icon: any }[] = [
|
||||||
{ id: 'ripping', label: 'Ripping', icon: Disc },
|
{ id: 'ripping', label: 'Ripping', icon: Disc },
|
||||||
{ id: 'verarbeitung', label: 'Verarbeitung', icon: Cpu },
|
{ id: 'verarbeitung', label: 'Verarbeitung', icon: Cpu },
|
||||||
@@ -732,12 +768,50 @@ export default function SettingsPage() {
|
|||||||
Speicherziele
|
Speicherziele
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
{/* ABLAGE — bis 26.07.2026 gab es dieses Feld im UI GAR NICHT.
|
||||||
|
outputDir stand in den Einstellungen und wurde von der
|
||||||
|
Vollautomatik und der Schnellwahl gelesen, ließ sich aber
|
||||||
|
nirgends ändern: die Ablage klebte auf /app/media, also der
|
||||||
|
Container-Platte. Deshalb konnte man das Arbeitsverzeichnis auf
|
||||||
|
eine NAS legen (dafür gab es eine Auswahl), das ZIEL aber nicht
|
||||||
|
— und genau daran scheiterte der erste Rip mit externem
|
||||||
|
Encoder: Rohdaten auf der erreichbaren NAS, Ziel auf der
|
||||||
|
VM-Platte, die der andere Rechner nicht sieht. */}
|
||||||
|
<div className="p-5 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||||
|
<HardDrive size={16} className="text-amber-500" /> Ablage — wohin die fertigen Filme kommen
|
||||||
|
</h3>
|
||||||
|
<Select
|
||||||
|
label="Ablage"
|
||||||
|
value={settings.outputDir}
|
||||||
|
onChange={(e) => handleChange('outputDir', e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="/app/media">Container-Platte (Standard)</option>
|
||||||
|
{ziele.filter(z => z.path !== '/app/media').map(z => (
|
||||||
|
<option key={z.path} value={z.path}>
|
||||||
|
{z.name}{z.is_mount ? ' (Netzwerk-Freigabe)' : ''}
|
||||||
|
{z.free_gb != null ? ` — ${z.free_gb} GB frei` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Gilt für die Schnellwahl im Rip-Dialog <strong className="text-slate-700 dark:text-slate-300">und
|
||||||
|
für die Vollautomatik</strong> — dort fragt niemand nach, also entscheidet dieser Wert.
|
||||||
|
Die Unterordner darunter kommen aus der Schnellwahl.
|
||||||
|
</p>
|
||||||
|
{externerWorkerVorhanden && ablageWarnung && (
|
||||||
|
<p className="text-xs p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 text-amber-700 dark:text-amber-300">
|
||||||
|
⚠️ {ablageWarnung}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-5 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
<div className="p-5 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
||||||
<h3 className="font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
<h3 className="font-semibold flex items-center gap-2 text-slate-900 dark:text-slate-100">
|
||||||
<Database size={16} className="text-amber-500" /> Schnellwahl beim Rippen
|
<Database size={16} className="text-amber-500" /> Schnellwahl beim Rippen
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
Name der Ordner im „Rippen starten"-Dialog (🎥/📺/🎵).
|
Name der Ordner im „Rippen starten"-Dialog (🎥/📺/🎵) — jeweils unterhalb der Ablage.
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<div className="grid grid-cols-3 gap-3">
|
||||||
{([['movieDir', '🎥 Filme'], ['seriesDir', '📺 Serien'], ['musicDir', '🎵 Musik']] as const).map(([feld, label]) => (
|
{([['movieDir', '🎥 Filme'], ['seriesDir', '📺 Serien'], ['musicDir', '🎵 Musik']] as const).map(([feld, label]) => (
|
||||||
|
|||||||
@@ -269,6 +269,16 @@ def werkzeug_versionen() -> dict:
|
|||||||
"cpu_modell": modell,
|
"cpu_modell": modell,
|
||||||
"cpu_kerne": str(kerne),
|
"cpu_kerne": str(kerne),
|
||||||
"cpu_simd": simd,
|
"cpu_simd": simd,
|
||||||
|
# Läuft dieser Worker INNERHALB des Rippy-Containers oder auf einer
|
||||||
|
# fremden Maschine? Entscheidend, weil ein externer Worker die
|
||||||
|
# Container-Pfade (/app/media, /app/temp) nur über eine Freigabe plus
|
||||||
|
# RIPPY_PATH_MAP erreicht. Das UI kann damit VOR dem Rip warnen, statt
|
||||||
|
# den Nutzer eine Stunde rippen zu lassen (Vorfall 25.07.2026).
|
||||||
|
# /app ist im Rippy-Image immer vorhanden — kein Ratespiel.
|
||||||
|
"extern": "nein" if os.path.isdir("/app") else "ja",
|
||||||
|
# Ist die Pfad-Übersetzung gesetzt? Ohne sie kann ein externer Worker
|
||||||
|
# grundsätzlich nicht komprimieren.
|
||||||
|
"pfad_map": os.getenv("RIPPY_PATH_MAP", ""),
|
||||||
}
|
}
|
||||||
hb_encoder = parse_encoder_liste(hole_handbrake_hilfe())
|
hb_encoder = parse_encoder_liste(hole_handbrake_hilfe())
|
||||||
if hb_encoder:
|
if hb_encoder:
|
||||||
|
|||||||
Reference in New Issue
Block a user