feat(v5): Etappe W-4 — Metadaten-Automatik mit Sicherheitsgrad
Ampel / ampel (push) Failing after 1m20s

metadaten/: discmerkmale.ts (BDMT-Studio-Titel, ISO-9660- und
UDF-Volume-Label direkt vom Medium — OHNE koffi, ueber fs auf \.\G: —,
Label-Normalisierung, Fingerabdruck, progressive Titel-Kandidaten),
tmdb.ts (v3-Key UND v4-Token, de-DE, /find fuer deutsche Texte zu
OMDb-Treffern), omdb.ts, zuordnung.ts (die Confidence-Kaskade
0,95/0,90/0,80/0,60/0,30 aus prescan.py). ablage/poster.ts. Kern meldet
'disc-info' beim Einlegen; die Pipeline benennt den fertigen Ordner
'<Titel> (Jahr)', schreibt movie.nfo/tvshow.nfo mit echten Metadaten
und laedt das Poster. Fenster zeigt 'sehr wahrscheinlich: … · 95 %'.

ZWEI Kaskaden-Schwaechen LIVE GEFUNDEN und gehaertet (gegen die echten
APIs gemessen, Keys aus den Docker-Settings der VM, nicht persistiert):
1. 'Spartacus Gods Of The Arena' wurde zum FILM 'Spartacus' (1960) —
   der stark gekuerzte Kandidat traf woertlich, bevor die volle Anfrage
   als spezifische SERIE galt. Kaskade jetzt JE KANDIDAT
   woertlich→spezifisch (spezifisch nur fuer die volle Anfrage, auch
   fuer Serien). Nachgemessen: jetzt Serie, 90 %.
2. OMDbs t=-Suche machte aus dem Label 'Bd Evg D2' selbstbewusst
   'BD Scream 5' — jetzt Wort-Jaccard-Gate >= 0,5. Nachgemessen: der
   Fall ist jetzt ein ehrlicher 60-%-Vorschlag, den das UI nachfragt.

Messwerte: Akira→95% · Evangelion 2.22→80% (der dokumentierte Fund) ·
Spartacus GotA→Serie 90% · Bd Evg D2→Vorschlag 60%.

Dazu ein neuer Quelltext-Hygiene-Waechter (keine rohen Steuerbytes —
zweimal beim Schreiben passiert, einmal haette ein Test dadurch anders
gemessen als gelesen). 165 Tests gruen, Smoke gruen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-30 18:39:09 +02:00
co-authored by Claude Fable 5
parent e840b7f327
commit 5f386e5406
10 changed files with 709 additions and 8 deletions
+31 -3
View File
@@ -2,7 +2,7 @@
// Disc-Wache. Die große Oberfläche (Dashboard, Bibliothek, Einstellungen)
// ist Etappe W-5 — hier wird das Gerüst SICHTBAR bewiesen.
import { useEffect, useState } from 'react'
import type { GeraeteInfo, HauptStatus, RipStatus } from '../gemeinsam/nachrichten'
import type { DiscInfoStand, GeraeteInfo, HauptStatus, RipStatus } from '../gemeinsam/nachrichten'
import {
aufVerbindung,
auswerfen,
@@ -60,7 +60,22 @@ function Kachel({ titel, zustand, zeilen }: KachelProps) {
const LAUFENDE_PHASEN = ['liest-info', 'rippt', 'komprimiert', 'legt-ab']
function LaufwerkKachel({ geraet, rip }: { geraet: GeraeteInfo; rip: RipStatus | undefined }) {
function sicherheitsWort(confidence: number): string {
if (confidence >= 0.9) return 'sehr wahrscheinlich'
if (confidence >= 0.8) return 'wahrscheinlich'
if (confidence >= 0.6) return 'Vorschlag'
return 'unsicher'
}
function LaufwerkKachel({
geraet,
rip,
info,
}: {
geraet: GeraeteInfo
rip: RipStatus | undefined
info: DiscInfoStand | undefined
}) {
const ripLaeuft = rip !== undefined && LAUFENDE_PHASEN.includes(rip.phase)
const zustand = geraet.status === 'ready' ? 'ok' : geraet.status === 'empty' ? 'wartet' : 'fehler'
const punktFarbe = ripLaeuft
@@ -113,6 +128,13 @@ function LaufwerkKachel({ geraet, rip }: { geraet: GeraeteInfo; rip: RipStatus |
{geraet.groesseBytes > 0 ? ` · ${gb(geraet.groesseBytes)}` : ''}
</p>
)}
{geraet.status === 'ready' && info !== undefined && info.titel.length > 0 && (
<p className={info.confidence >= 0.8 ? 'text-emerald-300' : 'text-amber-300'}>
{sicherheitsWort(info.confidence)}: {info.titel}
{info.jahr !== null ? ` (${info.jahr})` : ''} · {Math.round(info.confidence * 100)} %
{info.quelle.length > 0 ? ` · ${info.quelle}` : ''}
</p>
)}
{geraet.status === 'empty' && <p>keine Disc eingelegt</p>}
{geraet.status === 'unknown' && <p className="text-red-300">{geraet.grund}</p>}
{geraet.status !== 'unknown' && geraet.grund.length > 0 && (
@@ -162,6 +184,7 @@ export default function App() {
pongLatenzMs: null,
laufwerke: [],
rips: {},
discInfos: {},
meldungen: [],
})
@@ -201,7 +224,12 @@ export default function App() {
) : (
<div className="grid gap-4 sm:grid-cols-2">
{verbindung.laufwerke.map((geraet) => (
<LaufwerkKachel key={geraet.id} geraet={geraet} rip={verbindung.rips[geraet.id]} />
<LaufwerkKachel
key={geraet.id}
geraet={geraet}
rip={verbindung.rips[geraet.id]}
info={verbindung.discInfos[geraet.id]}
/>
))}
</div>
)}
+33 -3
View File
@@ -6,7 +6,13 @@
// jedem neuen Abonnenten sofort wiedergegeben — er wird nie auf »leer«
// zurückgesetzt, nur weil gerade keine Nachricht kam. Insbesondere die
// Laufwerksliste: Sie ändert sich NUR durch eine neue Liste vom Kern.
import { istKernNachricht, type GeraeteInfo, type KernNachricht, type RipStatus } from '../gemeinsam/nachrichten'
import {
istKernNachricht,
type DiscInfoStand,
type GeraeteInfo,
type KernNachricht,
type RipStatus,
} from '../gemeinsam/nachrichten'
export interface Verbindung {
verbunden: boolean
@@ -16,6 +22,8 @@ export interface Verbindung {
/** Letzter Rip-Stand je Laufwerks-Kennung. Bleibt nach dem Ende stehen,
* bis ein neuer Rip startet (R2: kein Verschwinden ohne neue Auskunft). */
rips: Record<string, RipStatus>
/** Was Rippy über die eingelegte Disc weiß (§ 5 Schritt 3). */
discInfos: Record<string, DiscInfoStand>
/** Kurze Ereigniszeilen (neueste zuerst): Disc rein/raus, Auswurf, Fehler. */
meldungen: string[]
}
@@ -30,6 +38,7 @@ const stand: Verbindung = {
pongLatenzMs: null,
laufwerke: [],
rips: {},
discInfos: {},
meldungen: [],
}
const horcher = new Set<Horcher>()
@@ -38,7 +47,13 @@ let port: MessagePort | null = null
function melden(): void {
for (const h of horcher) {
h({ ...stand, laufwerke: [...stand.laufwerke], rips: { ...stand.rips }, meldungen: [...stand.meldungen] })
h({
...stand,
laufwerke: [...stand.laufwerke],
rips: { ...stand.rips },
discInfos: { ...stand.discInfos },
meldungen: [...stand.meldungen],
})
}
}
@@ -79,6 +94,15 @@ function kernNachricht(nachricht: KernNachricht): void {
meldung(nachricht.grund === 'ok' ? text : `${text}`)
break
}
case 'disc-info': {
stand.discInfos = { ...stand.discInfos, [nachricht.info.id]: nachricht.info }
if (nachricht.info.titel.length > 0) {
meldung(
`Laufwerk ${nachricht.info.id}: erkannt als „${nachricht.info.titel}${nachricht.info.jahr !== null ? ` (${nachricht.info.jahr})` : ''}“ · ${Math.round(nachricht.info.confidence * 100)} %`,
)
}
break
}
case 'rip-status': {
stand.rips = { ...stand.rips, [nachricht.status.id]: nachricht.status }
if (nachricht.status.phase === 'fertig') {
@@ -130,7 +154,13 @@ export function verbindungStarten(): void {
export function aufVerbindung(h: Horcher): () => void {
horcher.add(h)
h({ ...stand, laufwerke: [...stand.laufwerke], rips: { ...stand.rips }, meldungen: [...stand.meldungen] })
h({
...stand,
laufwerke: [...stand.laufwerke],
rips: { ...stand.rips },
discInfos: { ...stand.discInfos },
meldungen: [...stand.meldungen],
})
return () => horcher.delete(h)
}