Transcode-Stufe: HandBrake komprimiert NACH dem MakeMKV-Rip + --noscan-Fix
Ampel / ampel (push) Successful in 34s
Ampel / ampel (push) Successful in 34s
Commander-Entscheid 23.07.: 40-GB-Rohdateien sind kein brauchbares Endprodukt. Architektur bleibt zweistufig, weil HandBrake AACS nicht lesen kann: MakeMKV rippt verlustfrei nach /app/temp/raw, HandBrake macht daraus x265 auf Arbeitsgroesse in /app/media, Roh-Verzeichnis wird erst NACH komplettem Erfolg geloescht (keepOriginal behaelt es). - Worker: handbrake-cli im Image, run_handbrake + _komprimiere, Job-Status "transcoding", Settings aus Postgres (transcodeEnabled/ Preset/keepOriginal), Fortschritt je Datei aggregiert - makemkvcon: --noscan im Kommando verankert — der Geraete-Scan haengt (1.18.4) bzw. crasht (1.17.7) im Container, mit --noscan + dev:-Pfad laeuft es (Befund 23.07., DRV-Zeile beweist Laufwerks-Erkennung) - UI: Settings-Tab "Verarbeitung" (Toggle/Preset/Original behalten), Status-Badge "Komprimieren", LiveLog erkennt transcoding als aktiv - KONZEPT/ROADMAP entsprechend aktualisiert; Tests fuer HandBrake-Cmd (arbeitet auf DATEI, nie am Geraet) und Progress-Regex Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ export default function LiveLogSection() {
|
||||
try {
|
||||
const response = await api.get('/jobs')
|
||||
const jobs = response.data
|
||||
const current = jobs.find((j: any) => j.status === 'processing')
|
||||
const current = jobs.find((j: any) => j.status === 'processing' || j.status === 'transcoding')
|
||||
setCurrentJob(current || jobs.find((j: any) => j.status === 'pending'))
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Jobs:', error)
|
||||
|
||||
@@ -8,7 +8,7 @@ import LiveLogSection from '../components/LiveLogSection'
|
||||
interface Job {
|
||||
id: string
|
||||
type: 'cd' | 'dvd' | 'bluray'
|
||||
status: 'pending' | 'processing' | 'completed' | 'failed'
|
||||
status: 'pending' | 'processing' | 'transcoding' | 'completed' | 'failed'
|
||||
device: string
|
||||
startTime: string
|
||||
endTime?: string
|
||||
@@ -31,19 +31,22 @@ function StatusBadge({ status }: { status: string }) {
|
||||
? {
|
||||
pending: 'bg-amber-900/30 text-amber-400 ring-amber-700/20',
|
||||
processing: 'bg-blue-900/30 text-blue-400 ring-blue-700/20',
|
||||
transcoding: 'bg-purple-900/30 text-purple-400 ring-purple-700/20',
|
||||
completed: 'bg-emerald-900/30 text-emerald-400 ring-emerald-700/20',
|
||||
failed: 'bg-rose-900/30 text-rose-400 ring-rose-700/20',
|
||||
}
|
||||
: {
|
||||
pending: 'bg-amber-100 text-amber-700 ring-amber-600/20',
|
||||
processing: 'bg-blue-100 text-blue-700 ring-blue-600/20',
|
||||
transcoding: 'bg-purple-100 text-purple-700 ring-purple-600/20',
|
||||
completed: 'bg-emerald-100 text-emerald-700 ring-emerald-600/20',
|
||||
failed: 'bg-rose-100 text-rose-700 ring-rose-600/20',
|
||||
}
|
||||
|
||||
|
||||
const labels = {
|
||||
pending: 'Wartend',
|
||||
processing: 'Bearbeitung',
|
||||
processing: 'Rippen',
|
||||
transcoding: 'Komprimieren',
|
||||
completed: 'Fertig',
|
||||
failed: 'Fehler',
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Save, Disc, Database, Globe, AlertCircle, CheckCircle } from 'lucide-react'
|
||||
import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle } from 'lucide-react'
|
||||
import { api } from '../lib/api'
|
||||
import { useDarkMode } from '../context/ThemeContext'
|
||||
|
||||
@@ -15,6 +15,9 @@ interface SettingsState {
|
||||
mainFeatureOnly: boolean
|
||||
autoEject: boolean
|
||||
notificationWebhook: string
|
||||
transcodeEnabled: boolean
|
||||
transcodePreset: string
|
||||
keepOriginal: boolean
|
||||
}
|
||||
|
||||
const defaultSettings: SettingsState = {
|
||||
@@ -29,9 +32,12 @@ const defaultSettings: SettingsState = {
|
||||
mainFeatureOnly: false,
|
||||
autoEject: true,
|
||||
notificationWebhook: '',
|
||||
transcodeEnabled: true,
|
||||
transcodePreset: 'H.265 MKV 1080p30',
|
||||
keepOriginal: false,
|
||||
}
|
||||
|
||||
type SettingsTab = 'ripping' | 'verzeichnisse' | 'apis' | 'benachrichtigungen'
|
||||
type SettingsTab = 'ripping' | 'verarbeitung' | 'verzeichnisse' | 'apis' | 'benachrichtigungen'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [settings, setSettings] = useState<SettingsState>(defaultSettings)
|
||||
@@ -98,6 +104,7 @@ export default function SettingsPage() {
|
||||
{/* Tabs — echte Navigation (vorher reine Dekoration ohne onClick) */}
|
||||
<div className={`flex transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} border-b`}>
|
||||
<TabButton icon={Disc} label="Ripping" isActive={activeTab === 'ripping'} onClick={() => setActiveTab('ripping')} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Cpu} label="Verarbeitung" isActive={activeTab === 'verarbeitung'} onClick={() => setActiveTab('verarbeitung')} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Database} label="Verzeichnisse" isActive={activeTab === 'verzeichnisse'} onClick={() => setActiveTab('verzeichnisse')} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={Globe} label="APIs" isActive={activeTab === 'apis'} onClick={() => setActiveTab('apis')} darkMode={theme === 'dark'} />
|
||||
<TabButton icon={AlertCircle} label="Benachrichtigungen" isActive={activeTab === 'benachrichtigungen'} onClick={() => setActiveTab('benachrichtigungen')} darkMode={theme === 'dark'} />
|
||||
@@ -166,6 +173,52 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</section>}
|
||||
|
||||
{/* Verarbeitung: Kompressions-Stufe nach dem Rip */}
|
||||
{activeTab === 'verarbeitung' && <section>
|
||||
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
|
||||
<Cpu size={20} className={theme === 'dark' ? 'text-indigo-400' : 'text-indigo-600'} />
|
||||
Verarbeitung (Kompression)
|
||||
</h2>
|
||||
<p className={`text-sm mb-4 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
|
||||
MakeMKV rippt verlustfrei (der einzige Weg durch den Kopierschutz), HandBrake
|
||||
komprimiert danach auf Arbeitsgröße. Die Roh-Datei wird nach Erfolg gelöscht.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 transition-colors duration-300">
|
||||
<div className={`flex items-center justify-between p-4 rounded-lg ${theme === 'dark' ? 'bg-slate-700/50' : 'bg-slate-50'}`}>
|
||||
<div>
|
||||
<h3 className={`font-medium ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Nach dem Rip komprimieren</h3>
|
||||
<p className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>x265 statt 40-GB-Rohdatei (empfohlen)</p>
|
||||
</div>
|
||||
<Toggle checked={settings.transcodeEnabled} onChange={(v) => handleChange('transcodeEnabled', v)} darkMode={theme === 'dark'} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className={`block text-sm font-medium ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>
|
||||
HandBrake-Preset
|
||||
</label>
|
||||
<select
|
||||
value={settings.transcodePreset}
|
||||
onChange={(e) => handleChange('transcodePreset', e.target.value)}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent ${theme === 'dark' ? 'bg-slate-800 border-slate-600 text-slate-200' : 'bg-white border-slate-300 text-slate-900'}`}
|
||||
>
|
||||
<option value="H.265 MKV 1080p30">H.265 MKV 1080p30 — klein & modern (Standard)</option>
|
||||
<option value="H.265 MKV 2160p60 4K">H.265 MKV 2160p60 4K — für UHD-Quellen</option>
|
||||
<option value="HQ 1080p30 Surround">HQ 1080p30 Surround — H.264, maximale Kompatibilität</option>
|
||||
<option value="Super HQ 1080p30 Surround">Super HQ 1080p30 Surround — größer, beste Qualität</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={`flex items-center justify-between p-4 rounded-lg ${theme === 'dark' ? 'bg-slate-700/50' : 'bg-slate-50'}`}>
|
||||
<div>
|
||||
<h3 className={`font-medium ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Original behalten</h3>
|
||||
<p className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Verlustfreie Roh-Datei zusätzlich aufheben (viel Speicher!)</p>
|
||||
</div>
|
||||
<Toggle checked={settings.keepOriginal} onChange={(v) => handleChange('keepOriginal', v)} darkMode={theme === 'dark'} />
|
||||
</div>
|
||||
</div>
|
||||
</section>}
|
||||
|
||||
{/* Output Directories */}
|
||||
{activeTab === 'verzeichnisse' && <section>
|
||||
<h2 className={`text-lg font-semibold mb-4 flex items-center gap-2 transition-colors duration-300 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
|
||||
|
||||
Reference in New Issue
Block a user