Restefeger: Auth komplett raus, Serien-Flow + Episoden-Matching, Jellyfin-Refresh, Duplikat-Warnung, echtes Nur-Hauptfilm
Ampel / ampel (push) Successful in 55s

AUTH ENTFERNT (Commander-Entscheid 24.07., KONZEPT §10): /token- und
/api-keys-Endpoints, auth.py, test_auth.py, passlib/bcrypt/PyJWT/
python-multipart, JWT_SECRET_KEY-Pflicht. Heimnetz-only, das UI hatte nie
einen Login — die Auth-Oberflaeche war Placebo und die passlib/bcrypt-
Falle brach die Ampel. Rate-Limit pro IP bleibt. Schnellstart laeuft
jetzt ganz ohne .env-Pflichtwerte.

Serien-Flow (Etappe-12-Kern, ARM-Wunde #395):
- Rip-Dialog: Serienname + Staffel -> Ablage <Serie>/Season NN
  (jellyfin.org/docs Naming-Schema); tvshow.nfo + poster.jpg im
  Serien-Ordner, bei Staffel 2 nicht ueberschrieben.
- Episoden-Matching per Laufzeitabgleich: HandBrakeCLI --scan
  ('+ duration:', handbrake.fr/docs) je MKV gegen TMDB-Staffel-Laufzeiten
  (GET /metadata/tv/{id}/season/{n}; tv-season-details-API).
  Ordnungserhaltend; komplette Staffel auf einer Disc klappt auch bei
  uniformen Anime-Laufzeiten (Sequenz-Stufe). Umbenannt wird NUR bei
  eindeutiger Zuordnung — sonst ehrliches Log. Mit Tests.

Weitere Punkte:
- Jellyfin/Emby-Bibliotheks-Refresh nach jedem fertigen Rip
  (POST /Library/Refresh, X-Emby-Token lt. jellyfin.org/docs) —
  URL/Key + Test-Knopf in Einstellungen -> Ripping.
- Duplikat-Warnung: Disc-Fingerabdruck (jetzt Teil des Prescan-Ergebnisses
  + der Job-Metadaten) gegen die Historie; Karte zeigt 'bereits gerippt',
  Vollautomatik ueberspringt Duplikate.
- 'Nur Hauptfilm' ECHT: makemkvcon info -> TINFO-Attr-9-Laufzeiten
  (usage.txt) -> laengster Titel -> mkv dev:X <nr>. Vorher wirkungsloses
  Setting; pro Rip im Dialog uebersteuerbar. Mit Tests.
- OMDb-Treffer eingedeutscht via TMDB /find (external_source=imdb_id,
  de-DE; find-by-id-API).
- Dashboard: Speicherplatz-Anzeige (amber < 60 GB) + CSV-Export
  (GET /jobs/export, Semikolon+BOM fuer deutsches Excel).
- Metadaten-Seite entfernt (Abnahme durch Commander-Auftrag) inkl.
  Placebo-Endpoints /metadata/lookup (scannte Dummy-Device) und
  /metadata/confirm (schrieb nie gelesenen Cache-Key).
- Doppel-Jahr-Fix: 'X (2009) (2009)' in Log und Ordnernamen.
- Remote-Worker-Blocker: redis (6379) + postgres (5432) waren NIE
  veroeffentlicht — kein Remote-Worker konnte sich je verbinden. Ports
  jetzt offen (Heimnetz-Kompromiss, kommentiert) + API_URL fuer Worker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-24 14:22:19 +02:00
parent e7a7d5fdb4
commit 8eb5653848
28 changed files with 934 additions and 892 deletions
+29 -1
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download, Trash2 } from 'lucide-react'
import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download, FileSpreadsheet, HardDrive, Trash2 } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
import { useToast } from '../context/ToastContext'
@@ -151,6 +151,7 @@ const ENCODER_KURZ: Record<string, string> = {
export default function Dashboard() {
const [jobs, setJobs] = useState<Job[]>([])
const [workers, setWorkers] = useState<WorkerInfo[]>([])
const [plaetze, setPlaetze] = useState<{ name: string, frei_gb: number, gesamt_gb: number }[]>([])
const [loading, setLoading] = useState(true)
const [detailJobId, setDetailJobId] = useState<string | null>(null)
const [aufraeumenOffen, setAufraeumenOffen] = useState(false)
@@ -189,6 +190,7 @@ export default function Dashboard() {
loadData()
api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => {})
api.get('/system/info').then(r => setPlaetze(r.data.plaetze || [])).catch(() => {})
const interval = setInterval(loadData, 5000)
return () => clearInterval(interval)
}, [])
@@ -244,6 +246,21 @@ export default function Dashboard() {
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>kein Job</span>
)}
</div>
{plaetze.length > 0 && (
<div className="flex items-center gap-2">
<HardDrive size={16} className="text-indigo-500" />
<span className={theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}>Frei:</span>
{plaetze.map(p => (
<span
key={p.name}
title={`${p.name}: ${p.frei_gb} von ${p.gesamt_gb} GB frei`}
className={`text-xs px-2 py-0.5 rounded ${p.frei_gb < 60 ? 'bg-amber-600/20 text-amber-500 font-medium' : theme === 'dark' ? 'bg-slate-700 text-slate-300' : 'bg-slate-100 text-slate-600'}`}
>
{p.name.startsWith('Media') ? 'Media' : 'Arbeit'} {p.frei_gb} GB
</span>
))}
</div>
)}
</div>
{/* Laufwerke + erkannte Disc */}
@@ -290,6 +307,17 @@ export default function Dashboard() {
<h2 className={`text-lg font-semibold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Neueste Jobs</h2>
<div className="flex items-center gap-3">
<span className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>{jobs.length} Jobs insgesamt</span>
{jobs.length > 0 && (
<a
href="/api/jobs/export"
download
title="Job-Historie als CSV herunterladen (öffnet in Excel)"
className={`inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg transition-colors ${theme === 'dark' ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'}`}
>
<FileSpreadsheet size={13} />
CSV
</a>
)}
{jobs.some(j => j.status === 'completed' || j.status === 'failed') && (
<button
onClick={() => setAufraeumenOffen(true)}