feat(v5): Etappe W-4 — Metadaten-Automatik mit Sicherheitsgrad
Ampel / ampel (push) Failing after 1m20s
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:
co-authored by
Claude Fable 5
parent
e840b7f327
commit
5f386e5406
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user