Transcode-Stufe: HandBrake komprimiert NACH dem MakeMKV-Rip + --noscan-Fix
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:
Hitonabi
2026-07-23 15:56:37 +02:00
parent 009898efae
commit 2474b683e2
10 changed files with 287 additions and 16 deletions
+1 -1
View File
@@ -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)
+6 -3
View File
@@ -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',
}
+55 -2
View File
@@ -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 &amp; 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'}`}>