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
+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)
}