Drei Praxis-Bugs: tote NAS-Mounts reparierbar, HandBrake-Versionen konsistent, Encoder-Wahl beim Rip
Ampel / ampel (push) Successful in 28s

1) Speicher-Mounts robust (Befund: toter CIFS-Mount nach NAS-Ausfall/Rebuild —
   mounted:false, verschwand aus 'Verfuegbare Ziele', Neu-Anlegen -> 409, man
   sass fest):
   - mounts.py: ist_erreichbar() (listdir, soft-Mount bricht schnell ab),
     ist_gemountet() faengt OSError toter Mounts, aushaengen() mit
     umount -l Fallback, reparieren() (lazy abhaengen + frisch mounten).
   - /storage-mounts liefert 'reachable'; POST bei existierendem Namen:
     aktiv -> 409, tot -> automatische Reparatur mit neuen Angaben;
     neuer POST /storage-mounts/{name}/repair (gespeicherte Zugangsdaten).
   - /storage-targets crasht nicht mehr an totem Mount (os.path.ismount
     OSError abgefangen).
   - UI: eigene 'Netzwerk-Mounts'-Liste mit Status (aktiv/nicht erreichbar/
     getrennt) + Reparieren- und Entfernen-Knopf — tote Mounts sind sichtbar
     und wiederherstellbar statt zu verschwinden.

2) HandBrake-Versionen konsistent (Befund: Docker 1.6.1, Windows-Skript
   fest 1.9.2, Update-Check meldet 1.11.2 — verwirrend):
   - Windows-Installer zieht jetzt DYNAMISCH die neueste Version (GitHub
     latest, Fallback 1.11.2) — passt zum Update-Check.
   - Update-UI erklaert klar: Docker = stabiles Debian-Paket (bewusst aelter,
     kein Fehler), Windows = neueste. MakeMKV-Update zeigt den Befehl.

3) Encoder-/Worker-Auswahl beim Rip (Feature):
   - Celery worker_direct=True: jeder Worker konsumiert zusaetzlich seine
     Direkt-Queue. API-Helper transcode_queue(node) routet gezielt an den
     gewaehlten Worker, faellt aber sicher auf die geteilte transcode-Queue
     zurueck, wenn er offline ist (kein Haengenbleiben).
   - /capabilities liefert den Celery-Node je Worker; POST /jobs nimmt
     transcode_node (-> Job-meta); rip_disc + retry-transcode routen danach.
   - Rip-Dialog: Encoder-/Worker-Dropdown, sichtbar ab 2 Online-Workern.
   - ping_worker-Task zum Verifizieren des gezielten Routings.
   - Nebenfund gefixt: DeviceDiscovery leitete die Titel-Auswahl (titles)
     gar nicht an die API weiter — jetzt titles + transcode_node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-24 18:19:57 +02:00
co-authored by Claude Fable 5
parent fe1387b1ec
commit 4ba02047db
10 changed files with 360 additions and 47 deletions
+44 -2
View File
@@ -1,9 +1,9 @@
import { useState, useEffect } from 'react'
import { Folder, FolderOpen, File, ArrowUp, CheckCircle, Film, Tv, Music } from 'lucide-react'
import { Folder, FolderOpen, File, ArrowUp, CheckCircle, Film, Tv, Music, Cpu } from 'lucide-react'
import { api } from '../lib/api'
import { Modal } from './ui/Modal'
import { Button } from './ui/Button'
import { Input } from './ui/Input'
import { Input, Select } from './ui/Input'
interface TargetConfig {
id: string
@@ -18,6 +18,18 @@ export interface RipOptionen {
season?: number
mainFeatureOnly?: boolean
titles?: number[]
transcodeNode?: string // gewählter Encoder-Worker (Celery-Node) oder leer = auto
}
interface WorkerWahl {
name: string
node: string | null
online?: boolean
encoders: string[]
}
const ENCODER_KURZ: Record<string, string> = {
'cpu-x264': 'H.264', 'cpu-x265': 'H.265', 'vaapi': 'VAAPI⚡', 'nvenc': 'NVENC⚡',
}
interface TitelInfo {
@@ -56,6 +68,8 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
const [scanFehler, setScanFehler] = useState('')
const [titelListe, setTitelListe] = useState<TitelInfo[]>([])
const [gewaehlt, setGewaehlt] = useState<Set<number>>(new Set())
const [workers, setWorkers] = useState<WorkerWahl[]>([])
const [encoderNode, setEncoderNode] = useState('') // '' = automatisch
const scanStarten = async () => {
if (!deviceId) return
@@ -109,6 +123,11 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
setScanFehler('')
setTitelListe([])
setGewaehlt(new Set())
setEncoderNode('')
// Online-Worker für die Encoder-Wahl (nur relevant, wenn ≥2 verfügbar)
api.get('/capabilities').then(r => {
setWorkers((r.data.workers || []).filter((w: WorkerWahl) => w.online && w.node))
}).catch(() => setWorkers([]))
api.get('/settings').then(r => {
const s = r.data || {}
setNurHauptfilm(!!s.mainFeatureOnly)
@@ -144,6 +163,7 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
if (scanStatus === 'done' && gewaehlt.size > 0 && gewaehlt.size < titelListe.length) {
optionen.titles = [...gewaehlt].sort((a, b) => a - b)
}
if (encoderNode) optionen.transcodeNode = encoderNode
const target = targets.find(t => t.type === selectedType)
if (customPath) {
onSave({ id: 'custom', name: 'Eigener Ordner', path: customPath, type: selectedType, isActive: true }, optionen)
@@ -225,6 +245,28 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
</label>
)}
{/* Encoder-/Worker-Wahl — nur wenn mehrere Worker online sind
(sonst gibt es nichts zu wählen). Musik wird nicht komprimiert. */}
{selectedType !== 'music' && workers.length >= 2 && (
<div>
<Select
label="Encoder / Worker für die Kompression"
value={encoderNode}
onChange={e => setEncoderNode(e.target.value)}
>
<option value="">Automatisch (erster freier Worker)</option>
{workers.map(w => (
<option key={w.node!} value={w.node!}>
{w.name} {w.encoders.map(e => ENCODER_KURZ[e] || e).join(', ')}
</option>
))}
</Select>
<p className="text-xs mt-1.5 text-slate-500 dark:text-slate-400 flex items-center gap-1.5">
<Cpu size={13} /> Wähle gezielt eine Maschine (z. B. die mit GPU) sonst nimmt der erste freie Worker.
</p>
</div>
)}
{/* Titel-Auswahl (Track-Tabelle) */}
{deviceId && selectedType !== 'music' && (
<div className="rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden bg-slate-50/50 dark:bg-slate-950/50">