feat(rip): Arbeitsverzeichnis je Rip waehlbar statt global vorgegeben
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
Commander-Vorgabe 25.07.2026: "Du sollst es nicht automatisch setzen, es soll auswaehlbar sein - z. B. beim Rippen starten, oder VORHER wenn Vollautomatik eingeschaltet ist." Bisher gab es nur die globale Einstellung workDir, und die war ein freies Textfeld - man musste den Container-Pfad (/app/media/...) kennen. Beim Akira-Rip landeten deshalb 74 GB Rohdaten auf der 148-GB-VM-Platte, obwohl eine NAS-Freigabe mit 2,3 TB eingehaengt war. - RipTargetModal: neue Auswahl "Arbeitsverzeichnis fuer die Rohdaten", gespeist aus /storage-targets (mit Kennzeichnung als Netzwerk-Freigabe und freiem Platz je Ziel). Leer = "Standard aus den Einstellungen", der Wert wird zur Orientierung mit angezeigt. Bei Musik ausgeblendet - CD-Rips gehen direkt als FLAC ins Ziel, ohne Roh-Zwischenstufe. - POST /jobs nimmt work_dir entgegen, mit derselben Pfad-Haerte wie das Ziel (_validiere_ziel: muss unter /app/media liegen), und legt es in die Job-Metadaten. - _arbeitsverzeichnis(einstellungen, job_wahl) im Worker: Wahl dieses Rips -> Setting -> Container-Default. Damit bleibt die Einstellung genau das, was bei Vollautomatik-Rips greift, weil dort niemand gefragt wird. Der Text im Einstellungen-Tab sagt das jetzt auch so. - posixpath statt os.path in _arbeitsverzeichnis: das sind immer Container-Pfade, auch wenn ein nativer Windows-Worker das Modul laedt (der uebersetzt erst spaeter per pfad_lokal). os.path.normpath machte unter Windows Backslashes daraus, wodurch die MEDIA_ROOT-Pruefung nicht mehr griff - lokal als Testfehler aufgefallen. - Test deckt die Reihenfolge und die Ausbruchsversuche ab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,7 @@ export default function DeviceDiscovery() {
|
||||
...(optionen?.mainFeatureOnly !== undefined ? { main_feature_only: optionen.mainFeatureOnly } : {}),
|
||||
...(optionen?.titles && optionen.titles.length ? { titles: optionen.titles } : {}),
|
||||
...(optionen?.transcodeNode ? { transcode_node: optionen.transcodeNode } : {}),
|
||||
...(optionen?.workDir ? { work_dir: optionen.workDir } : {}),
|
||||
})
|
||||
setActionFeedback(`✓ Job angelegt (${response.data.id.slice(0, 8)}…) — Fortschritt im Dashboard`)
|
||||
toast('success', 'Rip gestartet — Fortschritt unten bei „Neueste Jobs"')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Folder, FolderOpen, File, ArrowUp, CheckCircle, Film, Tv, Music, Cpu } from 'lucide-react'
|
||||
import { Folder, FolderOpen, File, ArrowUp, CheckCircle, Film, Tv, Music, Cpu, HardDrive } from 'lucide-react'
|
||||
import { api } from '../lib/api'
|
||||
import { Modal } from './ui/Modal'
|
||||
import { Button } from './ui/Button'
|
||||
@@ -19,6 +19,15 @@ export interface RipOptionen {
|
||||
mainFeatureOnly?: boolean
|
||||
titles?: number[]
|
||||
transcodeNode?: string // gewählter Encoder-Worker (Celery-Node) oder leer = auto
|
||||
workDir?: string // Arbeitsverzeichnis für die Rohdaten; leer = Einstellung
|
||||
}
|
||||
|
||||
// Ein Ablageziel aus GET /storage-targets.
|
||||
interface StorageZiel {
|
||||
name: string
|
||||
path: string
|
||||
is_mount: boolean
|
||||
free_gb: number | null
|
||||
}
|
||||
|
||||
interface WorkerWahl {
|
||||
@@ -112,6 +121,12 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
|
||||
const [browseDirs, setBrowseDirs] = useState<BrowseDir[]>([])
|
||||
const [browseFiles, setBrowseFiles] = useState<{ name: string, size_mb: number | null }[]>([])
|
||||
const [customPath, setCustomPath] = useState('')
|
||||
// Arbeitsverzeichnis dieses Rips (Commander-Wunsch 25.07.2026: hier wählbar,
|
||||
// nicht global vorgegeben). '' = der Wert aus den Einstellungen, der auch
|
||||
// bei Vollautomatik-Rips gilt, weil dort niemand gefragt wird.
|
||||
const [arbeitsZiele, setArbeitsZiele] = useState<StorageZiel[]>([])
|
||||
const [arbeitsDir, setArbeitsDir] = useState('')
|
||||
const [standardArbeitsDir, setStandardArbeitsDir] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
@@ -124,6 +139,10 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
|
||||
setTitelListe([])
|
||||
setGewaehlt(new Set())
|
||||
setEncoderNode('')
|
||||
setArbeitsDir('')
|
||||
api.get('/storage-targets')
|
||||
.then(r => setArbeitsZiele(Array.isArray(r.data) ? r.data : []))
|
||||
.catch(() => setArbeitsZiele([]))
|
||||
// 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))
|
||||
@@ -131,6 +150,7 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
|
||||
api.get('/settings').then(r => {
|
||||
const s = r.data || {}
|
||||
setNurHauptfilm(!!s.mainFeatureOnly)
|
||||
setStandardArbeitsDir((s.workDir || '').trim())
|
||||
const basis = s.outputDir || '/app/media'
|
||||
setTargets([
|
||||
{ id: '1', name: 'Filme', path: `${basis}/${s.movieDir || 'movies'}`, type: 'movies', isActive: true },
|
||||
@@ -164,6 +184,7 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
|
||||
optionen.titles = [...gewaehlt].sort((a, b) => a - b)
|
||||
}
|
||||
if (encoderNode) optionen.transcodeNode = encoderNode
|
||||
if (arbeitsDir) optionen.workDir = arbeitsDir
|
||||
const target = targets.find(t => t.type === selectedType)
|
||||
if (customPath) {
|
||||
onSave({ id: 'custom', name: 'Eigener Ordner', path: customPath, type: selectedType, isActive: true }, optionen)
|
||||
@@ -245,6 +266,42 @@ export default function RipTargetModal({ isOpen, initialType, discTitle, deviceI
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/*
|
||||
Arbeitsverzeichnis für DIESEN Rip (Commander-Wunsch 25.07.2026).
|
||||
Warum es hier steht: Der Roh-Rip einer 4K-UHD ist bis zu 100 GB groß
|
||||
und lag bisher immer auf der Container-Platte — am 25.07. lief sie
|
||||
damit voll (74 GB Rohschnitt auf 148 GB Platte). Die Wahl gehört zur
|
||||
Disc, nicht in eine globale Einstellung. Leer = der Wert aus
|
||||
Einstellungen → Verarbeitung; genau der greift auch bei
|
||||
Vollautomatik-Rips, weil dort niemand gefragt wird.
|
||||
Musik-Rips gehen direkt als FLAC ins Ziel, ohne Roh-Zwischenstufe.
|
||||
*/}
|
||||
{selectedType !== 'music' && (
|
||||
<div>
|
||||
<Select
|
||||
label="Arbeitsverzeichnis für die Rohdaten"
|
||||
value={arbeitsDir}
|
||||
onChange={e => setArbeitsDir(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
Standard aus den Einstellungen
|
||||
{standardArbeitsDir ? ` (${standardArbeitsDir})` : ' (Container-Platte)'}
|
||||
</option>
|
||||
{arbeitsZiele.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 mt-1.5 text-slate-500 dark:text-slate-400 flex items-center gap-1.5">
|
||||
<HardDrive size={13} />
|
||||
Bei 4K-UHD bis zu 100 GB — nimm eine Freigabe mit Platz, am besten dieselbe wie das Ziel oben.
|
||||
Dann muss Rippy am Ende nur umhängen statt zu kopieren.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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 && (
|
||||
|
||||
Reference in New Issue
Block a user