Drei Praxis-Bugs: tote NAS-Mounts reparierbar, HandBrake-Versionen konsistent, Encoder-Wahl beim Rip
Ampel / ampel (push) Successful in 28s
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:
co-authored by
Claude Fable 5
parent
fe1387b1ec
commit
4ba02047db
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user