FUND 1 — 'Key korrekt, aber als ungueltig angezeigt': Stimmt beides. Live gemessen: Der aktuelle Forum-Key ist in Ordnung, aber MakeMKV 1.18.4 ist ZU ALT fuer ihn — makemkvcon meldet 5020 UND 5021 zusammen, und die alte Pruefung brach beim ersten Code ab und beschuldigte den Schluessel. urteilAusAusgabe sammelt jetzt ALLE Codes (5021 schlaegt 5020, MakeMKV-Version aus MSG 1005 wird mitgelesen) und die Kachel sagt den wahren Satz: 'Der Key ist in Ordnung — MakeMKV 1.18.4 ist zu alt. Rippy hat ihn entfernt, damit der Beta-Modus weiterlaeuft (Rips klappen trotzdem). Abhilfe: MakeMKV aktualisieren.' Neues Feld weiterBetrieb: Beta-Modus ist GELB (Warnung), nicht rot. Test mit der echten Ausgabe als Fixture. FUND 2 — 'Komprimieren auf CPU statt GPU': Der Default 'H.265 MKV 1080p30' ist ein CPU-Preset. Neue GEMESSENE Empfehlung (gemeinsam/preset-empfehlung.ts): aus den echten Backends und der echten Preset-Liste des eigenen HandBrake — VCN vor NVENC vor QSV vor CPU, nur Namen die WIRKLICH in der Liste stehen, HandBrake-Presets deckeln die Aufloesung nur (DVD bleibt SD). Kette: eigene Wahl > allgemeines Preset > Empfehlung > CPU-Default. Auf dem Commander-PC: bluray/dvd -> 'H.265 VCN 1080p', uhd -> 'H.265 VCN 2160p 4K'. Die Preset-Auswahl zeigt 'automatisch: <Empfehlung>' als Leer-Option. FUND 3 — 'keine Alternative waehlbar': 'Aendern …'-Dialog an der Laufwerks-Kachel (§ 5 Schritt 4): TMDb-Suche (Film + Serie, mit Postern), Klick uebernimmt — Nutzer-Wahl gilt als 100 % und steuert Ablage-Ordner, NFO und Poster. Neue Nachrichten metadaten-suchen / zuordnung-setzen / metadaten-vorschlaege. Dazu: Poster in der Laufwerks-Kachel (CSP um image.tmdb.org erweitert), Restzeit-Schaetzung am Fortschritt. 192 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
0e1c927e7b
commit
c5dd90cc18
@@ -24,7 +24,9 @@ function cspNurImBau(): Plugin {
|
|||||||
transformIndexHtml(html) {
|
transformIndexHtml(html) {
|
||||||
return html.replace(
|
return html.replace(
|
||||||
'<head>',
|
'<head>',
|
||||||
`<head>\n <meta http-equiv="Content-Security-Policy" content="default-src 'self'" />`,
|
// img-src erlaubt TMDb-Poster (Kachel + Ändern-Dialog) — sonst
|
||||||
|
// bleibt alles bei 'self'.
|
||||||
|
`<head>\n <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data: https://image.tmdb.org" />`,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,33 @@
|
|||||||
// Bibliothek (§ 6.6). Design-Sprache wie das heutige Rippy: dunkel, Kacheln.
|
// Bibliothek (§ 6.6). Design-Sprache wie das heutige Rippy: dunkel, Kacheln.
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import type { DiscInfoStand, GeraeteInfo, HauptStatus, RipStatus } from '../gemeinsam/nachrichten'
|
import type { DiscInfoStand, GeraeteInfo, HauptStatus, RipStatus } from '../gemeinsam/nachrichten'
|
||||||
|
import { presetEmpfehlung } from '../gemeinsam/preset-empfehlung'
|
||||||
import {
|
import {
|
||||||
aufVerbindung,
|
aufVerbindung,
|
||||||
auswerfen,
|
auswerfen,
|
||||||
einstellungSetzen,
|
einstellungSetzen,
|
||||||
|
metadatenSuchen,
|
||||||
ripAbbrechen,
|
ripAbbrechen,
|
||||||
ripStarten,
|
ripStarten,
|
||||||
verbindungStarten,
|
verbindungStarten,
|
||||||
|
zuordnungSetzen,
|
||||||
type Verbindung,
|
type Verbindung,
|
||||||
} from './kernverbindung'
|
} from './kernverbindung'
|
||||||
|
import type { MetaVorschlag } from '../gemeinsam/nachrichten'
|
||||||
|
|
||||||
|
function posterUrlKlein(pfad: string): string {
|
||||||
|
if (pfad.length === 0) return ''
|
||||||
|
if (pfad.startsWith('http')) return pfad
|
||||||
|
return `https://image.tmdb.org/t/p/w154${pfad}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function restzeitText(prozent: number, startMs: number | undefined): string {
|
||||||
|
if (startMs === undefined || prozent <= 2 || prozent >= 100) return ''
|
||||||
|
const verstrichen = (Date.now() - startMs) / 1000
|
||||||
|
const restS = Math.round((verstrichen * (100 - prozent)) / prozent)
|
||||||
|
if (restS < 90) return `· ~${restS} s`
|
||||||
|
return `· ~${Math.round(restS / 60)} min`
|
||||||
|
}
|
||||||
|
|
||||||
function istHauptStatus(wert: unknown): wert is HauptStatus {
|
function istHauptStatus(wert: unknown): wert is HauptStatus {
|
||||||
return (
|
return (
|
||||||
@@ -68,15 +86,101 @@ function Kachel({ titel, zustand, zeilen }: KachelProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// § 5 Schritt 4: „nein → fragen" — Suche + Auswahl der richtigen Metadaten.
|
||||||
|
function AendernDialog({
|
||||||
|
id,
|
||||||
|
startText,
|
||||||
|
vorschlaege,
|
||||||
|
schliessen,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
startText: string
|
||||||
|
vorschlaege: MetaVorschlag[]
|
||||||
|
schliessen: () => void
|
||||||
|
}) {
|
||||||
|
const [text, setText] = useState(startText)
|
||||||
|
useEffect(() => {
|
||||||
|
metadatenSuchen(id, startText)
|
||||||
|
// Nur beim Öffnen — weitere Suchen löst der Knopf aus.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-6" onClick={schliessen}>
|
||||||
|
<div
|
||||||
|
className="max-h-[80vh] w-full max-w-lg overflow-y-auto rounded-xl border border-slate-600 bg-slate-900 p-4 shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h3 className="mb-3 text-sm font-semibold text-slate-200">Was ist das wirklich?</h3>
|
||||||
|
<div className="mb-3 flex gap-2">
|
||||||
|
<input
|
||||||
|
value={text}
|
||||||
|
onChange={(e) => setText(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') metadatenSuchen(id, text)
|
||||||
|
}}
|
||||||
|
placeholder="Titel suchen …"
|
||||||
|
className="flex-1 rounded-lg border border-slate-600 bg-slate-950 px-3 py-1.5 font-mono text-xs text-slate-200 focus:border-sky-400 focus:outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => metadatenSuchen(id, text)}
|
||||||
|
className="rounded-lg border border-sky-500/60 px-3 py-1.5 text-xs text-sky-300 hover:bg-sky-500/10"
|
||||||
|
>
|
||||||
|
Suchen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{vorschlaege.length === 0 ? (
|
||||||
|
<p className="font-mono text-xs text-slate-500">Keine Treffer — anderen Suchtext probieren.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{vorschlaege.map((v) => (
|
||||||
|
<button
|
||||||
|
key={`${v.typ}-${v.tmdbId}`}
|
||||||
|
onClick={() => {
|
||||||
|
zuordnungSetzen(id, v)
|
||||||
|
schliessen()
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center gap-3 rounded-lg border border-slate-700/60 bg-slate-800/40 p-2 text-left hover:border-sky-400/60 hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
{v.posterPfad.length > 0 ? (
|
||||||
|
<img src={posterUrlKlein(v.posterPfad)} alt="" className="h-16 w-11 rounded object-cover" />
|
||||||
|
) : (
|
||||||
|
<div className="flex h-16 w-11 items-center justify-center rounded bg-slate-700 text-lg">🎬</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-slate-200">
|
||||||
|
{v.titel}
|
||||||
|
{v.jahr !== null ? ` (${v.jahr})` : ''}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-500">{v.typ === 'tv' ? 'Serie' : 'Film'}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mt-3 text-right">
|
||||||
|
<button onClick={schliessen} className="rounded-lg border border-slate-600 px-3 py-1 text-xs text-slate-300 hover:bg-slate-700">
|
||||||
|
Schließen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function LaufwerkKachel({
|
function LaufwerkKachel({
|
||||||
geraet,
|
geraet,
|
||||||
rip,
|
rip,
|
||||||
info,
|
info,
|
||||||
|
vorschlaege,
|
||||||
|
ripStartMs,
|
||||||
}: {
|
}: {
|
||||||
geraet: GeraeteInfo
|
geraet: GeraeteInfo
|
||||||
rip: RipStatus | undefined
|
rip: RipStatus | undefined
|
||||||
info: DiscInfoStand | undefined
|
info: DiscInfoStand | undefined
|
||||||
|
vorschlaege: MetaVorschlag[]
|
||||||
|
ripStartMs: number | undefined
|
||||||
}) {
|
}) {
|
||||||
|
const [aendernOffen, setAendernOffen] = useState(false)
|
||||||
const ripLaeuft = rip !== undefined && LAUFENDE_PHASEN.includes(rip.phase)
|
const ripLaeuft = rip !== undefined && LAUFENDE_PHASEN.includes(rip.phase)
|
||||||
const zustand = geraet.status === 'ready' ? 'ok' : geraet.status === 'empty' ? 'wartet' : 'fehler'
|
const zustand = geraet.status === 'ready' ? 'ok' : geraet.status === 'empty' ? 'wartet' : 'fehler'
|
||||||
const punktFarbe = ripLaeuft
|
const punktFarbe = ripLaeuft
|
||||||
@@ -121,7 +225,15 @@ function LaufwerkKachel({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 space-y-0.5 font-mono text-xs text-slate-400">
|
<div className="mt-2 flex gap-3">
|
||||||
|
{geraet.status === 'ready' && info !== undefined && info.posterPfad.length > 0 && (
|
||||||
|
<img
|
||||||
|
src={posterUrlKlein(info.posterPfad)}
|
||||||
|
alt=""
|
||||||
|
className="h-24 w-16 flex-none rounded-lg object-cover shadow-lg shadow-black/40"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0 flex-1 space-y-0.5 font-mono text-xs text-slate-400">
|
||||||
<p>{geraet.modell.length > 0 ? geraet.modell : 'Modell unbekannt'}</p>
|
<p>{geraet.modell.length > 0 ? geraet.modell : 'Modell unbekannt'}</p>
|
||||||
{geraet.status === 'ready' && (
|
{geraet.status === 'ready' && (
|
||||||
<p className="text-slate-200">
|
<p className="text-slate-200">
|
||||||
@@ -136,6 +248,16 @@ function LaufwerkKachel({
|
|||||||
{info.quelle.length > 0 ? ` · ${info.quelle}` : ''}
|
{info.quelle.length > 0 ? ` · ${info.quelle}` : ''}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{geraet.status === 'ready' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setAendernOffen(true)}
|
||||||
|
disabled={ripLaeuft}
|
||||||
|
className="mt-1 rounded border border-slate-600 px-2 py-0.5 text-xs text-slate-300 hover:bg-slate-700 disabled:opacity-40"
|
||||||
|
title="Falsch erkannt? Selbst suchen und wählen — die Ablage folgt deiner Wahl."
|
||||||
|
>
|
||||||
|
Ändern …
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{geraet.status === 'ready' && info !== undefined && info.schonGerippt !== null && (
|
{geraet.status === 'ready' && info !== undefined && info.schonGerippt !== null && (
|
||||||
<p className="text-amber-300">
|
<p className="text-amber-300">
|
||||||
⚠ Diese Disc wurde schon einmal gerippt ({info.schonGerippt.slice(0, 10)}) — siehe Bibliothek.
|
⚠ Diese Disc wurde schon einmal gerippt ({info.schonGerippt.slice(0, 10)}) — siehe Bibliothek.
|
||||||
@@ -147,6 +269,15 @@ function LaufwerkKachel({
|
|||||||
<p className="text-amber-300">{geraet.grund}</p>
|
<p className="text-amber-300">{geraet.grund}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{aendernOffen && (
|
||||||
|
<AendernDialog
|
||||||
|
id={geraet.id}
|
||||||
|
startText={info !== undefined && info.titel.length > 0 ? info.titel : ''}
|
||||||
|
vorschlaege={vorschlaege}
|
||||||
|
schliessen={() => setAendernOffen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{rip !== undefined && (
|
{rip !== undefined && (
|
||||||
<div className="mt-3 space-y-1.5 border-t border-slate-700/60 pt-3">
|
<div className="mt-3 space-y-1.5 border-t border-slate-700/60 pt-3">
|
||||||
{ripLaeuft && (
|
{ripLaeuft && (
|
||||||
@@ -168,7 +299,9 @@ function LaufwerkKachel({
|
|||||||
: 'text-slate-300'
|
: 'text-slate-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{ripLaeuft && rip.prozent >= 0 ? `${rip.prozent} % — ` : ''}
|
{ripLaeuft && rip.prozent >= 0
|
||||||
|
? `${rip.prozent} % ${restzeitText(rip.prozent, ripStartMs)} — `
|
||||||
|
: ''}
|
||||||
{rip.text}
|
{rip.text}
|
||||||
</p>
|
</p>
|
||||||
{rip.dateien.map((datei) => (
|
{rip.dateien.map((datei) => (
|
||||||
@@ -220,12 +353,14 @@ function PresetWahl({
|
|||||||
schluessel,
|
schluessel,
|
||||||
wert,
|
wert,
|
||||||
presets,
|
presets,
|
||||||
|
empfehlung,
|
||||||
aufAendern,
|
aufAendern,
|
||||||
}: {
|
}: {
|
||||||
name: string
|
name: string
|
||||||
schluessel: string
|
schluessel: string
|
||||||
wert: string
|
wert: string
|
||||||
presets: string[]
|
presets: string[]
|
||||||
|
empfehlung: string
|
||||||
aufAendern: (schluessel: string, wert: string) => void
|
aufAendern: (schluessel: string, wert: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -236,7 +371,7 @@ function PresetWahl({
|
|||||||
onChange={(e) => aufAendern(schluessel, e.target.value)}
|
onChange={(e) => aufAendern(schluessel, e.target.value)}
|
||||||
className="mt-1 w-full rounded-lg border border-slate-600 bg-slate-900 px-3 py-1.5 font-mono text-xs text-slate-200 focus:border-sky-400 focus:outline-none"
|
className="mt-1 w-full rounded-lg border border-slate-600 bg-slate-900 px-3 py-1.5 font-mono text-xs text-slate-200 focus:border-sky-400 focus:outline-none"
|
||||||
>
|
>
|
||||||
<option value="">(allgemeines Preset)</option>
|
<option value="">{empfehlung.length > 0 ? `automatisch: ${empfehlung}` : '(allgemeines Preset)'}</option>
|
||||||
<option value="keine">NICHT komprimieren (verlustfrei lassen)</option>
|
<option value="keine">NICHT komprimieren (verlustfrei lassen)</option>
|
||||||
{presets.map((preset) => (
|
{presets.map((preset) => (
|
||||||
<option key={preset} value={preset}>
|
<option key={preset} value={preset}>
|
||||||
@@ -252,6 +387,8 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
|||||||
const werte = verbindung.einstellungen
|
const werte = verbindung.einstellungen
|
||||||
const presets = verbindung.werkzeuge?.presets ?? []
|
const presets = verbindung.werkzeuge?.presets ?? []
|
||||||
const setzen = einstellungSetzen
|
const setzen = einstellungSetzen
|
||||||
|
const empfehlungFuer = (typ: string): string =>
|
||||||
|
presetEmpfehlung(typ, verbindung.werkzeuge?.backends ?? [], presets)
|
||||||
|
|
||||||
const ordnerWaehlen = async (): Promise<void> => {
|
const ordnerWaehlen = async (): Promise<void> => {
|
||||||
const ordner = await window.rippy.ordnerWaehlen('Ablage-Ordner wählen')
|
const ordner = await window.rippy.ordnerWaehlen('Ablage-Ordner wählen')
|
||||||
@@ -290,9 +427,9 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
|||||||
Nach dem Rip komprimieren (der verlustfreie Rip bleibt bis zum Erfolg erhalten)
|
Nach dem Rip komprimieren (der verlustfreie Rip bleibt bis zum Erfolg erhalten)
|
||||||
</label>
|
</label>
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
<PresetWahl name="DVD" schluessel="transcodePresetDvd" wert={werte['transcodePresetDvd'] ?? ''} presets={presets} aufAendern={setzen} />
|
<PresetWahl name="DVD" schluessel="transcodePresetDvd" wert={werte['transcodePresetDvd'] ?? ''} presets={presets} empfehlung={empfehlungFuer('dvd')} aufAendern={setzen} />
|
||||||
<PresetWahl name="Blu-ray" schluessel="transcodePresetBluray" wert={werte['transcodePresetBluray'] ?? ''} presets={presets} aufAendern={setzen} />
|
<PresetWahl name="Blu-ray" schluessel="transcodePresetBluray" wert={werte['transcodePresetBluray'] ?? ''} presets={presets} empfehlung={empfehlungFuer('bluray')} aufAendern={setzen} />
|
||||||
<PresetWahl name="4K-UHD" schluessel="transcodePresetUhd" wert={werte['transcodePresetUhd'] ?? ''} presets={presets} aufAendern={setzen} />
|
<PresetWahl name="4K-UHD" schluessel="transcodePresetUhd" wert={werte['transcodePresetUhd'] ?? ''} presets={presets} empfehlung={empfehlungFuer('uhd')} aufAendern={setzen} />
|
||||||
</div>
|
</div>
|
||||||
<Feld
|
<Feld
|
||||||
name="Allgemeines Preset"
|
name="Allgemeines Preset"
|
||||||
@@ -618,6 +755,8 @@ export default function App() {
|
|||||||
laufwerke: [],
|
laufwerke: [],
|
||||||
rips: {},
|
rips: {},
|
||||||
discInfos: {},
|
discInfos: {},
|
||||||
|
vorschlaege: {},
|
||||||
|
ripStart: {},
|
||||||
einstellungen: {},
|
einstellungen: {},
|
||||||
werkzeuge: null,
|
werkzeuge: null,
|
||||||
bibliothek: [],
|
bibliothek: [],
|
||||||
@@ -689,6 +828,8 @@ export default function App() {
|
|||||||
geraet={geraet}
|
geraet={geraet}
|
||||||
rip={verbindung.rips[geraet.id]}
|
rip={verbindung.rips[geraet.id]}
|
||||||
info={verbindung.discInfos[geraet.id]}
|
info={verbindung.discInfos[geraet.id]}
|
||||||
|
vorschlaege={verbindung.vorschlaege[geraet.id] ?? []}
|
||||||
|
ripStartMs={verbindung.ripStart[geraet.id]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -744,6 +885,8 @@ export default function App() {
|
|||||||
? 'wartet'
|
? 'wartet'
|
||||||
: verbindung.schluessel.angenommen === true
|
: verbindung.schluessel.angenommen === true
|
||||||
? 'ok'
|
? 'ok'
|
||||||
|
: verbindung.schluessel.weiterBetrieb
|
||||||
|
? 'wartet' // gelb: Rips laufen, aber mit Handlungsempfehlung
|
||||||
: 'fehler'
|
: 'fehler'
|
||||||
}
|
}
|
||||||
zeilen={
|
zeilen={
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
type DiscInfoStand,
|
type DiscInfoStand,
|
||||||
type GeraeteInfo,
|
type GeraeteInfo,
|
||||||
type KernNachricht,
|
type KernNachricht,
|
||||||
|
type MetaVorschlag,
|
||||||
type RipStatus,
|
type RipStatus,
|
||||||
type WerkzeugAuskunft,
|
type WerkzeugAuskunft,
|
||||||
} from '../gemeinsam/nachrichten'
|
} from '../gemeinsam/nachrichten'
|
||||||
@@ -26,10 +27,21 @@ export interface Verbindung {
|
|||||||
rips: Record<string, RipStatus>
|
rips: Record<string, RipStatus>
|
||||||
/** Was Rippy über die eingelegte Disc weiß (§ 5 Schritt 3). */
|
/** Was Rippy über die eingelegte Disc weiß (§ 5 Schritt 3). */
|
||||||
discInfos: Record<string, DiscInfoStand>
|
discInfos: Record<string, DiscInfoStand>
|
||||||
|
/** Suchergebnisse des Ändern-Dialogs je Laufwerk (§ 5 Schritt 4). */
|
||||||
|
vorschlaege: Record<string, MetaVorschlag[]>
|
||||||
|
/** Wann der laufende Vorgang je Laufwerk begann (für die Restzeit). */
|
||||||
|
ripStart: Record<string, number>
|
||||||
einstellungen: Record<string, string>
|
einstellungen: Record<string, string>
|
||||||
werkzeuge: WerkzeugAuskunft | null
|
werkzeuge: WerkzeugAuskunft | null
|
||||||
bibliothek: BibliothekEintrag[]
|
bibliothek: BibliothekEintrag[]
|
||||||
schluessel: { quelle: string; angenommen: boolean | null; gueltigBis: string; geholtAm: string; text: string } | null
|
schluessel: {
|
||||||
|
quelle: string
|
||||||
|
angenommen: boolean | null
|
||||||
|
weiterBetrieb: boolean
|
||||||
|
gueltigBis: string
|
||||||
|
geholtAm: string
|
||||||
|
text: string
|
||||||
|
} | null
|
||||||
/** Kurze Ereigniszeilen (neueste zuerst): Disc rein/raus, Auswurf, Fehler. */
|
/** Kurze Ereigniszeilen (neueste zuerst): Disc rein/raus, Auswurf, Fehler. */
|
||||||
meldungen: string[]
|
meldungen: string[]
|
||||||
}
|
}
|
||||||
@@ -45,6 +57,8 @@ const stand: Verbindung = {
|
|||||||
laufwerke: [],
|
laufwerke: [],
|
||||||
rips: {},
|
rips: {},
|
||||||
discInfos: {},
|
discInfos: {},
|
||||||
|
vorschlaege: {},
|
||||||
|
ripStart: {},
|
||||||
einstellungen: {},
|
einstellungen: {},
|
||||||
werkzeuge: null,
|
werkzeuge: null,
|
||||||
bibliothek: [],
|
bibliothek: [],
|
||||||
@@ -62,6 +76,7 @@ function melden(): void {
|
|||||||
laufwerke: [...stand.laufwerke],
|
laufwerke: [...stand.laufwerke],
|
||||||
rips: { ...stand.rips },
|
rips: { ...stand.rips },
|
||||||
discInfos: { ...stand.discInfos },
|
discInfos: { ...stand.discInfos },
|
||||||
|
vorschlaege: { ...stand.vorschlaege },
|
||||||
einstellungen: { ...stand.einstellungen },
|
einstellungen: { ...stand.einstellungen },
|
||||||
bibliothek: [...stand.bibliothek],
|
bibliothek: [...stand.bibliothek],
|
||||||
meldungen: [...stand.meldungen],
|
meldungen: [...stand.meldungen],
|
||||||
@@ -115,6 +130,9 @@ function kernNachricht(nachricht: KernNachricht): void {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'metadaten-vorschlaege':
|
||||||
|
stand.vorschlaege = { ...stand.vorschlaege, [nachricht.id]: nachricht.vorschlaege }
|
||||||
|
break
|
||||||
case 'einstellungen':
|
case 'einstellungen':
|
||||||
stand.einstellungen = nachricht.werte
|
stand.einstellungen = nachricht.werte
|
||||||
break
|
break
|
||||||
@@ -130,6 +148,13 @@ function kernNachricht(nachricht: KernNachricht): void {
|
|||||||
break
|
break
|
||||||
case 'rip-status': {
|
case 'rip-status': {
|
||||||
stand.rips = { ...stand.rips, [nachricht.status.id]: nachricht.status }
|
stand.rips = { ...stand.rips, [nachricht.status.id]: nachricht.status }
|
||||||
|
const laeuft = ['liest-info', 'rippt', 'komprimiert', 'legt-ab'].includes(nachricht.status.phase)
|
||||||
|
if (laeuft && stand.ripStart[nachricht.status.id] === undefined) {
|
||||||
|
stand.ripStart = { ...stand.ripStart, [nachricht.status.id]: Date.now() }
|
||||||
|
} else if (!laeuft) {
|
||||||
|
const { [nachricht.status.id]: _weg, ...rest } = stand.ripStart
|
||||||
|
stand.ripStart = rest
|
||||||
|
}
|
||||||
if (nachricht.status.phase === 'fertig') {
|
if (nachricht.status.phase === 'fertig') {
|
||||||
meldung(
|
meldung(
|
||||||
`Laufwerk ${nachricht.status.id}: Rip fertig — ${nachricht.status.dateien.length} Datei(en)` +
|
`Laufwerk ${nachricht.status.id}: Rip fertig — ${nachricht.status.dateien.length} Datei(en)` +
|
||||||
@@ -187,6 +212,7 @@ export function aufVerbindung(h: Horcher): () => void {
|
|||||||
laufwerke: [...stand.laufwerke],
|
laufwerke: [...stand.laufwerke],
|
||||||
rips: { ...stand.rips },
|
rips: { ...stand.rips },
|
||||||
discInfos: { ...stand.discInfos },
|
discInfos: { ...stand.discInfos },
|
||||||
|
vorschlaege: { ...stand.vorschlaege },
|
||||||
einstellungen: { ...stand.einstellungen },
|
einstellungen: { ...stand.einstellungen },
|
||||||
bibliothek: [...stand.bibliothek],
|
bibliothek: [...stand.bibliothek],
|
||||||
meldungen: [...stand.meldungen],
|
meldungen: [...stand.meldungen],
|
||||||
@@ -215,6 +241,16 @@ export function ripAbbrechen(id: string): void {
|
|||||||
anKern({ art: 'rip-abbruch', id }, `Rip-Abbruch ${id}`)
|
anKern({ art: 'rip-abbruch', id }, `Rip-Abbruch ${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function metadatenSuchen(id: string, text: string): void {
|
||||||
|
if (port === null) return
|
||||||
|
port.postMessage({ art: 'metadaten-suchen', id, text })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zuordnungSetzen(id: string, vorschlag: MetaVorschlag): void {
|
||||||
|
if (port === null) return
|
||||||
|
port.postMessage({ art: 'zuordnung-setzen', id, vorschlag })
|
||||||
|
}
|
||||||
|
|
||||||
export function einstellungSetzen(schluessel: string, wert: string): void {
|
export function einstellungSetzen(schluessel: string, wert: string): void {
|
||||||
if (port === null) {
|
if (port === null) {
|
||||||
meldung(`⚠ Einstellung ${schluessel}: keine Verbindung zum Kern`)
|
meldung(`⚠ Einstellung ${schluessel}: keine Verbindung zum Kern`)
|
||||||
|
|||||||
@@ -85,6 +85,15 @@ export interface BibliothekEintrag {
|
|||||||
dauerS: number
|
dauerS: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ein wählbarer Metadaten-Vorschlag (§ 5 Schritt 4: „nein → fragen"). */
|
||||||
|
export interface MetaVorschlag {
|
||||||
|
typ: 'movie' | 'tv'
|
||||||
|
tmdbId: number
|
||||||
|
titel: string
|
||||||
|
jahr: number | null
|
||||||
|
posterPfad: string
|
||||||
|
}
|
||||||
|
|
||||||
/** Auskunft über die Werkzeuge (Katalog + gemessene HandBrake-Listen). */
|
/** Auskunft über die Werkzeuge (Katalog + gemessene HandBrake-Listen). */
|
||||||
export interface WerkzeugAuskunft {
|
export interface WerkzeugAuskunft {
|
||||||
makemkvcon: string
|
makemkvcon: string
|
||||||
@@ -103,12 +112,21 @@ export type KernNachricht =
|
|||||||
| { art: 'auswurf-ergebnis'; id: string; grund: AuswurfGrund; fehler: string | null }
|
| { art: 'auswurf-ergebnis'; id: string; grund: AuswurfGrund; fehler: string | null }
|
||||||
| { art: 'rip-status'; status: RipStatus }
|
| { art: 'rip-status'; status: RipStatus }
|
||||||
| { art: 'disc-info'; info: DiscInfoStand }
|
| { art: 'disc-info'; info: DiscInfoStand }
|
||||||
|
| { art: 'metadaten-vorschlaege'; id: string; vorschlaege: MetaVorschlag[] }
|
||||||
| { art: 'einstellungen'; werte: Record<string, string> }
|
| { art: 'einstellungen'; werte: Record<string, string> }
|
||||||
| { art: 'werkzeuge'; auskunft: WerkzeugAuskunft }
|
| { art: 'werkzeuge'; auskunft: WerkzeugAuskunft }
|
||||||
| { art: 'bibliothek'; eintraege: BibliothekEintrag[] }
|
| { art: 'bibliothek'; eintraege: BibliothekEintrag[] }
|
||||||
| {
|
| {
|
||||||
art: 'schluessel-status'
|
art: 'schluessel-status'
|
||||||
stand: { quelle: string; angenommen: boolean | null; gueltigBis: string; geholtAm: string; text: string }
|
stand: {
|
||||||
|
quelle: string
|
||||||
|
angenommen: boolean | null
|
||||||
|
/** Rips laufen trotzdem (Beta-Modus) — Warnung, kein Ausfall. */
|
||||||
|
weiterBetrieb: boolean
|
||||||
|
gueltigBis: string
|
||||||
|
geholtAm: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nachrichten, die das Fenster über den MessagePort an den Kern schickt. */
|
/** Nachrichten, die das Fenster über den MessagePort an den Kern schickt. */
|
||||||
@@ -118,6 +136,8 @@ export type FensterNachricht =
|
|||||||
| { art: 'rip-start'; id: string }
|
| { art: 'rip-start'; id: string }
|
||||||
| { art: 'rip-abbruch'; id: string }
|
| { art: 'rip-abbruch'; id: string }
|
||||||
| { art: 'einstellung-setzen'; schluessel: string; wert: string }
|
| { art: 'einstellung-setzen'; schluessel: string; wert: string }
|
||||||
|
| { art: 'metadaten-suchen'; id: string; text: string }
|
||||||
|
| { art: 'zuordnung-setzen'; id: string; vorschlag: MetaVorschlag }
|
||||||
| { art: 'einstellungen-laden' }
|
| { art: 'einstellungen-laden' }
|
||||||
| { art: 'werkzeuge-laden' }
|
| { art: 'werkzeuge-laden' }
|
||||||
| { art: 'bibliothek-laden' }
|
| { art: 'bibliothek-laden' }
|
||||||
@@ -178,11 +198,24 @@ export function istKernNachricht(wert: unknown): wert is KernNachricht {
|
|||||||
return Array.isArray(wert.eintraege)
|
return Array.isArray(wert.eintraege)
|
||||||
case 'schluessel-status':
|
case 'schluessel-status':
|
||||||
return istObjekt(wert.stand) && typeof (wert.stand as Record<string, unknown>).text === 'string'
|
return istObjekt(wert.stand) && typeof (wert.stand as Record<string, unknown>).text === 'string'
|
||||||
|
case 'metadaten-vorschlaege':
|
||||||
|
return typeof wert.id === 'string' && Array.isArray(wert.vorschlaege)
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function istMetaVorschlag(wert: unknown): wert is MetaVorschlag {
|
||||||
|
if (!istObjekt(wert)) return false
|
||||||
|
return (
|
||||||
|
(wert.typ === 'movie' || wert.typ === 'tv') &&
|
||||||
|
typeof wert.tmdbId === 'number' &&
|
||||||
|
typeof wert.titel === 'string' &&
|
||||||
|
(wert.jahr === null || typeof wert.jahr === 'number') &&
|
||||||
|
typeof wert.posterPfad === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function istRipStatus(wert: unknown): wert is RipStatus {
|
export function istRipStatus(wert: unknown): wert is RipStatus {
|
||||||
if (!istObjekt(wert)) return false
|
if (!istObjekt(wert)) return false
|
||||||
return (
|
return (
|
||||||
@@ -211,6 +244,12 @@ export function istFensterNachricht(wert: unknown): wert is FensterNachricht {
|
|||||||
if (wert.art === 'einstellung-setzen') {
|
if (wert.art === 'einstellung-setzen') {
|
||||||
return typeof wert.schluessel === 'string' && wert.schluessel.length > 0 && typeof wert.wert === 'string'
|
return typeof wert.schluessel === 'string' && wert.schluessel.length > 0 && typeof wert.wert === 'string'
|
||||||
}
|
}
|
||||||
|
if (wert.art === 'metadaten-suchen') {
|
||||||
|
return typeof wert.id === 'string' && wert.id.length > 0 && typeof wert.text === 'string'
|
||||||
|
}
|
||||||
|
if (wert.art === 'zuordnung-setzen') {
|
||||||
|
return typeof wert.id === 'string' && wert.id.length > 0 && istMetaVorschlag(wert.vorschlag)
|
||||||
|
}
|
||||||
return wert.art === 'einstellungen-laden' || wert.art === 'werkzeuge-laden' || wert.art === 'bibliothek-laden'
|
return wert.art === 'einstellungen-laden' || wert.art === 'werkzeuge-laden' || wert.art === 'bibliothek-laden'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Die Preset-Empfehlung — GEMESSEN, nicht behauptet (Commander-Fund
|
||||||
|
// 30.08.2026: „Das Komprimieren passiert auf der CPU, nicht auf der
|
||||||
|
// deutlich schnelleren GPU"). Der alte Default „H.265 MKV 1080p30" ist
|
||||||
|
// ein CPU-x265-Preset; auf dem Commander-PC kodiert die RX 9070 XT
|
||||||
|
// (VCN) denselben Film in 2 statt 60+ Minuten.
|
||||||
|
//
|
||||||
|
// Regeln:
|
||||||
|
// * Empfohlen wird NUR ein Preset, das in der vom eigenen HandBrake
|
||||||
|
// gemessenen Liste WIRKLICH steht (encoder.ts) — nie ein geratener
|
||||||
|
// Name (die „Invalid preset"-Falle).
|
||||||
|
// * Familienreihenfolge nach den gemessenen Backends: VCN (AMD) →
|
||||||
|
// NVENC (NVIDIA) → QSV (Intel) → CPU-Default.
|
||||||
|
// * HandBrake-Presets DECKELN die Auflösung nur — eine DVD bleibt SD,
|
||||||
|
// auch unter „…1080p" (deshalb ist die 1080p-Empfehlung auch für
|
||||||
|
// DVDs richtig; hochskaliert wird nichts).
|
||||||
|
// Liegt hier in gemeinsam/, weil Kern (Pipeline-Fallback) UND Fenster
|
||||||
|
// (Anzeige „automatisch: …") dieselbe Rechnung brauchen.
|
||||||
|
|
||||||
|
export const DEFAULT_HB_PRESET = 'H.265 MKV 1080p30'
|
||||||
|
|
||||||
|
const FAMILIEN: ReadonlyArray<readonly [backend: string, presetKennung: string]> = [
|
||||||
|
['vce', 'VCN'],
|
||||||
|
['nvenc', 'NVENC'],
|
||||||
|
['qsv', 'QSV'],
|
||||||
|
]
|
||||||
|
|
||||||
|
/** Das empfohlene Preset für diesen Disc-Typ — aus den GEMESSENEN
|
||||||
|
* Backends und Preset-Namen. Fällt auf den CPU-Default zurück. */
|
||||||
|
export function presetEmpfehlung(
|
||||||
|
discTyp: string,
|
||||||
|
backends: readonly string[],
|
||||||
|
presets: readonly string[],
|
||||||
|
): string {
|
||||||
|
const gewuenscht = discTyp === 'uhd' ? '2160p 4K' : '1080p'
|
||||||
|
for (const [backend, kennung] of FAMILIEN) {
|
||||||
|
if (!backends.includes(backend)) continue
|
||||||
|
const treffer = presets.find((p) => p.startsWith('H.265 ') && p.includes(kennung) && p.includes(gewuenscht))
|
||||||
|
if (treffer !== undefined) return treffer
|
||||||
|
// 4K-Familie ohne 4K-Preset: das 1080p-Pendant ist besser als CPU.
|
||||||
|
const kleiner = presets.find((p) => p.startsWith('H.265 ') && p.includes(kennung))
|
||||||
|
if (kleiner !== undefined) return kleiner
|
||||||
|
}
|
||||||
|
return DEFAULT_HB_PRESET
|
||||||
|
}
|
||||||
@@ -53,6 +53,9 @@ export interface PipelineUmgebung {
|
|||||||
/** Wurzel der Ablage (Roh-Ordner und fertige Filme liegen darunter). */
|
/** Wurzel der Ablage (Roh-Ordner und fertige Filme liegen darunter). */
|
||||||
ablage: () => string
|
ablage: () => string
|
||||||
einstellungen: () => Einstellungen
|
einstellungen: () => Einstellungen
|
||||||
|
/** Gemessene Preset-Empfehlung (Hardware zuerst) für diesen Disc-Typ —
|
||||||
|
* greift nur, wenn der Nutzer nichts eingestellt hat. */
|
||||||
|
presetEmpfehlung?: (discTyp: string) => string
|
||||||
melden: (status: RipStatus) => void
|
melden: (status: RipStatus) => void
|
||||||
/** Die letzte Metadaten-Zuordnung dieses Laufwerks (W-4) — oder null. */
|
/** Die letzte Metadaten-Zuordnung dieses Laufwerks (W-4) — oder null. */
|
||||||
zuordnung?: (id: string) => Zuordnung | null
|
zuordnung?: (id: string) => Zuordnung | null
|
||||||
@@ -204,7 +207,7 @@ export class Pipeline {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const preset = presetFuer(geraet.typ, einstellungen)
|
const preset = presetFuer(geraet.typ, einstellungen, this.umgebung.presetEmpfehlung?.(geraet.typ) ?? '')
|
||||||
// Mit Metadaten (W-4) heißt der Ordner „<Titel> (Jahr)" — daran
|
// Mit Metadaten (W-4) heißt der Ordner „<Titel> (Jahr)" — daran
|
||||||
// erkennen Jellyfin & Co. den Film; ohne bleibt der Vorgangsname.
|
// erkennen Jellyfin & Co. den Film; ohne bleibt der Vorgangsname.
|
||||||
const zuordnung = this.umgebung.zuordnung?.(id) ?? null
|
const zuordnung = this.umgebung.zuordnung?.(id) ?? null
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
istHauptNachricht,
|
istHauptNachricht,
|
||||||
type DbStatus,
|
type DbStatus,
|
||||||
type KernNachricht,
|
type KernNachricht,
|
||||||
|
type MetaVorschlag,
|
||||||
} from '../gemeinsam/nachrichten'
|
} from '../gemeinsam/nachrichten'
|
||||||
import { Pipeline } from './ablauf/pipeline'
|
import { Pipeline } from './ablauf/pipeline'
|
||||||
import type { LaufwerkApi } from './laufwerk/codes'
|
import type { LaufwerkApi } from './laufwerk/codes'
|
||||||
@@ -29,6 +30,7 @@ import { discFingerprint, discTitel } from './metadaten/discmerkmale'
|
|||||||
import { OmdbClient } from './metadaten/omdb'
|
import { OmdbClient } from './metadaten/omdb'
|
||||||
import { TmdbClient } from './metadaten/tmdb'
|
import { TmdbClient } from './metadaten/tmdb'
|
||||||
import { zuordnen, type Zuordnung } from './metadaten/zuordnung'
|
import { zuordnen, type Zuordnung } from './metadaten/zuordnung'
|
||||||
|
import { presetEmpfehlung } from '../gemeinsam/preset-empfehlung'
|
||||||
import { Datenbank } from './speicher/db'
|
import { Datenbank } from './speicher/db'
|
||||||
import { finden } from './werkzeuge/katalog'
|
import { finden } from './werkzeuge/katalog'
|
||||||
|
|
||||||
@@ -124,6 +126,85 @@ async function discErmitteln(id: string, geraetepfad: string, groesseBytes: numb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// § 5 Schritt 4: „nein → fragen" — der Nutzer sucht und WÄHLT selbst.
|
||||||
|
function jahrAus(datum: string | undefined): number | null {
|
||||||
|
if (datum === undefined || datum.length < 4) return null
|
||||||
|
const jahr = Number(datum.slice(0, 4))
|
||||||
|
return Number.isFinite(jahr) && jahr > 0 ? jahr : null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function metadatenSuchen(id: string, text: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tmdb = new TmdbClient(db?.einstellungLesen('tmdbApiKey') ?? '')
|
||||||
|
if (!tmdb.verfuegbar) {
|
||||||
|
anFenster({ art: 'kern-fehler', text: 'Metadaten-Suche braucht einen TMDb-Key (Einstellungen → Metadaten).' })
|
||||||
|
anFenster({ art: 'metadaten-vorschlaege', id, vorschlaege: [] })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [filme, serien] = await Promise.all([tmdb.searchMovie(text), tmdb.searchTv(text)])
|
||||||
|
const vorschlaege: MetaVorschlag[] = [
|
||||||
|
...(filme ?? []).slice(0, 6).map((f) => ({
|
||||||
|
typ: 'movie' as const,
|
||||||
|
tmdbId: f.id,
|
||||||
|
titel: f.title ?? '',
|
||||||
|
jahr: jahrAus(f.release_date),
|
||||||
|
posterPfad: f.poster_path ?? '',
|
||||||
|
})),
|
||||||
|
...(serien ?? []).slice(0, 6).map((s) => ({
|
||||||
|
typ: 'tv' as const,
|
||||||
|
tmdbId: s.id,
|
||||||
|
titel: s.name ?? '',
|
||||||
|
jahr: jahrAus(s.first_air_date),
|
||||||
|
posterPfad: s.poster_path ?? '',
|
||||||
|
})),
|
||||||
|
].filter((v) => v.titel.length > 0)
|
||||||
|
anFenster({ art: 'metadaten-vorschlaege', id, vorschlaege })
|
||||||
|
} catch (fehler) {
|
||||||
|
anFenster({ art: 'kern-fehler', text: `Metadaten-Suche: ${String(fehler)}` })
|
||||||
|
anFenster({ art: 'metadaten-vorschlaege', id, vorschlaege: [] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function zuordnungSetzen(id: string, vorschlag: MetaVorschlag): Promise<void> {
|
||||||
|
try {
|
||||||
|
const tmdb = new TmdbClient(db?.einstellungLesen('tmdbApiKey') ?? '')
|
||||||
|
const details =
|
||||||
|
vorschlag.typ === 'movie' ? await tmdb.movieDetails(vorschlag.tmdbId) : await tmdb.tvDetails(vorschlag.tmdbId)
|
||||||
|
const zuordnung: Zuordnung = {
|
||||||
|
titel: (vorschlag.typ === 'movie' ? details?.title : details?.name) ?? vorschlag.titel,
|
||||||
|
jahr: jahrAus(vorschlag.typ === 'movie' ? details?.release_date : details?.first_air_date) ?? vorschlag.jahr,
|
||||||
|
// Vom Nutzer gewählt = sicher. Genau dafür ist die Frage da (§ 5).
|
||||||
|
confidence: 1,
|
||||||
|
meta: {
|
||||||
|
typ: vorschlag.typ,
|
||||||
|
id: vorschlag.tmdbId,
|
||||||
|
titel: (vorschlag.typ === 'movie' ? details?.title : details?.name) ?? vorschlag.titel,
|
||||||
|
jahr: jahrAus(vorschlag.typ === 'movie' ? details?.release_date : details?.first_air_date) ?? vorschlag.jahr,
|
||||||
|
beschreibung: details?.overview ?? '',
|
||||||
|
posterPfad: details?.poster_path ?? vorschlag.posterPfad,
|
||||||
|
laufzeitMinuten: details?.runtime ?? 0,
|
||||||
|
genres: (details?.genres ?? []).map((g) => g.name),
|
||||||
|
quelle: 'tmdb',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
discZuordnungen.set(id, zuordnung)
|
||||||
|
anHauptUndFenster({
|
||||||
|
art: 'disc-info',
|
||||||
|
info: {
|
||||||
|
id,
|
||||||
|
titel: zuordnung.titel,
|
||||||
|
jahr: zuordnung.jahr,
|
||||||
|
confidence: 1,
|
||||||
|
quelle: 'deine Wahl',
|
||||||
|
posterPfad: zuordnung.meta.posterPfad,
|
||||||
|
schonGerippt: db?.bibliothekSchonGerippt(discFingerprints.get(id) ?? '') ?? null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (fehler) {
|
||||||
|
anFenster({ art: 'kern-fehler', text: `Zuordnung übernehmen: ${String(fehler)}` })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const laufwerkeBereit: Promise<void> = (async () => {
|
const laufwerkeBereit: Promise<void> = (async () => {
|
||||||
try {
|
try {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
@@ -229,6 +310,7 @@ const ripVerwaltung = new Pipeline({
|
|||||||
},
|
},
|
||||||
ablage: ablageOrdner,
|
ablage: ablageOrdner,
|
||||||
einstellungen: einstellungenLaden,
|
einstellungen: einstellungenLaden,
|
||||||
|
presetEmpfehlung: (typ) => presetEmpfehlung(typ, werkzeugAuskunft.backends, werkzeugAuskunft.presets),
|
||||||
melden: (status) => anHauptUndFenster({ art: 'rip-status', status }),
|
melden: (status) => anHauptUndFenster({ art: 'rip-status', status }),
|
||||||
zuordnung: (id) => discZuordnungen.get(id) ?? null,
|
zuordnung: (id) => discZuordnungen.get(id) ?? null,
|
||||||
fingerprint: (id) => discFingerprints.get(id) ?? null,
|
fingerprint: (id) => discFingerprints.get(id) ?? null,
|
||||||
@@ -303,12 +385,17 @@ async function schluesselLauf(): Promise<void> {
|
|||||||
// Täglich neu prüfen (§ 9.1) — ohne den Prozess-Exit aufzuhalten.
|
// Täglich neu prüfen (§ 9.1) — ohne den Prozess-Exit aufzuhalten.
|
||||||
setInterval(() => void schluesselLauf(), 24 * 60 * 60 * 1000).unref()
|
setInterval(() => void schluesselLauf(), 24 * 60 * 60 * 1000).unref()
|
||||||
|
|
||||||
|
// Die gemessene Auskunft bleibt liegen — die Pipeline braucht sie für die
|
||||||
|
// Preset-Empfehlung, auch wenn gerade kein Fenster offen ist.
|
||||||
|
let werkzeugAuskunft: { presets: string[]; backends: string[] } = { presets: [], backends: [] }
|
||||||
|
|
||||||
async function werkzeugeSenden(): Promise<void> {
|
async function werkzeugeSenden(): Promise<void> {
|
||||||
const makemkvcon = finden('makemkv', db?.einstellungLesen('werkzeug.makemkv') ?? '')
|
const makemkvcon = finden('makemkv', db?.einstellungLesen('werkzeug.makemkv') ?? '')
|
||||||
const handbrake = finden('handbrake', db?.einstellungLesen('werkzeug.handbrake') ?? '')
|
const handbrake = finden('handbrake', db?.einstellungLesen('werkzeug.handbrake') ?? '')
|
||||||
const flac = finden('flac', db?.einstellungLesen('werkzeug.flac') ?? '')
|
const flac = finden('flac', db?.einstellungLesen('werkzeug.flac') ?? '')
|
||||||
const { encoderMessen } = await import('./komprimieren/encoder')
|
const { encoderMessen } = await import('./komprimieren/encoder')
|
||||||
const gemessen = await encoderMessen(handbrake)
|
const gemessen = await encoderMessen(handbrake)
|
||||||
|
werkzeugAuskunft = { presets: gemessen.presets, backends: gemessen.backends }
|
||||||
anFenster({
|
anFenster({
|
||||||
art: 'werkzeuge',
|
art: 'werkzeuge',
|
||||||
auskunft: { makemkvcon, handbrake, flac, presets: gemessen.presets, backends: gemessen.backends },
|
auskunft: { makemkvcon, handbrake, flac, presets: gemessen.presets, backends: gemessen.backends },
|
||||||
@@ -362,6 +449,10 @@ process.parentPort.on('message', (ereignis) => {
|
|||||||
void laufwerkeBereit.then(() => ripVerwaltung.starten(frage.id))
|
void laufwerkeBereit.then(() => ripVerwaltung.starten(frage.id))
|
||||||
} else if (frage.art === 'rip-abbruch') {
|
} else if (frage.art === 'rip-abbruch') {
|
||||||
ripVerwaltung.abbrechen(frage.id)
|
ripVerwaltung.abbrechen(frage.id)
|
||||||
|
} else if (frage.art === 'metadaten-suchen') {
|
||||||
|
void metadatenSuchen(frage.id, frage.text)
|
||||||
|
} else if (frage.art === 'zuordnung-setzen') {
|
||||||
|
void zuordnungSetzen(frage.id, frage.vorschlag)
|
||||||
} else if (frage.art === 'einstellung-setzen') {
|
} else if (frage.art === 'einstellung-setzen') {
|
||||||
if (!ERLAUBTE_EINSTELLUNGEN.has(frage.schluessel)) {
|
if (!ERLAUBTE_EINSTELLUNGEN.has(frage.schluessel)) {
|
||||||
anFenster({ art: 'kern-fehler', text: `Unbekannter Einstellungs-Schlüssel: ${frage.schluessel}` })
|
anFenster({ art: 'kern-fehler', text: `Unbekannter Einstellungs-Schlüssel: ${frage.schluessel}` })
|
||||||
@@ -410,4 +501,7 @@ einstellungenSenden()
|
|||||||
// der Gerüst-Beweis soll ohne Netz und ohne MakeMKV grün sein.
|
// der Gerüst-Beweis soll ohne Netz und ohne MakeMKV grün sein.
|
||||||
if (!process.argv.includes('--smoke-kern') && process.env['RIPPY_OHNE_SCHLUESSEL'] !== '1') {
|
if (!process.argv.includes('--smoke-kern') && process.env['RIPPY_OHNE_SCHLUESSEL'] !== '1') {
|
||||||
void schluesselLauf()
|
void schluesselLauf()
|
||||||
|
// Werkzeuge einmal beim Start messen — die Preset-Empfehlung der
|
||||||
|
// Pipeline braucht Backends und Preset-Liste auch ohne offenes Fenster.
|
||||||
|
void werkzeugeSenden()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
// mit den Befunden vom 25.07.2026 begründet: EIN Preset für alles rechnete
|
// mit den Befunden vom 25.07.2026 begründet: EIN Preset für alles rechnete
|
||||||
// 4K auf 1080p herunter und DVDs hinauf.
|
// 4K auf 1080p herunter und DVDs hinauf.
|
||||||
|
|
||||||
export const DEFAULT_HB_PRESET = 'H.265 MKV 1080p30'
|
import { DEFAULT_HB_PRESET, presetEmpfehlung } from '../../gemeinsam/preset-empfehlung'
|
||||||
|
|
||||||
|
export { DEFAULT_HB_PRESET, presetEmpfehlung }
|
||||||
|
|
||||||
/** Einstellungs-Schlüssel je Disc-Typ. */
|
/** Einstellungs-Schlüssel je Disc-Typ. */
|
||||||
export const PRESET_SCHLUESSEL: Record<string, string> = {
|
export const PRESET_SCHLUESSEL: Record<string, string> = {
|
||||||
@@ -32,17 +34,19 @@ export function komprimierenFuer(discTyp: string, einstellungen: Einstellungen):
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Welches HandBrake-Preset gilt für diesen Disc-Typ? (pur)
|
/** Welches HandBrake-Preset gilt für diesen Disc-Typ? (pur)
|
||||||
* Kette: Typ-Preset → allgemeines transcodePreset → DEFAULT. PRESET_KEINE
|
* Kette: Typ-Preset → allgemeines transcodePreset → EMPFEHLUNG (gemessen,
|
||||||
* wird ÜBERSPRUNGEN — diese Funktion darf nie „keine" als Preset-Namen
|
* Hardware zuerst) → CPU-DEFAULT. PRESET_KEINE wird ÜBERSPRUNGEN — diese
|
||||||
* liefern, sonst bekäme HandBrake `--preset keine` und scheiterte. */
|
* Funktion darf nie „keine" als Preset-Namen liefern, sonst bekäme
|
||||||
export function presetFuer(discTyp: string, einstellungen: Einstellungen): string {
|
* HandBrake `--preset keine` und scheiterte. */
|
||||||
|
export function presetFuer(discTyp: string, einstellungen: Einstellungen, empfehlung = ''): string {
|
||||||
const schluessel = PRESET_SCHLUESSEL[discTyp] ?? ''
|
const schluessel = PRESET_SCHLUESSEL[discTyp] ?? ''
|
||||||
if (schluessel.length > 0) {
|
if (schluessel.length > 0) {
|
||||||
const eigen = String(einstellungen[schluessel] ?? '').trim()
|
const eigen = String(einstellungen[schluessel] ?? '').trim()
|
||||||
if (eigen.length > 0 && eigen !== PRESET_KEINE) return eigen
|
if (eigen.length > 0 && eigen !== PRESET_KEINE) return eigen
|
||||||
}
|
}
|
||||||
const allgemein = String(einstellungen['transcodePreset'] ?? '').trim()
|
const allgemein = String(einstellungen['transcodePreset'] ?? '').trim()
|
||||||
return allgemein.length > 0 ? allgemein : DEFAULT_HB_PRESET
|
if (allgemein.length > 0) return allgemein
|
||||||
|
return empfehlung.length > 0 ? empfehlung : DEFAULT_HB_PRESET
|
||||||
}
|
}
|
||||||
|
|
||||||
/** „deu, eng , ,DEU" → ["deu","eng"] — HandBrake will ISO-639-2 klein;
|
/** „deu, eng , ,DEU" → ["deu","eng"] — HandBrake will ISO-639-2 klein;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export interface FilmTreffer {
|
|||||||
id: number
|
id: number
|
||||||
title?: string
|
title?: string
|
||||||
name?: string
|
name?: string
|
||||||
|
poster_path?: string
|
||||||
|
release_date?: string
|
||||||
|
first_air_date?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilmDetails {
|
export interface FilmDetails {
|
||||||
|
|||||||
@@ -91,15 +91,24 @@ export interface KeyUrteil {
|
|||||||
ok: boolean
|
ok: boolean
|
||||||
code: number | null
|
code: number | null
|
||||||
grund: string
|
grund: string
|
||||||
|
/** Die MakeMKV-Version aus MSG 1005 („MakeMKV v1.18.4 …") — '' wenn
|
||||||
|
* nicht erkennbar. */
|
||||||
|
version: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nimmt MakeMKV den hinterlegten Schlüssel an? — es selbst gefragt:
|
/** Nimmt MakeMKV den hinterlegten Schlüssel an? — es selbst gefragt:
|
||||||
* `info disc:9999` braucht keinen Datenträger, die Freischaltungs-Meldung
|
* `info disc:9999` braucht keinen Datenträger, die Freischaltungs-Meldung
|
||||||
* kommt trotzdem (makemkv_aufruf.py, 29.08.2026). */
|
* kommt trotzdem (makemkv_aufruf.py, 29.08.2026).
|
||||||
|
*
|
||||||
|
* ⚠ ALLE Codes einsammeln, dann urteilen: Beim Commander kamen 5020 UND
|
||||||
|
* 5021 zusammen (30.08.2026 live gemessen) — 5020 allein klingt nach
|
||||||
|
* „Schlüssel kaputt", die URSACHE steht aber in 5021: MakeMKV ist zu alt
|
||||||
|
* für den aktuellen Beta-Key. Wer beim ersten Code abbricht, beschuldigt
|
||||||
|
* den falschen. */
|
||||||
export function keyPruefen(programm: string, timeoutMs = 120_000): Promise<KeyUrteil> {
|
export function keyPruefen(programm: string, timeoutMs = 120_000): Promise<KeyUrteil> {
|
||||||
return new Promise((fertig) => {
|
return new Promise((fertig) => {
|
||||||
if (programm.length === 0) {
|
if (programm.length === 0) {
|
||||||
fertig({ ok: false, code: null, grund: 'MakeMKV nicht gefunden' })
|
fertig({ ok: false, code: null, grund: 'MakeMKV nicht gefunden', version: '' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const kind = spawn(programm, ['-r', '--noscan', 'info', 'disc:9999'], {
|
const kind = spawn(programm, ['-r', '--noscan', 'info', 'disc:9999'], {
|
||||||
@@ -112,28 +121,43 @@ export function keyPruefen(programm: string, timeoutMs = 120_000): Promise<KeyUr
|
|||||||
const zeiger = setTimeout(() => kind.kill(), timeoutMs)
|
const zeiger = setTimeout(() => kind.kill(), timeoutMs)
|
||||||
kind.on('error', (fehler) => {
|
kind.on('error', (fehler) => {
|
||||||
clearTimeout(zeiger)
|
clearTimeout(zeiger)
|
||||||
fertig({ ok: false, code: null, grund: `MakeMKV nicht befragbar (${String(fehler)})` })
|
fertig({ ok: false, code: null, grund: `MakeMKV nicht befragbar (${String(fehler)})`, version: '' })
|
||||||
})
|
})
|
||||||
kind.on('close', () => {
|
kind.on('close', () => {
|
||||||
clearTimeout(zeiger)
|
clearTimeout(zeiger)
|
||||||
for (const zeile of textVon(Buffer.concat(stuecke)).split(/\r?\n/)) {
|
fertig(urteilAusAusgabe(textVon(Buffer.concat(stuecke))))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die Ausgabe eines info-Laufs → Urteil (pur, testbar). */
|
||||||
|
export function urteilAusAusgabe(ausgabe: string): KeyUrteil {
|
||||||
|
const codes: number[] = []
|
||||||
|
let version = ''
|
||||||
|
for (const zeile of ausgabe.split(/\r?\n/)) {
|
||||||
if (!zeile.startsWith('MSG:')) continue
|
if (!zeile.startsWith('MSG:')) continue
|
||||||
const code = Number(zeile.slice(4).split(',', 1)[0])
|
const code = Number(zeile.slice(4).split(',', 1)[0])
|
||||||
if (!Number.isInteger(code)) continue
|
if (!Number.isInteger(code)) continue
|
||||||
if (KEY_ABGELEHNT.includes(code) || code === 5051) {
|
if (code === 1005) {
|
||||||
fertig({ ok: false, code, grund: KRITISCHE_CODES[code] ?? `MSG ${code}` })
|
const treffer = /MakeMKV v([0-9][0-9.]*)/.exec(zeile)
|
||||||
return
|
if (treffer !== null) version = treffer[1]
|
||||||
}
|
}
|
||||||
|
if (KEY_ABGELEHNT.includes(code) || code === 5051) codes.push(code)
|
||||||
}
|
}
|
||||||
fertig({ ok: true, code: null, grund: '' })
|
if (codes.length === 0) return { ok: true, code: null, grund: '', version }
|
||||||
})
|
// 5021 schlägt alles: Es benennt die URSACHE (Version zu alt) — 5020
|
||||||
})
|
// ist dann nur die Folge und würde den Schlüssel zu Unrecht beschuldigen.
|
||||||
|
const code = codes.includes(5021) ? 5021 : codes[0]
|
||||||
|
return { ok: false, code, grund: KRITISCHE_CODES[code] ?? `MSG ${code}`, version }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SchluesselStand {
|
export interface SchluesselStand {
|
||||||
/** 'dauerlizenz' | 'forum' | 'keiner' */
|
/** 'dauerlizenz' | 'forum' | 'keiner' */
|
||||||
quelle: string
|
quelle: string
|
||||||
angenommen: boolean | null
|
angenommen: boolean | null
|
||||||
|
/** Laufen Rips trotzdem? (Beta-Modus ohne Key = ja — das ist eine
|
||||||
|
* WARNUNG mit Handlungsempfehlung, kein Ausfall.) */
|
||||||
|
weiterBetrieb: boolean
|
||||||
gueltigBis: string
|
gueltigBis: string
|
||||||
geholtAm: string
|
geholtAm: string
|
||||||
text: string
|
text: string
|
||||||
@@ -155,9 +179,12 @@ export async function schluesselKette(
|
|||||||
return {
|
return {
|
||||||
quelle: 'dauerlizenz',
|
quelle: 'dauerlizenz',
|
||||||
angenommen: urteil.ok,
|
angenommen: urteil.ok,
|
||||||
|
weiterBetrieb: urteil.ok,
|
||||||
gueltigBis: '',
|
gueltigBis: '',
|
||||||
geholtAm: jetztIso,
|
geholtAm: jetztIso,
|
||||||
text: urteil.ok ? 'Eigener Schlüssel hinterlegt und angenommen.' : `Eigener Schlüssel abgelehnt: ${urteil.grund}`,
|
text: urteil.ok
|
||||||
|
? `Eigener Schlüssel hinterlegt und von MakeMKV${urteil.version.length > 0 ? ` ${urteil.version}` : ''} angenommen.`
|
||||||
|
: `Eigener Schlüssel abgelehnt: ${urteil.grund}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +196,7 @@ export async function schluesselKette(
|
|||||||
return {
|
return {
|
||||||
quelle: 'keiner',
|
quelle: 'keiner',
|
||||||
angenommen: urteil.ok,
|
angenommen: urteil.ok,
|
||||||
|
weiterBetrieb: true,
|
||||||
gueltigBis: '',
|
gueltigBis: '',
|
||||||
geholtAm: jetztIso,
|
geholtAm: jetztIso,
|
||||||
text: urteil.ok
|
text: urteil.ok
|
||||||
@@ -181,15 +209,25 @@ export async function schluesselKette(
|
|||||||
const urteil = await keyPruefen(makemkvcon)
|
const urteil = await keyPruefen(makemkvcon)
|
||||||
if (!urteil.ok) {
|
if (!urteil.ok) {
|
||||||
// Ein abgelehnter Schlüssel ist schlimmer als keiner — zurücknehmen.
|
// Ein abgelehnter Schlüssel ist schlimmer als keiner — zurücknehmen.
|
||||||
|
// (Live gemessen 30.08.2026: MIT abgelehntem Key verweigert MakeMKV
|
||||||
|
// alles; OHNE läuft es im Beta-Modus weiter und rippt.)
|
||||||
keyZuruecknehmen()
|
keyZuruecknehmen()
|
||||||
}
|
}
|
||||||
|
const versionsText = urteil.version.length > 0 ? ` ${urteil.version}` : ''
|
||||||
return {
|
return {
|
||||||
quelle: 'forum',
|
quelle: 'forum',
|
||||||
angenommen: urteil.ok,
|
angenommen: urteil.ok,
|
||||||
|
// Der Zurücknahme-Weg lässt MakeMKV im Beta-Modus weiterlaufen —
|
||||||
|
// Rips gehen, nur eben ohne Freischaltungs-Komfort.
|
||||||
|
weiterBetrieb: true,
|
||||||
gueltigBis: forum.gueltigBis,
|
gueltigBis: forum.gueltigBis,
|
||||||
geholtAm: jetztIso,
|
geholtAm: jetztIso,
|
||||||
text: urteil.ok
|
text: urteil.ok
|
||||||
? `Beta-Key aus dem Forum hinterlegt${forum.gueltigBis.length > 0 ? ` — gültig bis ${forum.gueltigBis}` : ''}.`
|
? `Beta-Key aus dem Forum hinterlegt und von MakeMKV${versionsText} angenommen${forum.gueltigBis.length > 0 ? ` — gültig bis ${forum.gueltigBis}` : ''}.`
|
||||||
: `Beta-Key abgelehnt (${urteil.grund}) — wieder entfernt. Vermutlich ist MakeMKV zu alt.`,
|
: urteil.code === 5021
|
||||||
|
? `Der aktuelle Beta-Key ist in Ordnung — aber MakeMKV${versionsText} ist ZU ALT für ihn. ` +
|
||||||
|
'Rippy hat ihn wieder entfernt, damit MakeMKV im Beta-Modus weiterläuft (so klappen Rips trotzdem). ' +
|
||||||
|
'Abhilfe: MakeMKV von makemkv.com aktualisieren — danach hinterlegt Rippy den Key automatisch neu.'
|
||||||
|
: `Beta-Key abgelehnt (${urteil.grund}) — wieder entfernt, damit MakeMKV im Beta-Modus weiterläuft.`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Die drei Funde aus dem ersten Commander-Test (30.08.2026, abends) —
|
||||||
|
// jeder als Test festgehalten, mit den LIVE gemessenen Fixtures.
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { urteilAusAusgabe } from '../src/kern/werkzeuge/schluessel'
|
||||||
|
import { presetEmpfehlung, DEFAULT_HB_PRESET } from '../src/gemeinsam/preset-empfehlung'
|
||||||
|
import { presetFuer } from '../src/kern/komprimieren/presets'
|
||||||
|
|
||||||
|
describe('Fund 1 — „Key korrekt, aber als ungültig angezeigt"', () => {
|
||||||
|
// Die ECHTE makemkvcon-Ausgabe vom Commander-PC (30.08.2026): Der
|
||||||
|
// aktuelle Forum-Key ist in Ordnung, MakeMKV 1.18.4 ist ZU ALT für ihn
|
||||||
|
// — es meldet 5020 UND 5021 zusammen.
|
||||||
|
const ECHTE_AUSGABE = [
|
||||||
|
'MSG:1005,0,1,"MakeMKV v1.18.4 win(x64-release) gestartet","%1 gestartet","MakeMKV v1.18.4 win(x64-release)"',
|
||||||
|
'MSG:5020,516,0,"Der hinterlegte Aktivierungsschlüssel ist ungültig. …","…"',
|
||||||
|
'MSG:5021,131332,1,"Diese Programmversion ist zu alt. Bitte laden Sie die aktuelle Version von http://www.makemkv.com/ herunter.","…","http://www.makemkv.com/"',
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
it('5021 schlägt 5020: Die Ursache ist die Version, nicht der Schlüssel', () => {
|
||||||
|
const urteil = urteilAusAusgabe(ECHTE_AUSGABE)
|
||||||
|
expect(urteil.ok).toBe(false)
|
||||||
|
expect(urteil.code).toBe(5021)
|
||||||
|
expect(urteil.grund).toContain('zu alt')
|
||||||
|
expect(urteil.version).toBe('1.18.4')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ohne Key-Meldungen ist alles gut — und die Version kommt trotzdem mit', () => {
|
||||||
|
const urteil = urteilAusAusgabe(
|
||||||
|
'MSG:1005,0,1,"MakeMKV v1.18.4 win(x64-release) gestartet","x"\nMSG:5010,0,0,"Das Öffnen der Disk schlug fehl","x"',
|
||||||
|
)
|
||||||
|
expect(urteil.ok).toBe(true)
|
||||||
|
expect(urteil.version).toBe('1.18.4')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('5020 allein bleibt „Schlüssel wird nicht angenommen"', () => {
|
||||||
|
const urteil = urteilAusAusgabe('MSG:5020,516,0,"…","…"')
|
||||||
|
expect(urteil.code).toBe(5020)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Fund 2 — „Komprimieren auf der CPU statt der GPU"', () => {
|
||||||
|
// Die ECHTE Hardware-Preset-Liste des Commander-HandBrake (30.08.2026)
|
||||||
|
// plus die gemessenen Backends der RX 9070 XT.
|
||||||
|
const PRESETS = [
|
||||||
|
'Very Fast 1080p30',
|
||||||
|
'H.265 MKV 1080p30',
|
||||||
|
'AV1 QSV 2160p 4K',
|
||||||
|
'AV1 VCN 2160p 4K',
|
||||||
|
'H.265 NVENC 2160p 4K',
|
||||||
|
'H.265 NVENC 1080p',
|
||||||
|
'H.265 QSV 2160p 4K',
|
||||||
|
'H.265 QSV 1080p',
|
||||||
|
'H.265 VCN 2160p 4K',
|
||||||
|
'H.265 VCN 1080p',
|
||||||
|
]
|
||||||
|
const BACKENDS = ['cpu-x264', 'cpu-x265', 'cpu-av1', 'vce', 'vce-av1']
|
||||||
|
|
||||||
|
it('empfiehlt auf dem Commander-PC VCN, nicht CPU', () => {
|
||||||
|
expect(presetEmpfehlung('bluray', BACKENDS, PRESETS)).toBe('H.265 VCN 1080p')
|
||||||
|
expect(presetEmpfehlung('dvd', BACKENDS, PRESETS)).toBe('H.265 VCN 1080p')
|
||||||
|
expect(presetEmpfehlung('uhd', BACKENDS, PRESETS)).toBe('H.265 VCN 2160p 4K')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('NVIDIA-Maschine bekäme NVENC, Intel QSV, reine CPU den Default', () => {
|
||||||
|
expect(presetEmpfehlung('bluray', ['cpu-x265', 'nvenc'], PRESETS)).toBe('H.265 NVENC 1080p')
|
||||||
|
expect(presetEmpfehlung('bluray', ['cpu-x265', 'qsv'], PRESETS)).toBe('H.265 QSV 1080p')
|
||||||
|
expect(presetEmpfehlung('bluray', ['cpu-x264', 'cpu-x265'], PRESETS)).toBe(DEFAULT_HB_PRESET)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('empfohlen wird NUR, was die gemessene Liste hergibt', () => {
|
||||||
|
// vce da, aber keine VCN-Presets in der Liste → CPU-Default statt
|
||||||
|
// eines geratenen Namens (die „Invalid preset"-Falle).
|
||||||
|
expect(presetEmpfehlung('bluray', ['vce'], ['Very Fast 1080p30'])).toBe(DEFAULT_HB_PRESET)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('die Kette bleibt: eigene Wahl schlägt die Empfehlung', () => {
|
||||||
|
expect(presetFuer('bluray', { transcodePresetBluray: 'Fast 480p30' }, 'H.265 VCN 1080p')).toBe('Fast 480p30')
|
||||||
|
expect(presetFuer('bluray', {}, 'H.265 VCN 1080p')).toBe('H.265 VCN 1080p')
|
||||||
|
expect(presetFuer('bluray', {}, '')).toBe(DEFAULT_HB_PRESET)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user