From e350523f7e51429db164dfdfeb7a00123cb6d808 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Thu, 23 Jul 2026 19:55:18 +0200 Subject: [PATCH] =?UTF-8?q?Dashboard-Disc-Karte:=20Auto-Pre-Scan=20beim=20?= =?UTF-8?q?Einlegen=20=E2=80=94=20man=20SIEHT=20was=20drinliegt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commander-Befund: Disc wurde erkannt, aber das UI verriet nicht WAS. - Watcher stoesst beim Einlegen (und beim Start mit bereits eingelegter Disc) automatisch den Pre-Scan an, Ergebnis landet im DISC_CACHE und im Log ("Disc erkannt: Titel (Jahr) [typ, Confidence]") - GET /devices liefert das disc-Feld mit Titel/Jahr/Typ/Confidence/Poster - UI: prominente Karte "Im Laufwerk erkannt" mit Poster (TMDB relativ oder OMDb absolut), Beschreibung und Rippen-Start (Ziel-Dialog) Co-Authored-By: Claude Fable 5 --- docker/api/main.py | 40 +++++++++++- docker/ui/src/components/DeviceDiscovery.tsx | 65 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/docker/api/main.py b/docker/api/main.py index ab57f6b..bf5a491 100644 --- a/docker/api/main.py +++ b/docker/api/main.py @@ -63,8 +63,34 @@ async def startup_event(): asyncio.create_task(disc_watcher()) +# Auto-Pre-Scan-Ergebnisse je Laufwerk: das Dashboard zeigt damit sofort, +# WAS im Laufwerk liegt (Titel/Jahr/Poster), ohne dass jemand klicken muss. +DISC_CACHE: Dict[str, Dict] = {} + + +async def _auto_prescan(pfad: str): + """Identifiziert die eingelegte Disc im Hintergrund und cached das Ergebnis.""" + if DISC_CACHE.get(pfad, {}).get("_laeuft"): + return + DISC_CACHE[pfad] = {"_laeuft": True, "title": "Wird erkannt…"} + try: + prescan = PreScan() + ergebnis = await asyncio.to_thread(prescan.scan, pfad) + DISC_CACHE[pfad] = ergebnis.to_dict() + db.add_log( + "info", "watcher", + f"Disc erkannt: {ergebnis.title}" + + (f" ({ergebnis.year})" if ergebnis.year else "") + + f" [{ergebnis.disc_type}, Confidence {ergebnis.confidence:.0%}] auf {pfad}", + ) + except Exception as e: + DISC_CACHE.pop(pfad, None) + print(f"Auto-Pre-Scan {pfad}: {e}") + + async def disc_watcher(): - """Disc-Wache: pollt die Laufwerke und protokolliert Einwurf/Auswurf. + """Disc-Wache: pollt die Laufwerke, protokolliert Einwurf/Auswurf und + stößt beim Einlegen automatisch den Pre-Scan an (Dashboard-Disc-Karte). Ersetzt den nie gebauten udev-Daemon aus dem KONZEPT: udev funktioniert im Container nicht sinnvoll (kein udevd) — ein 3-Sekunden-Poll per ioctl ist @@ -79,11 +105,17 @@ async def disc_watcher(): except OSError: continue vorher = bekannt.get(pfad) - if vorher is not None and status != vorher: + if vorher is None: + # Erststart: liegt schon eine Disc drin, direkt erkennen + if status == CDS_DISC_OK: + asyncio.create_task(_auto_prescan(pfad)) + elif status != vorher: if status == CDS_DISC_OK: db.add_log("info", "watcher", f"Disc eingelegt: {pfad}") + asyncio.create_task(_auto_prescan(pfad)) elif status in (CDS_NO_DISC, CDS_TRAY_OPEN) and vorher == CDS_DISC_OK: db.add_log("info", "watcher", f"Disc entfernt: {pfad}") + DISC_CACHE.pop(pfad, None) bekannt[pfad] = status except Exception as e: print(f"Disc-Wache: {e}") @@ -147,6 +179,7 @@ class Device(BaseModel): status: str model: Optional[str] = None serial: Optional[str] = None + disc: Optional[Dict] = None # Auto-Pre-Scan-Ergebnis (Titel/Jahr/Poster) def _job_row_to_model(zeile: dict) -> Job: @@ -323,6 +356,9 @@ async def get_devices(): geraete = [] for pfad in device_discovery.list_optical_devices(): info = await asyncio.to_thread(device_discovery.device_info, pfad) + disc = DISC_CACHE.get(pfad) + if disc and not disc.get("_laeuft"): + info["disc"] = disc geraete.append(Device(**info)) return geraete diff --git a/docker/ui/src/components/DeviceDiscovery.tsx b/docker/ui/src/components/DeviceDiscovery.tsx index 9ec996c..8454e83 100644 --- a/docker/ui/src/components/DeviceDiscovery.tsx +++ b/docker/ui/src/components/DeviceDiscovery.tsx @@ -4,6 +4,18 @@ import { api } from '../lib/api' import { useDarkMode } from '../context/ThemeContext' import RipTargetModal from './RipTargetModal' +interface DiscInfo { + title: string + year?: number + disc_type: string + confidence: number + metadata?: { + poster_path?: string + overview?: string + type?: string + } +} + interface Device { id: string name: string @@ -12,6 +24,14 @@ interface Device { status: 'empty' | 'ready' | 'ripping' serial?: string model?: string + disc?: DiscInfo +} + +// TMDB liefert relative Poster-Pfade, OMDb absolute URLs — beides abdecken +function posterUrl(disc?: DiscInfo): string | null { + const p = disc?.metadata?.poster_path + if (!p) return null + return p.startsWith('http') ? p : `https://image.tmdb.org/t/p/w342${p}` } export default function DeviceDiscovery() { @@ -122,6 +142,51 @@ export default function DeviceDiscovery() {
+ {/* Disc-Karte: WAS liegt gerade im Laufwerk (Auto-Pre-Scan) */} + {devices.filter(d => d.status === 'ready' && d.disc).map(device => ( +
+ {posterUrl(device.disc) && ( + {device.disc!.title} + )} +
+

+ Im Laufwerk erkannt +

+

+ {device.disc!.title} + {device.disc!.year ? ({device.disc!.year}) : null} +

+
+ + {device.disc!.disc_type?.toUpperCase()} + + + Übereinstimmung {Math.round((device.disc!.confidence || 0) * 100)} % + +
+ {device.disc!.metadata?.overview && ( +

+ {device.disc!.metadata.overview} +

+ )} + +
+
+ ))} + {devices.length === 0 ? (