feat(v5): Titel lesen bis 15 Minuten (einstellbar), Zwischenstand im Dialog, Abbrechen beendet den Lauf
WAS: Die Grenze fürs Titel-Lesen kommt aus der Einstellung titelLesenMinuten (5–60, Vorgabe 15; Einstellungen → Allgemein). Ab der zweiten Minute meldet der Kern jede Minute, wie lange MakeMKV schon liest und wo die Grenze liegt; „Abbrechen" im Dialog beendet den Lauf auch im Kern (titel-abbruch), damit das Laufwerk frei wird. Läuft die Zeit ab, nennt die Meldung Kopierschutz und viele Playlisten als mögliche Gründe statt nur „beschädigte Disc" — mit dem Rat, die Disc im MakeMKV-Programm zu öffnen und die Zeit zu stoppen. WARUM: Durchsicht 12.09.2026, Kandidat 2 (Commander-Entscheid: 15 Minuten plus Regler) — fünf Minuten fest war für ältere DVDs mit Kopierschutz zu kurz, das GUI wartet, Rippy nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ae82d8c0fd
commit
6b6e5c3436
@@ -23,7 +23,7 @@ import type {
|
|||||||
TitelStand,
|
TitelStand,
|
||||||
} from '../../gemeinsam/nachrichten'
|
} from '../../gemeinsam/nachrichten'
|
||||||
import { DialogRahmen, IconDisc, KNOPF_GOLD, datumKurz, dauerText, gb, gbKurz, posterUrlKlein, sprachenVorbelegt } from '../bausteine'
|
import { DialogRahmen, IconDisc, KNOPF_GOLD, datumKurz, dauerText, gb, gbKurz, posterUrlKlein, sprachenVorbelegt } from '../bausteine'
|
||||||
import { metadatenSuchen, ripStarten } from '../kernverbindung'
|
import { metadatenSuchen, ripStarten, titelAbbrechen } from '../kernverbindung'
|
||||||
|
|
||||||
/** Sprach-Chips eines Titels — anklickbar, außerhalb des Auswahl-Labels
|
/** Sprach-Chips eines Titels — anklickbar, außerhalb des Auswahl-Labels
|
||||||
* (ein Knopf IM Label würde die Checkbox mit umschalten). */
|
* (ein Knopf IM Label würde die Checkbox mit umschalten). */
|
||||||
@@ -181,9 +181,12 @@ export function TitelWahlDialog({
|
|||||||
<h3 className="font-display text-xl font-bold text-schnee">Was soll gerippt werden?</h3>
|
<h3 className="font-display text-xl font-bold text-schnee">Was soll gerippt werden?</h3>
|
||||||
|
|
||||||
{stand === undefined || stand.laeuft ? (
|
{stand === undefined || stand.laeuft ? (
|
||||||
<p className="mt-4 font-mono text-xs text-nebel">
|
<div className="mt-4">
|
||||||
Rippy liest die Titel der Disc … das dauert bis zu zwei Minuten. Solange dreht das Laufwerk.
|
<p className="font-mono text-xs text-nebel">Rippy liest die Titel der Disc … meist dauert das ein bis zwei Minuten. Solange dreht das Laufwerk.</p>
|
||||||
</p>
|
{/* 5.7.0 (Kandidat 2): Ab der zweiten Minute sagt der Kern, wie lange
|
||||||
|
es schon läuft und wo die Grenze liegt. */}
|
||||||
|
{stand !== undefined && stand.einordnung.length > 0 && <p className="mt-2 font-mono text-xs text-warn">{stand.einordnung}</p>}
|
||||||
|
</div>
|
||||||
) : stand.fehler.length > 0 ? (
|
) : stand.fehler.length > 0 ? (
|
||||||
<p className="mt-4 font-mono text-xs text-rot">✗ {stand.fehler}</p>
|
<p className="mt-4 font-mono text-xs text-rot">✗ {stand.fehler}</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -463,7 +466,15 @@ export function TitelWahlDialog({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-5 flex justify-between gap-2">
|
<div className="mt-5 flex justify-between gap-2">
|
||||||
<button onClick={schliessen} className="rounded-lg border border-tinte/15 px-4 py-1.5 text-xs text-dunst hover:text-schnee">
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Läuft der Titel-Lauf noch, beendet „Abbrechen" ihn auch im Kern —
|
||||||
|
// sonst dreht das Laufwerk weiter, obwohl der Dialog zu ist.
|
||||||
|
if (stand !== undefined && stand.laeuft) titelAbbrechen(id)
|
||||||
|
schliessen()
|
||||||
|
}}
|
||||||
|
className="rounded-lg border border-tinte/15 px-4 py-1.5 text-xs text-dunst hover:text-schnee"
|
||||||
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -577,6 +577,17 @@ export function AllgemeinKarte({ verbindung, haupt }: { verbindung: Verbindung;
|
|||||||
return (
|
return (
|
||||||
<SektionsKarte id="allgemein" titel="ALLGEMEIN" intro="Rippy lebt im Infobereich neben der Uhr: Fenster zu heißt weiterlaufen, beendet wird über das Tray-Menü.">
|
<SektionsKarte id="allgemein" titel="ALLGEMEIN" intro="Rippy lebt im Infobereich neben der Uhr: Fenster zu heißt weiterlaufen, beendet wird über das Tray-Menü.">
|
||||||
<Schalter an={haupt?.autostart === true} aufWechsel={(an) => void window.rippy.autostartSetzen(an)} label="Rippy mit Windows starten (Autostart im Benutzerkonto — kein Dienst, keine Adminrechte)" />
|
<Schalter an={haupt?.autostart === true} aufWechsel={(an) => void window.rippy.autostartSetzen(an)} label="Rippy mit Windows starten (Autostart im Benutzerkonto — kein Dienst, keine Adminrechte)" />
|
||||||
|
<label className="block max-w-sm">
|
||||||
|
<span className="text-xs font-semibold text-dunst">Titel lesen — höchstens</span>
|
||||||
|
<select value={werte['titelLesenMinuten'] ?? '15'} onChange={(e) => setzen('titelLesenMinuten', e.target.value)} className={FELD_KLASSE}>
|
||||||
|
<option value="5">5 Minuten</option>
|
||||||
|
<option value="10">10 Minuten</option>
|
||||||
|
<option value="15">15 Minuten (Vorgabe)</option>
|
||||||
|
<option value="30">30 Minuten</option>
|
||||||
|
<option value="60">60 Minuten</option>
|
||||||
|
</select>
|
||||||
|
<span className="mt-1 block text-[11px] leading-4 text-nebel">So lange darf MakeMKV die Titel einer Disc lesen. Kopiergeschützte DVDs und Discs mit vielen Playlisten brauchen im MakeMKV-Programm oft länger als fünf Minuten — braucht eine Disc dort länger als hier eingestellt, erhöhe den Wert.</span>
|
||||||
|
</label>
|
||||||
<label className="block max-w-sm">
|
<label className="block max-w-sm">
|
||||||
<span className="text-xs font-semibold text-dunst">Erscheinungsbild</span>
|
<span className="text-xs font-semibold text-dunst">Erscheinungsbild</span>
|
||||||
<select value={werte['design'] ?? 'dunkel'} onChange={(e) => setzen('design', e.target.value)} className={FELD_KLASSE}>
|
<select value={werte['design'] ?? 'dunkel'} onChange={(e) => setzen('design', e.target.value)} className={FELD_KLASSE}>
|
||||||
|
|||||||
@@ -347,6 +347,11 @@ export function titelLesen(id: string): void {
|
|||||||
anKern({ art: 'titel-lesen', id }, `Titel lesen ${id}`)
|
anKern({ art: 'titel-lesen', id }, `Titel lesen ${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 5.7.0: den laufenden Titel-Lauf beenden — makemkvcon gibt das Laufwerk frei. */
|
||||||
|
export function titelAbbrechen(id: string): void {
|
||||||
|
anKern({ art: 'titel-abbruch', id }, `Titel-Lauf abbrechen ${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
/** `auftrag` leer heißt: alles rippen (wie vor 5.2). */
|
/** `auftrag` leer heißt: alles rippen (wie vor 5.2). */
|
||||||
export function ripStarten(id: string, auftrag: RipAuftragTitel[] = [], optionen: RipAuftragOptionen = {}): void {
|
export function ripStarten(id: string, auftrag: RipAuftragTitel[] = [], optionen: RipAuftragOptionen = {}): void {
|
||||||
anKern({ art: 'rip-start', id, auftrag, optionen }, `Rip ${id}`)
|
anKern({ art: 'rip-start', id, auftrag, optionen }, `Rip ${id}`)
|
||||||
|
|||||||
@@ -438,6 +438,8 @@ export type FensterNachricht =
|
|||||||
* und nicht schon beim Einlegen (der 30.08.-Fund „das Erkennen dauert
|
* und nicht schon beim Einlegen (der 30.08.-Fund „das Erkennen dauert
|
||||||
* sehr sehr lange"). */
|
* sehr sehr lange"). */
|
||||||
| { art: 'titel-lesen'; id: string }
|
| { art: 'titel-lesen'; id: string }
|
||||||
|
/** 5.7.0: den laufenden Titel-Lauf abbrechen (Kandidat 2). */
|
||||||
|
| { art: 'titel-abbruch'; id: string }
|
||||||
/** `auftrag` leer heißt: alles rippen — wie vor 5.2. */
|
/** `auftrag` leer heißt: alles rippen — wie vor 5.2. */
|
||||||
| { art: 'rip-start'; id: string; auftrag?: RipAuftragTitel[]; optionen?: RipAuftragOptionen }
|
| { art: 'rip-start'; id: string; auftrag?: RipAuftragTitel[]; optionen?: RipAuftragOptionen }
|
||||||
| { art: 'rip-abbruch'; id: string }
|
| { art: 'rip-abbruch'; id: string }
|
||||||
@@ -705,6 +707,7 @@ export function istFensterNachricht(wert: unknown): wert is FensterNachricht {
|
|||||||
if (
|
if (
|
||||||
wert.art === 'auswerfen' ||
|
wert.art === 'auswerfen' ||
|
||||||
wert.art === 'titel-lesen' ||
|
wert.art === 'titel-lesen' ||
|
||||||
|
wert.art === 'titel-abbruch' ||
|
||||||
wert.art === 'rip-abbruch' ||
|
wert.art === 'rip-abbruch' ||
|
||||||
wert.art === 'rip-ueberspringen' ||
|
wert.art === 'rip-ueberspringen' ||
|
||||||
wert.art === 'laufwerk-pruefen' ||
|
wert.art === 'laufwerk-pruefen' ||
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export const ERLAUBTE_EINSTELLUNGEN: ReadonlySet<string> = new Set([
|
|||||||
'telegramToken',
|
'telegramToken',
|
||||||
'telegramChatId',
|
'telegramChatId',
|
||||||
'design',
|
'design',
|
||||||
|
// 5.7.0: wie lange makemkvcon die Titel lesen darf (Kandidat 2 der Durchsicht)
|
||||||
|
'titelLesenMinuten',
|
||||||
])
|
])
|
||||||
|
|
||||||
/** Die Schlüssel, die Rip und Kompression lesen (Pipeline/Ablage). Das
|
/** Die Schlüssel, die Rip und Kompression lesen (Pipeline/Ablage). Das
|
||||||
@@ -56,6 +58,10 @@ const PIPELINE_EINSTELLUNGEN = [
|
|||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const AUTOMATIK_SEKUNDEN_VORGABE = 30
|
export const AUTOMATIK_SEKUNDEN_VORGABE = 30
|
||||||
|
/** 5.7.0: Vorgabe 15 min (Commander-Entscheid 12.09.2026) — kopiergeschützte
|
||||||
|
* DVDs und Discs mit vielen Playlisten brauchen im MakeMKV-GUI regelmäßig
|
||||||
|
* länger als die alten fünf Minuten. */
|
||||||
|
export const TITEL_LESEN_MINUTEN_VORGABE = 15
|
||||||
|
|
||||||
export class EinstellungsDienst {
|
export class EinstellungsDienst {
|
||||||
constructor(private readonly kontext: KernKontext) {}
|
constructor(private readonly kontext: KernKontext) {}
|
||||||
@@ -101,6 +107,12 @@ export class EinstellungsDienst {
|
|||||||
return this.lesen('automatik', 'an') !== 'aus'
|
return this.lesen('automatik', 'an') !== 'aus'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Grenze fürs Titel-Lesen in Minuten (5–60, Vorgabe 15). */
|
||||||
|
titelLesenMinuten(): number {
|
||||||
|
const n = Number(this.lesen('titelLesenMinuten', String(TITEL_LESEN_MINUTEN_VORGABE)))
|
||||||
|
return Number.isInteger(n) && n >= 5 && n <= 60 ? n : TITEL_LESEN_MINUTEN_VORGABE
|
||||||
|
}
|
||||||
|
|
||||||
automatikSekunden(): number {
|
automatikSekunden(): number {
|
||||||
const n = Number(this.lesen('automatikSekunden', String(AUTOMATIK_SEKUNDEN_VORGABE)))
|
const n = Number(this.lesen('automatikSekunden', String(AUTOMATIK_SEKUNDEN_VORGABE)))
|
||||||
return Number.isInteger(n) && n >= 5 && n <= 600 ? n : AUTOMATIK_SEKUNDEN_VORGABE
|
return Number.isInteger(n) && n >= 5 && n <= 600 ? n : AUTOMATIK_SEKUNDEN_VORGABE
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface ErkennungAbhaengigkeiten {
|
|||||||
ripLaeuft(id: string): boolean
|
ripLaeuft(id: string): boolean
|
||||||
geraetInfoVon(id: string): GeraeteInfo | null
|
geraetInfoVon(id: string): GeraeteInfo | null
|
||||||
makemkvcon(): string
|
makemkvcon(): string
|
||||||
|
/** 5.7.0: wie lange makemkvcon die Titel lesen darf (Minuten). Fehlt: 15. */
|
||||||
|
titelLesenMinuten?(): number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Der Serien-Merker (5.5.0): je TMDb-Serie, was die vorige Disc hinterließ. */
|
/** Der Serien-Merker (5.5.0): je TMDb-Serie, was die vorige Disc hinterließ. */
|
||||||
@@ -311,8 +313,22 @@ export class ErkennungDienst {
|
|||||||
|
|
||||||
senden({ ...leer, laeuft: true })
|
senden({ ...leer, laeuft: true })
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
|
// 5.7.0 (Kandidat 2): Die Grenze kommt aus den Einstellungen (Vorgabe
|
||||||
|
// 15 min statt fest fünf), und ab der zweiten Minute sagt der Dialog,
|
||||||
|
// dass es länger dauert — mit Abbrechen-Knopf, statt stumm zu drehen.
|
||||||
|
const minuten = this.abh.titelLesenMinuten?.() ?? 15
|
||||||
|
const zwischenstand = setInterval(() => {
|
||||||
|
if (!nochAktuell()) return
|
||||||
|
const vergangen = Math.round((Date.now() - start) / 60_000)
|
||||||
|
senden({
|
||||||
|
...leer,
|
||||||
|
laeuft: true,
|
||||||
|
einordnung: `MakeMKV liest seit ${vergangen} min. Kopiergeschützte DVDs und Discs mit vielen Playlisten brauchen länger — Rippy wartet bis ${minuten} min (Einstellungen → Allgemein), „Abbrechen" beendet den Lauf.`,
|
||||||
|
})
|
||||||
|
}, 60_000)
|
||||||
|
zwischenstand.unref()
|
||||||
try {
|
try {
|
||||||
const info = await titelInfoLesen(programm, geraet.pfad, 300_000, abbruch.signal)
|
const info = await titelInfoLesen(programm, geraet.pfad, minuten * 60_000, abbruch.signal)
|
||||||
if (!nochAktuell()) return { ...leer, fehler: ABGELOEST }
|
if (!nochAktuell()) return { ...leer, fehler: ABGELOEST }
|
||||||
const merkmale = this.merkmale.get(id)
|
const merkmale = this.merkmale.get(id)
|
||||||
const wahl = titelVorauswahl(info.titel, {
|
const wahl = titelVorauswahl(info.titel, {
|
||||||
@@ -352,11 +368,12 @@ export class ErkennungDienst {
|
|||||||
// R4: sonst laut scheitern. „Konnte nicht nachsehen" ist etwas anderes
|
// R4: sonst laut scheitern. „Konnte nicht nachsehen" ist etwas anderes
|
||||||
// als „die Disc hat keine Titel" — beides zu vermischen erzeugt Unsinn.
|
// als „die Disc hat keine Titel" — beides zu vermischen erzeugt Unsinn.
|
||||||
const grund = String(fehler).includes('keine Antwort nach')
|
const grund = String(fehler).includes('keine Antwort nach')
|
||||||
? 'MakeMKV hat die Titel nach fünf Minuten nicht gelesen. Bei einer beschädigten Disc kann das passieren — Rippy hat den Versuch beendet und das Laufwerk freigegeben.'
|
? `MakeMKV hat die Titel nach ${minuten} Minuten nicht gelesen — Rippy hat den Versuch beendet und das Laufwerk freigegeben. Das kann an einer beschädigten Disc liegen, aber auch an Kopierschutz oder vielen Playlisten, die einfach länger brauchen: Die Grenze steht unter Einstellungen → Allgemein. Zur Probe die Disc im MakeMKV-Programm öffnen und die Zeit stoppen.`
|
||||||
: `Titel lesen: ${String(fehler).slice(0, 200)}`
|
: `Titel lesen: ${String(fehler).slice(0, 200)}`
|
||||||
this.kontext.protokoll.schreiben(`Titel-Lauf ${id} gescheitert: ${grund}`)
|
this.kontext.protokoll.schreiben(`Titel-Lauf ${id} gescheitert: ${grund}`)
|
||||||
return senden({ ...leer, fehler: grund })
|
return senden({ ...leer, fehler: grund })
|
||||||
} finally {
|
} finally {
|
||||||
|
clearInterval(zwischenstand)
|
||||||
// Nur aufräumen, wenn ICH noch der aktuelle Lauf bin.
|
// Nur aufräumen, wenn ICH noch der aktuelle Lauf bin.
|
||||||
if (nochAktuell()) {
|
if (nochAktuell()) {
|
||||||
this.titelAbbrueche.delete(id)
|
this.titelAbbrueche.delete(id)
|
||||||
@@ -365,6 +382,18 @@ export class ErkennungDienst {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Der Nutzer bricht das Titel-Lesen ab (5.7.0): Der Lauf endet sofort,
|
||||||
|
* makemkvcon gibt das Laufwerk frei, der Dialog zeigt „abgebrochen". */
|
||||||
|
titelAbbrechen(id: string): void {
|
||||||
|
const abbruch = this.titelAbbrueche.get(id)
|
||||||
|
if (abbruch === undefined) return
|
||||||
|
abbruch.abort()
|
||||||
|
this.titelAbbrueche.delete(id)
|
||||||
|
this.titelLaufNummern.delete(id)
|
||||||
|
this.kontext.protokoll.schreiben(`Titel-Lauf ${id} vom Nutzer abgebrochen`)
|
||||||
|
this.kontext.anFenster({ art: 'disc-titel', stand: { id, laeuft: false, titel: [], einordnung: '', unsicher: false, fehler: 'Abgebrochen — „Rippen" liest die Titel neu.' } })
|
||||||
|
}
|
||||||
|
|
||||||
/** Erkennung v2 (5.4.0): Erkannt wird beim EINLEGEN, vor jeder Messung.
|
/** Erkennung v2 (5.4.0): Erkannt wird beim EINLEGEN, vor jeder Messung.
|
||||||
* Nach dem Info-Lauf wird noch einmal gerechnet — Laufzeit gegen TMDb,
|
* Nach dem Info-Lauf wird noch einmal gerechnet — Laufzeit gegen TMDb,
|
||||||
* Titel-Struktur gegen den Treffer-Typ. Aus „wahrscheinlich" wird
|
* Titel-Struktur gegen den Treffer-Typ. Aus „wahrscheinlich" wird
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ const erkennung = new ErkennungDienst(kontext, {
|
|||||||
ripLaeuft: (id) => ripVerwaltung.laeuft(id),
|
ripLaeuft: (id) => ripVerwaltung.laeuft(id),
|
||||||
geraetInfoVon: (id) => laufwerke.geraetInfoVon(id),
|
geraetInfoVon: (id) => laufwerke.geraetInfoVon(id),
|
||||||
makemkvcon: () => werkzeuge.makemkvcon(),
|
makemkvcon: () => werkzeuge.makemkvcon(),
|
||||||
|
titelLesenMinuten: () => einstellungen.titelLesenMinuten(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const vorgaenge = new VorgangDienst(kontext, einstellungen, werkzeuge, erkennung, benachrichtigung, () => speicher.senden())
|
const vorgaenge = new VorgangDienst(kontext, einstellungen, werkzeuge, erkennung, benachrichtigung, () => speicher.senden())
|
||||||
@@ -239,6 +240,9 @@ function fensterNachricht(frage: unknown): void {
|
|||||||
automatik.stopp(frage.id, '')
|
automatik.stopp(frage.id, '')
|
||||||
void erkennung.titelLesen(frage.id)
|
void erkennung.titelLesen(frage.id)
|
||||||
return
|
return
|
||||||
|
case 'titel-abbruch':
|
||||||
|
erkennung.titelAbbrechen(frage.id)
|
||||||
|
return
|
||||||
case 'rip-start':
|
case 'rip-start':
|
||||||
automatik.stopp(frage.id, '')
|
automatik.stopp(frage.id, '')
|
||||||
ripMitPlatz(frage.id, frage.auftrag ?? [], frage.optionen ?? {})
|
ripMitPlatz(frage.id, frage.auftrag ?? [], frage.optionen ?? {})
|
||||||
|
|||||||
Reference in New Issue
Block a user