feat(v5): Kern 5.4.0 — Vorgaenge, Kompressions-Warteschlange, Roh-Aufraeumen, Sprachen je Titel, Erkennung v2
WAS: Der Rip endet, sobald die Roh-Dateien liegen; die Kompression laeuft danach in einer Warteschlange (eine zur Zeit), das Laufwerk ist frei fuer die naechste Disc. Jeder Rip ist jetzt ein Datensatz (Tabelle vorgaenge, Schema 4): Roh-Ordner, Zielordner, Phase, Auftrag als JSON. Darauf bauen: Roh-Aufraeumen nach Regel (Vorgabe 7 Tage, nur bei gelungener Kompression und nur wenn jede fertige Datei da und nicht leer ist), Wiederholen ab Kompression, Aufraeumen halber Dateien nach Abbruch/Fehler, Wiederaufnahme nach Neustart. Unkomprimierte Rips (4K, kein HandBrake) werden in den Zielordner VERSCHOBEN statt unter roh\ liegen zu bleiben. Sprachen je Titel reisen vom Info-Lauf (SINFO) bis in HandBrake; fremder Ton schaltet den deutschen Untertitel vor (--subtitle-default=1). Vor der Kompression prueft ein HandBrake-Scan die Roh-Datei. MakeMKVs Auswahlregel wird gesetzt, wenn keine steht, damit alle Sprachen in die Roh-Datei kommen. Staffel und erste Folge von Hand schlagen die Laufzeit-Raterei. Durchgehender Fortschritt, nach Groesse gewichtet. Erkennung v2: leere TMDb-Eintraege (Stubs) zaehlen nicht, Laufzeit wird nach dem Info-Lauf gegen TMDb gehalten, Titel-Struktur als Fingerabdruck, bdmt_deu als zweite Titel-Variante. TMDb liefert Logo, Tagline, Bewertung, FSK und Besetzung in derselben Anfrage. Laufwerks-Gesundheit aus dem Windows-Ereignisprotokoll (wevtutil): Disc oder Hardware. WARUM: Uebergabe-Punkte 1-5, 8-12 des SAVEPOINT vom 01.09.2026 — vom Commander gutgeheissen (7 Tage / Untertitel vorgewaehlt / entkoppelte Kompression waren seine Wahl). GEMESSEN (Regel D): HandBrake 1.11 Sprach-Schalter an einer ffmpeg- Testdatei (jpn/deu/deu/eng + deu/eng), HandBrake --scan --json Format, TMDb movie/149 + tv/46296 mit append_to_response, wevtutil /f:xml an diesem PC (338x cdrom 7, 8x cdrom 11 in 30 Tagen), makemkvcon --minlength, MakeMKV-Auswahlregel laut forum.makemkv.com t=4386. 348 Tests gruen (vorher 261), Typpruefung und Bau gruen. Oberflaeche folgt im naechsten Schritt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d70a81d194
commit
3f7f962dff
+538
-101
@@ -3,19 +3,42 @@
|
||||
// Datenbank (node:sqlite, § 3.4), Disc-Wache im 3-Sekunden-Takt (§ 5),
|
||||
// Auswerfen mit Nachsehen (R3), Ping/Pong über den direkten MessagePort.
|
||||
//
|
||||
// Seit 5.4.0 dazu: Vorgänge als Datensätze (welcher Roh-Ordner gehört zu
|
||||
// welchem Film), die Kompressions-Warteschlange (das Laufwerk ist frei,
|
||||
// sobald die Roh-Dateien liegen), das Roh-Aufräumen im Takt, die
|
||||
// Wiederaufnahme nach einem Neustart, die Laufwerks-Prüfung aus dem
|
||||
// Windows-Protokoll und die Nachprüfung der Erkennung nach dem Info-Lauf.
|
||||
//
|
||||
// parentPort/MessagePortMain: Schnittstelle laut Electron-Doku »utilityProcess«
|
||||
// und »MessagePorts in Electron« — dieselbe Verdrahtung wie im gemessenen
|
||||
// Beweis beweise/sqlite-main.js.
|
||||
import type { MessagePortMain } from 'electron'
|
||||
import { existsSync, rmSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
istFensterNachricht,
|
||||
istHauptNachricht,
|
||||
type DbStatus,
|
||||
type DiscInfoStand,
|
||||
type KernNachricht,
|
||||
type MetaVorschlag,
|
||||
type VorgaengeStand,
|
||||
type VorgangEintrag,
|
||||
} from '../gemeinsam/nachrichten'
|
||||
import { komprimierenUndAblegen, type AblageUmgebung } from './ablauf/ablegen'
|
||||
import { auftragAusJson, auftragZuJson } from './ablauf/auftrag'
|
||||
import { Pipeline } from './ablauf/pipeline'
|
||||
import { KompressionsWarteschlange } from './ablauf/warteschlange'
|
||||
import {
|
||||
AUFBEWAHRUNG_SCHLUESSEL,
|
||||
aufraeumen,
|
||||
faelligAm,
|
||||
fremdeOrdner,
|
||||
istUnterRohWurzel,
|
||||
ordnerGroesse,
|
||||
regelLesen,
|
||||
regelText,
|
||||
} from './ablage/rohaufraeumen'
|
||||
import type { LaufwerkApi } from './laufwerk/codes'
|
||||
import {
|
||||
audioSpurLesen,
|
||||
@@ -25,15 +48,17 @@ import {
|
||||
kennung,
|
||||
listOptischeLaufwerke,
|
||||
} from './laufwerk/disc'
|
||||
import { beurteilen, ereignisseLesen } from './laufwerk/ereignisse'
|
||||
import { DiscWache } from './laufwerk/wache'
|
||||
import { discFingerprint, discTitel, type DiscTitel } from './metadaten/discmerkmale'
|
||||
import { OmdbClient } from './metadaten/omdb'
|
||||
import { TmdbClient } from './metadaten/tmdb'
|
||||
import { zuordnen, type Zuordnung } from './metadaten/zuordnung'
|
||||
import { LEERER_TREFFER, ausDetails, nachpruefen, zuordnen, type Zuordnung } from './metadaten/zuordnung'
|
||||
import { presetEmpfehlung } from '../gemeinsam/preset-empfehlung'
|
||||
import { Datenbank } from './speicher/db'
|
||||
import { Datenbank, type Vorgang } from './speicher/db'
|
||||
import { titelInfoLesen, ABGELOEST } from './rip/makemkv'
|
||||
import { titelVorauswahl } from './rip/titelwahl'
|
||||
import { sprachenFuerTitel } from './rip/parser'
|
||||
import { strukturHinweis, titelVorauswahl } from './rip/titelwahl'
|
||||
import { finden } from './werkzeuge/katalog'
|
||||
|
||||
// --datenbank=<pfad> kommt vom Haupt (der Kern kennt Electrons app.getPath
|
||||
@@ -72,6 +97,8 @@ if (pfad === null) {
|
||||
}
|
||||
}
|
||||
|
||||
const smokeKern = process.argv.includes('--smoke-kern')
|
||||
|
||||
let fensterPort: MessagePortMain | null = null
|
||||
|
||||
function anFenster(nachricht: KernNachricht): void {
|
||||
@@ -83,6 +110,11 @@ function anHauptUndFenster(nachricht: KernNachricht): void {
|
||||
anFenster(nachricht)
|
||||
}
|
||||
|
||||
function hinweis(text: string): void {
|
||||
console.log(`[kern] ${text}`)
|
||||
anFenster({ art: 'kern-hinweis', text })
|
||||
}
|
||||
|
||||
// ── Laufwerke (W-1) ─────────────────────────────────────────────────────
|
||||
// koffi wird erst HIER geladen, über einen echten dynamischen import():
|
||||
// Rollup legt win32.ts damit als eigenen Chunk neben kern.js (ein
|
||||
@@ -100,6 +132,39 @@ const discFingerprints = new Map<string, string>()
|
||||
// das bleibt wahr, auch wenn der Commander den Titel selbst neu wählt.
|
||||
const discMerkmale = new Map<string, DiscTitel>()
|
||||
|
||||
/** EINE Stelle, die aus Zuordnung + Disc-Merkmalen den Stand fürs Fenster
|
||||
* baut (5.4.0) — vorher stand dieselbe Struktur dreimal im Code, und jedes
|
||||
* neue Feld hätte dreimal nachgezogen werden müssen. */
|
||||
function discInfoStand(id: string, zuordnung: Zuordnung | null, quelle?: string): DiscInfoStand {
|
||||
const m = discMerkmale.get(id)
|
||||
const meta = zuordnung?.meta
|
||||
return {
|
||||
id,
|
||||
titel: zuordnung?.titel ?? '',
|
||||
jahr: zuordnung?.jahr ?? null,
|
||||
confidence: zuordnung?.confidence ?? 0,
|
||||
quelle: quelle ?? meta?.quelle ?? '',
|
||||
posterPfad: meta?.posterPfad ?? '',
|
||||
backdropPfad: meta?.backdropPfad ?? '',
|
||||
beschreibung: meta?.beschreibung ?? '',
|
||||
// § 6.6: Rippy erkennt beim Einlegen, wenn eine Disc schon einmal
|
||||
// durchgelaufen ist — und fragt nach, statt stumm zu doppeln.
|
||||
schonGerippt: zuordnung === null ? null : (db?.bibliothekSchonGerippt(discFingerprints.get(id) ?? '') ?? null),
|
||||
discNr: m?.discNr ?? null,
|
||||
staffel: m?.staffel ?? null,
|
||||
folgen: m?.folgen ?? 0,
|
||||
typ: meta?.typ === 'movie' || meta?.typ === 'tv' ? meta.typ : '',
|
||||
logoPfad: meta?.logoPfad ?? '',
|
||||
tagline: meta?.tagline ?? '',
|
||||
bewertung: meta?.bewertung ?? 0,
|
||||
fsk: meta?.fsk ?? '',
|
||||
laufzeitMinuten: meta?.laufzeitMinuten ?? 0,
|
||||
genres: meta?.genres ?? [],
|
||||
besetzung: meta?.besetzung ?? [],
|
||||
hinweise: zuordnung?.hinweise ?? [],
|
||||
}
|
||||
}
|
||||
|
||||
async function discErmitteln(id: string, geraetepfad: string, groesseBytes: number): Promise<void> {
|
||||
try {
|
||||
const gefunden = discTitel(geraetepfad)
|
||||
@@ -109,28 +174,14 @@ async function discErmitteln(id: string, geraetepfad: string, groesseBytes: numb
|
||||
const tmdb = new TmdbClient(db?.einstellungLesen('tmdbApiKey') ?? '')
|
||||
const omdb = new OmdbClient(db?.einstellungLesen('omdbApiKey') ?? '')
|
||||
// Der Hinweis kommt von der Disc selbst (bdmt-Inhaltsverzeichnis) —
|
||||
// trägt sie Folgen, kann kein Filmtreffer mehr gewinnen.
|
||||
const zuordnung = await zuordnen(gefunden.titel, tmdb, omdb, { istSerie: gefunden.istSerie })
|
||||
discZuordnungen.set(id, zuordnung)
|
||||
anHauptUndFenster({
|
||||
art: 'disc-info',
|
||||
info: {
|
||||
id,
|
||||
titel: zuordnung.titel,
|
||||
jahr: zuordnung.jahr,
|
||||
confidence: zuordnung.confidence,
|
||||
quelle: zuordnung.meta.quelle,
|
||||
posterPfad: zuordnung.meta.posterPfad,
|
||||
backdropPfad: zuordnung.meta.backdropPfad,
|
||||
beschreibung: zuordnung.meta.beschreibung,
|
||||
// § 6.6: Rippy erkennt beim Einlegen, wenn eine Disc schon einmal
|
||||
// durchgelaufen ist — und fragt nach, statt stumm zu doppeln.
|
||||
schonGerippt: db?.bibliothekSchonGerippt(fingerprint) ?? null,
|
||||
discNr: gefunden.discNr,
|
||||
staffel: gefunden.staffel,
|
||||
folgen: gefunden.folgen,
|
||||
},
|
||||
// trägt sie Folgen, kann kein Filmtreffer mehr gewinnen. Und jede
|
||||
// Titel-Variante der Disc (bdmt_deu) wird mitgefragt (5.4.0).
|
||||
const zuordnung = await zuordnen(gefunden.titel, tmdb, omdb, {
|
||||
istSerie: gefunden.istSerie,
|
||||
alternativen: gefunden.alternativen,
|
||||
})
|
||||
discZuordnungen.set(id, zuordnung)
|
||||
anHauptUndFenster({ art: 'disc-info', info: discInfoStand(id, zuordnung) })
|
||||
} catch (fehler) {
|
||||
// R4: Eine gescheiterte Erkennung ist eine laute Zeile — gerippt
|
||||
// werden kann trotzdem (die Zuordnung ist Komfort, kein Torwächter).
|
||||
@@ -183,43 +234,27 @@ async function zuordnungSetzen(id: string, vorschlag: MetaVorschlag): Promise<vo
|
||||
const tmdb = new TmdbClient(db?.einstellungLesen('tmdbApiKey') ?? '')
|
||||
const details =
|
||||
vorschlag.typ === 'movie' ? await tmdb.movieDetails(vorschlag.tmdbId) : await tmdb.tvDetails(vorschlag.tmdbId)
|
||||
const meta =
|
||||
details !== null
|
||||
? ausDetails(vorschlag.typ, vorschlag.tmdbId, details)
|
||||
: {
|
||||
...LEERER_TREFFER,
|
||||
typ: vorschlag.typ,
|
||||
id: vorschlag.tmdbId,
|
||||
titel: vorschlag.titel,
|
||||
jahr: vorschlag.jahr,
|
||||
posterPfad: vorschlag.posterPfad,
|
||||
quelle: 'tmdb' as const,
|
||||
}
|
||||
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,
|
||||
titel: meta.titel.length > 0 ? meta.titel : vorschlag.titel,
|
||||
jahr: meta.jahr ?? 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,
|
||||
backdropPfad: details?.backdrop_path ?? '',
|
||||
laufzeitMinuten: details?.runtime ?? 0,
|
||||
genres: (details?.genres ?? []).map((g) => g.name),
|
||||
quelle: 'tmdb',
|
||||
},
|
||||
meta,
|
||||
}
|
||||
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,
|
||||
backdropPfad: zuordnung.meta.backdropPfad,
|
||||
beschreibung: zuordnung.meta.beschreibung,
|
||||
schonGerippt: db?.bibliothekSchonGerippt(discFingerprints.get(id) ?? '') ?? null,
|
||||
// Eigene Titelwahl ändert nichts daran, welche Disc der Ausgabe hier liegt.
|
||||
discNr: discMerkmale.get(id)?.discNr ?? null,
|
||||
staffel: discMerkmale.get(id)?.staffel ?? null,
|
||||
folgen: discMerkmale.get(id)?.folgen ?? 0,
|
||||
},
|
||||
})
|
||||
anHauptUndFenster({ art: 'disc-info', info: discInfoStand(id, zuordnung, 'deine Wahl') })
|
||||
} catch (fehler) {
|
||||
anFenster({ art: 'kern-fehler', text: `Zuordnung übernehmen: ${String(fehler)}` })
|
||||
}
|
||||
@@ -243,23 +278,7 @@ const laufwerkeBereit: Promise<void> = (async () => {
|
||||
discZuordnungen.delete(ereignis.geraet.id)
|
||||
discFingerprints.delete(ereignis.geraet.id)
|
||||
discMerkmale.delete(ereignis.geraet.id)
|
||||
anHauptUndFenster({
|
||||
art: 'disc-info',
|
||||
info: {
|
||||
id: ereignis.geraet.id,
|
||||
titel: '',
|
||||
jahr: null,
|
||||
confidence: 0,
|
||||
quelle: '',
|
||||
posterPfad: '',
|
||||
backdropPfad: '',
|
||||
beschreibung: '',
|
||||
schonGerippt: null,
|
||||
discNr: null,
|
||||
staffel: null,
|
||||
folgen: 0,
|
||||
},
|
||||
})
|
||||
anHauptUndFenster({ art: 'disc-info', info: discInfoStand(ereignis.geraet.id, null) })
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -277,9 +296,6 @@ const laufwerkeBereit: Promise<void> = (async () => {
|
||||
}
|
||||
})()
|
||||
|
||||
// ── Rippen (W-2) ────────────────────────────────────────────────────────
|
||||
// Die Ablage kommt aus der EINEN Einstellungsquelle (§ 6.7); solange dort
|
||||
// nichts steht, gilt Videos\Rippy im Benutzerprofil.
|
||||
// ── Titel der Disc lesen (§ 6.2) ────────────────────────────────────────
|
||||
// Der Info-Lauf dauert 20–120 s — deshalb geschieht er erst auf Zuruf und
|
||||
// nicht schon beim Einlegen (der 30.08.-Fund: „das Erkennen dauert sehr
|
||||
@@ -343,12 +359,16 @@ async function titelLesen(id: string): Promise<void> {
|
||||
stand: {
|
||||
id,
|
||||
laeuft: false,
|
||||
titel: wahl.titel,
|
||||
// Die Sprachen JE TITEL reisen mit (5.4.0) — der Dialog bietet sie
|
||||
// zum Anhaken an.
|
||||
titel: wahl.titel.map((t) => ({ ...t, sprachen: sprachenFuerTitel(info.streams.get(t.nr)) })),
|
||||
einordnung: wahl.einordnung,
|
||||
unsicher: wahl.unsicher,
|
||||
fehler: ohneErgebnis ? info.ursachen.join(' · ') : '',
|
||||
},
|
||||
})
|
||||
// Erkennung v2 (5.4.0): Jetzt liegt die Laufzeit vor — nachprüfen.
|
||||
if (info.titel.length > 0) await erkennungNachpruefen(id, wahl, info.titel)
|
||||
} catch (fehler) {
|
||||
// Ein ABGELÖSTER Lauf schweigt: Ihn hat ein neuerer ersetzt, das ist
|
||||
// Absicht und kein Fehlschlag. Genau hier lag der Commander-Fund —
|
||||
@@ -369,14 +389,59 @@ async function titelLesen(id: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Erkennung v2 (5.4.0): Erkannt wird beim EINLEGEN, vor jeder Messung.
|
||||
* Nach dem Info-Lauf wird noch einmal gerechnet — Laufzeit gegen TMDb,
|
||||
* Titel-Struktur gegen den Treffer-Typ. Aus „wahrscheinlich" wird
|
||||
* „bestätigt", oder Rippy merkt selbst, dass es danebenlag. */
|
||||
async function erkennungNachpruefen(
|
||||
id: string,
|
||||
wahl: ReturnType<typeof titelVorauswahl>,
|
||||
titel: Parameters<typeof strukturHinweis>[0],
|
||||
): Promise<void> {
|
||||
const bisher = discZuordnungen.get(id)
|
||||
if (bisher === undefined) return
|
||||
const gewaehlt = wahl.titel.filter((t) => t.gewaehlt)
|
||||
const folgen = gewaehlt.filter((t) => t.rolle === 'folge').map((t) => t.dauerS)
|
||||
const laengster = [...titel].sort((a, b) => b.dauerS - a.dauerS)[0]?.dauerS ?? 0
|
||||
const messung = {
|
||||
hauptS: folgen.length > 0 ? 0 : (gewaehlt[0]?.dauerS ?? laengster),
|
||||
folgenS: folgen,
|
||||
struktur: strukturHinweis(titel),
|
||||
}
|
||||
let zuordnung = bisher
|
||||
// Sagt die Struktur „Serie", der Treffer aber „Film" (und der Nutzer hat
|
||||
// nicht selbst gewählt): einmal als Serie fragen — das ist der
|
||||
// Fingerabdruck, der Serien auch ohne bdmt erkennt.
|
||||
if (messung.struktur === 'serie' && bisher.meta.typ !== 'tv' && bisher.confidence < 1) {
|
||||
const merkmale = discMerkmale.get(id)
|
||||
if (merkmale !== undefined && merkmale.titel.length > 0) {
|
||||
const tmdb = new TmdbClient(db?.einstellungLesen('tmdbApiKey') ?? '')
|
||||
const omdb = new OmdbClient(db?.einstellungLesen('omdbApiKey') ?? '')
|
||||
const nochmal = await zuordnen(merkmale.titel, tmdb, omdb, { istSerie: true, alternativen: merkmale.alternativen })
|
||||
if (nochmal.meta.typ === 'tv') {
|
||||
zuordnung = { ...nochmal, hinweise: ['Serie erkannt an der Titel-Struktur (mehrere gleich lange Titel).'] }
|
||||
}
|
||||
}
|
||||
}
|
||||
const geprueft = nachpruefen(zuordnung, messung)
|
||||
const hinweise = [...(zuordnung.hinweise ?? []), ...(geprueft.hinweise ?? [])]
|
||||
const neu = { ...geprueft, hinweise }
|
||||
discZuordnungen.set(id, neu)
|
||||
anHauptUndFenster({ art: 'disc-info', info: discInfoStand(id, neu) })
|
||||
}
|
||||
|
||||
function ablageOrdner(): string {
|
||||
const eingestellt = db?.einstellungLesen('ablage')
|
||||
if (eingestellt !== null && eingestellt !== undefined && eingestellt.length > 0) return eingestellt
|
||||
return join(process.env['USERPROFILE'] ?? '.', 'Videos', 'Rippy')
|
||||
}
|
||||
|
||||
function rohWurzel(): string {
|
||||
return join(ablageOrdner(), 'roh')
|
||||
}
|
||||
|
||||
// Die Einstellungs-Schlüssel, die die Pipeline liest — alle aus der EINEN
|
||||
// Quelle (§ 6.7). Das UI dafür kommt mit W-5; bis dahin gelten Defaults.
|
||||
// Quelle (§ 6.7).
|
||||
const PIPELINE_EINSTELLUNGEN = [
|
||||
'transcodeEnabled',
|
||||
'transcodePreset',
|
||||
@@ -404,6 +469,274 @@ function geraetVon(id: string): string | null {
|
||||
return listOptischeLaufwerke(laufwerkApi).find((g) => kennung(g) === id) ?? null
|
||||
}
|
||||
|
||||
// ── Vorgänge (5.4.0): Datensatz je Rip, Warteschlange, Aufräumen ────────
|
||||
function dateienAusJson(text: string): string[] {
|
||||
const liste: string[] = []
|
||||
try {
|
||||
const roh: unknown = JSON.parse(text)
|
||||
if (Array.isArray(roh)) liste.push(...roh.map(String))
|
||||
} catch (fehler) {
|
||||
// R4: Ein unlesbarer Eintrag wird gesagt — und zählt als „keine
|
||||
// fertigen Dateien bekannt", womit das Aufräumen ihn nie anfasst.
|
||||
console.error(`[kern] fertige Dateien nicht lesbar: ${String(fehler)}`)
|
||||
}
|
||||
return liste
|
||||
}
|
||||
|
||||
const ablageUmgebung: AblageUmgebung = {
|
||||
handbrake: () => finden('handbrake', db?.einstellungLesen('werkzeug.handbrake') ?? ''),
|
||||
ablage: ablageOrdner,
|
||||
einstellungen: einstellungenLaden,
|
||||
presetEmpfehlung: (typ) => presetEmpfehlung(typ, werkzeugAuskunft.backends, werkzeugAuskunft.presets),
|
||||
// Staffel-Laufzeiten von TMDb — Grundlage der Episoden-Zuordnung (§ 4.2).
|
||||
episodenLaufzeiten: async (tmdbId, staffel) => {
|
||||
const tmdb = new TmdbClient(db?.einstellungLesen('tmdbApiKey') ?? '')
|
||||
if (!tmdb.verfuegbar) return []
|
||||
const antwort = await tmdb.tvStaffel(tmdbId, staffel)
|
||||
const paare: Array<[number, number]> = []
|
||||
for (const ep of antwort?.episodes ?? []) {
|
||||
if (typeof ep.runtime === 'number' && ep.runtime > 0) paare.push([ep.episode_number, ep.runtime * 60])
|
||||
}
|
||||
return paare
|
||||
},
|
||||
bibliothekEintragen: (eintrag) => {
|
||||
try {
|
||||
db?.bibliothekEintragen(eintrag)
|
||||
anFenster({ art: 'bibliothek', eintraege: db?.bibliothekListe() ?? [] })
|
||||
} catch (fehler) {
|
||||
// R4: Ein Bibliotheks-Fehler kippt keinen gelungenen Rip, wird aber
|
||||
// gesagt.
|
||||
anFenster({ art: 'kern-fehler', text: `Bibliothek: ${String(fehler)}` })
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/** Nur bei einer PHASEN-Änderung in die Datenbank — nicht bei jedem
|
||||
* Prozent-Tick. */
|
||||
const letztePhase = new Map<string, string>()
|
||||
|
||||
const warteschlange = new KompressionsWarteschlange(
|
||||
(auftrag, melden, abbruch) => komprimierenUndAblegen(auftrag, ablageUmgebung, melden, abbruch),
|
||||
(stand) => {
|
||||
const l = stand.laeuft
|
||||
if (l !== null && letztePhase.get(l.vorgangId) !== l.phase) {
|
||||
letztePhase.set(l.vorgangId, l.phase)
|
||||
db?.vorgangAktualisieren(l.vorgangId, { phase: l.phase === 'legt-ab' ? 'legt-ab' : 'komprimiert' })
|
||||
}
|
||||
vorgaengeSenden()
|
||||
},
|
||||
(auftrag, ergebnis) => {
|
||||
letztePhase.delete(auftrag.vorgangId)
|
||||
const jetzt = new Date().toISOString()
|
||||
db?.vorgangAktualisieren(auftrag.vorgangId, {
|
||||
phase: ergebnis.status,
|
||||
beendet: jetzt,
|
||||
zielOrdner: ergebnis.zielOrdner,
|
||||
fertigBytes: ergebnis.fertigBytes,
|
||||
komprimiert: ergebnis.komprimiert,
|
||||
preset: ergebnis.preset,
|
||||
fehler: ergebnis.status === 'fertig' ? '' : ergebnis.text,
|
||||
fertigeDateien: JSON.stringify(ergebnis.dateien),
|
||||
})
|
||||
const vorgang = db?.vorgangLesen(auftrag.vorgangId)
|
||||
const titel = vorgang?.titel ?? auftrag.vorgangId
|
||||
const text = ergebnis.text + (ergebnis.hinweise.length > 0 ? ` · ${ergebnis.hinweise.join(' · ')}` : '')
|
||||
anHauptUndFenster({
|
||||
art: 'kompression-ergebnis',
|
||||
vorgangId: auftrag.vorgangId,
|
||||
titel,
|
||||
ok: ergebnis.status === 'fertig',
|
||||
text,
|
||||
})
|
||||
// Regel „sofort": direkt nach der gelungenen Kompression aufräumen.
|
||||
if (ergebnis.status === 'fertig') rohAufraeumenLauf()
|
||||
else vorgaengeSenden()
|
||||
},
|
||||
)
|
||||
|
||||
function vorgaengeStand(): VorgaengeStand {
|
||||
const regel = regelLesen(db?.einstellungLesen(AUFBEWAHRUNG_SCHLUESSEL))
|
||||
const liste = db?.vorgangListe() ?? []
|
||||
let rohGesamtBytes = 0
|
||||
const vorgaenge: VorgangEintrag[] = liste.map((v) => {
|
||||
const rohDa = v.rohGeloeschtAm === null && existsSync(v.rohOrdner)
|
||||
const rohBytes = rohDa ? ordnerGroesse(v.rohOrdner) : 0
|
||||
rohGesamtBytes += rohBytes
|
||||
const auftrag = auftragAusJson(v.auftrag)
|
||||
const ruhig = v.phase === 'fertig' || v.phase === 'fehler' || v.phase === 'abgebrochen'
|
||||
return {
|
||||
id: v.id,
|
||||
laufwerk: v.laufwerk,
|
||||
typ: v.typ,
|
||||
titel: v.titel,
|
||||
jahr: v.jahr,
|
||||
phase: v.phase,
|
||||
begonnen: v.begonnen,
|
||||
beendet: v.beendet,
|
||||
rohOrdner: v.rohOrdner,
|
||||
rohBytes,
|
||||
rohGeloescht: !rohDa,
|
||||
zielOrdner: v.zielOrdner,
|
||||
fertigBytes: v.fertigBytes,
|
||||
komprimiert: v.komprimiert,
|
||||
preset: v.preset,
|
||||
fehler: v.fehler,
|
||||
titelZahl: auftrag?.dateien.length ?? 0,
|
||||
neuKomprimierbar: rohDa && ruhig && auftrag !== null && auftrag.dateien.length > 0 && !warteschlange.kennt(v.id),
|
||||
faelligAm: v.phase === 'fertig' && v.komprimiert && rohDa ? faelligAm(v.beendet, regel) : '',
|
||||
}
|
||||
})
|
||||
const bekannte = new Set(liste.map((v) => v.rohOrdner))
|
||||
const fremde = fremdeOrdner(rohWurzel(), bekannte)
|
||||
for (const f of fremde) rohGesamtBytes += f.bytes
|
||||
return { vorgaenge, rohGesamtBytes, fremde, regel: regelText(regel), kompression: warteschlange.stand() }
|
||||
}
|
||||
|
||||
function vorgaengeSenden(): void {
|
||||
try {
|
||||
anHauptUndFenster({ art: 'vorgaenge', stand: vorgaengeStand() })
|
||||
} catch (fehler) {
|
||||
anFenster({ art: 'kern-fehler', text: `Vorgänge: ${String(fehler)}` })
|
||||
}
|
||||
}
|
||||
|
||||
function rohAufraeumenLauf(): void {
|
||||
if (db === null) return
|
||||
try {
|
||||
const regel = regelLesen(db.einstellungLesen(AUFBEWAHRUNG_SCHLUESSEL))
|
||||
const kandidaten = db.vorgangListe().map((v) => ({
|
||||
id: v.id,
|
||||
phase: v.phase,
|
||||
komprimiert: v.komprimiert,
|
||||
rohOrdner: v.rohOrdner,
|
||||
rohGeloeschtAm: v.rohGeloeschtAm,
|
||||
beendet: v.beendet,
|
||||
fertigeDateien: dateienAusJson(v.fertigeDateien),
|
||||
}))
|
||||
const ergebnis = aufraeumen(kandidaten, regel, new Date())
|
||||
const jetzt = new Date().toISOString()
|
||||
for (const id of ergebnis.geloescht) {
|
||||
db.vorgangAktualisieren(id, { rohGeloeschtAm: jetzt, rohBytes: 0 })
|
||||
hinweis(`Roh-Dateien von „${db.vorgangLesen(id)?.titel ?? id}" gelöscht (${regelText(regel)})`)
|
||||
}
|
||||
for (const f of ergebnis.fehler) {
|
||||
anFenster({ art: 'kern-fehler', text: `Roh-Ordner von ${f.id} nicht gelöscht: ${f.grund}` })
|
||||
}
|
||||
} catch (fehler) {
|
||||
anFenster({ art: 'kern-fehler', text: `Roh-Aufräumen: ${String(fehler)}` })
|
||||
}
|
||||
vorgaengeSenden()
|
||||
}
|
||||
|
||||
function vorgangRohLoeschen(vorgangId: string): void {
|
||||
const v = db?.vorgangLesen(vorgangId)
|
||||
if (v === null || v === undefined) {
|
||||
anFenster({ art: 'kern-fehler', text: `Vorgang ${vorgangId}: unbekannt.` })
|
||||
return
|
||||
}
|
||||
if (v.phase === 'rippt' || v.phase === 'wartet' || v.phase === 'komprimiert' || v.phase === 'legt-ab' || warteschlange.kennt(vorgangId)) {
|
||||
anFenster({ art: 'kern-fehler', text: `„${v.titel}" läuft noch (${v.phase}) — erst abbrechen, dann löschen.` })
|
||||
return
|
||||
}
|
||||
if (!istUnterRohWurzel(rohWurzel(), v.rohOrdner)) {
|
||||
anFenster({ art: 'kern-fehler', text: `Roh-Ordner von „${v.titel}" liegt nicht unter ${rohWurzel()} — nicht angefasst.` })
|
||||
return
|
||||
}
|
||||
try {
|
||||
rmSync(v.rohOrdner, { recursive: true, force: true })
|
||||
db?.vorgangAktualisieren(vorgangId, { rohGeloeschtAm: new Date().toISOString(), rohBytes: 0 })
|
||||
hinweis(`Roh-Dateien von „${v.titel}" gelöscht (von Hand).`)
|
||||
} catch (fehler) {
|
||||
anFenster({ art: 'kern-fehler', text: `Roh-Ordner von „${v.titel}" nicht gelöscht: ${String(fehler)}` })
|
||||
}
|
||||
vorgaengeSenden()
|
||||
}
|
||||
|
||||
function fremdenOrdnerLoeschen(ordner: string): void {
|
||||
if (!istUnterRohWurzel(rohWurzel(), ordner)) {
|
||||
anFenster({ art: 'kern-fehler', text: `„${ordner}" liegt nicht direkt unter ${rohWurzel()} — nicht angefasst.` })
|
||||
return
|
||||
}
|
||||
const bekannt = (db?.vorgangListe() ?? []).some((v) => v.rohOrdner.toLowerCase() === ordner.toLowerCase())
|
||||
if (bekannt) {
|
||||
anFenster({ art: 'kern-fehler', text: `„${ordner}" gehört zu einem Vorgang — dort löschen.` })
|
||||
return
|
||||
}
|
||||
try {
|
||||
rmSync(ordner, { recursive: true, force: true })
|
||||
hinweis(`Ordner „${ordner}" gelöscht (von Hand).`)
|
||||
} catch (fehler) {
|
||||
anFenster({ art: 'kern-fehler', text: `„${ordner}" nicht gelöscht: ${String(fehler)}` })
|
||||
}
|
||||
vorgaengeSenden()
|
||||
}
|
||||
|
||||
/** „Wiederholen ab Kompression": Die Roh-Dateien liegen genau dafür da. */
|
||||
function vorgangNeuKomprimieren(vorgangId: string, preset: string): void {
|
||||
const v = db?.vorgangLesen(vorgangId)
|
||||
if (v === null || v === undefined) {
|
||||
anFenster({ art: 'kern-fehler', text: `Vorgang ${vorgangId}: unbekannt.` })
|
||||
return
|
||||
}
|
||||
if (warteschlange.kennt(vorgangId)) {
|
||||
anFenster({ art: 'kern-fehler', text: `„${v.titel}" ist schon in der Warteschlange.` })
|
||||
return
|
||||
}
|
||||
const auftrag = auftragAusJson(v.auftrag)
|
||||
if (auftrag === null) {
|
||||
anFenster({ art: 'kern-fehler', text: `„${v.titel}": Der Auftrag ist nicht mehr lesbar — nur ein neuer Rip hilft.` })
|
||||
return
|
||||
}
|
||||
const vorhandene = auftrag.dateien.filter((d) => existsSync(d.pfad))
|
||||
if (vorhandene.length === 0) {
|
||||
anFenster({ art: 'kern-fehler', text: `„${v.titel}": Die Roh-Dateien sind nicht mehr da (${v.rohOrdner}).` })
|
||||
return
|
||||
}
|
||||
if (vorhandene.length < auftrag.dateien.length) {
|
||||
hinweis(`„${v.titel}": ${auftrag.dateien.length - vorhandene.length} Roh-Datei(en) fehlen — der Rest wird komprimiert.`)
|
||||
}
|
||||
const neu = {
|
||||
...auftrag,
|
||||
dateien: vorhandene,
|
||||
preset,
|
||||
// Die neue Fassung ersetzt die alte im selben Ordner.
|
||||
zielOrdner: v.phase === 'fertig' && v.zielOrdner.length > 0 && existsSync(v.zielOrdner) ? v.zielOrdner : '',
|
||||
}
|
||||
db?.vorgangAktualisieren(vorgangId, { phase: 'wartet', fehler: '', auftrag: auftragZuJson(neu) })
|
||||
const platz = warteschlange.einreihen(neu, v.titel)
|
||||
hinweis(`„${v.titel}": Kompression eingereiht${platz > 1 ? ` (Platz ${platz})` : ''}${preset.length > 0 ? ` mit Preset „${preset}"` : ''}.`)
|
||||
vorgaengeSenden()
|
||||
}
|
||||
|
||||
/** Nach einem Neustart: Was wartete noch? Was war mitten im Rip? */
|
||||
function vorgaengeWiederaufnehmen(): void {
|
||||
if (db === null) return
|
||||
const jetzt = new Date().toISOString()
|
||||
for (const v of db.vorgangListe()) {
|
||||
if (v.phase === 'rippt') {
|
||||
db.vorgangAktualisieren(v.id, {
|
||||
phase: 'abgebrochen',
|
||||
beendet: jetzt,
|
||||
fehler: 'Rippy wurde beendet, bevor der Rip fertig war — die Roh-Dateien können unvollständig sein.',
|
||||
})
|
||||
continue
|
||||
}
|
||||
if (v.phase === 'wartet' || v.phase === 'komprimiert' || v.phase === 'legt-ab') {
|
||||
const auftrag = auftragAusJson(v.auftrag)
|
||||
if (auftrag !== null && existsSync(v.rohOrdner) && auftrag.dateien.some((d) => existsSync(d.pfad))) {
|
||||
warteschlange.einreihen({ ...auftrag, dateien: auftrag.dateien.filter((d) => existsSync(d.pfad)) }, v.titel)
|
||||
hinweis(`„${v.titel}": Kompression nach dem Neustart wieder aufgenommen.`)
|
||||
} else {
|
||||
db.vorgangAktualisieren(v.id, {
|
||||
phase: 'fehler',
|
||||
beendet: jetzt,
|
||||
fehler: 'Nach dem Neustart fehlen die Roh-Dateien — die Kompression kann nicht wieder aufgenommen werden.',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ripVerwaltung = new Pipeline({
|
||||
makemkvcon: () => finden('makemkv', db?.einstellungLesen('werkzeug.makemkv') ?? ''),
|
||||
handbrake: () => finden('handbrake', db?.einstellungLesen('werkzeug.handbrake') ?? ''),
|
||||
@@ -425,37 +758,93 @@ const ripVerwaltung = new Pipeline({
|
||||
},
|
||||
ablage: ablageOrdner,
|
||||
einstellungen: einstellungenLaden,
|
||||
presetEmpfehlung: (typ) => presetEmpfehlung(typ, werkzeugAuskunft.backends, werkzeugAuskunft.presets),
|
||||
melden: (status) => anHauptUndFenster({ art: 'rip-status', status }),
|
||||
melden: (status) => {
|
||||
anHauptUndFenster({ art: 'rip-status', status })
|
||||
// Nach einem gescheiterten Rip (oder Lesefehlern) gleich das Windows-
|
||||
// Protokoll fragen: Disc oder Hardware? (5.4.0, Punkt 8)
|
||||
if (status.phase === 'fehler' || (status.phase === 'fertig' && status.lesefehler)) void laufwerkPruefen(status.id)
|
||||
},
|
||||
zuordnung: (id) => discZuordnungen.get(id) ?? null,
|
||||
discAngaben: (id) => {
|
||||
const m = discMerkmale.get(id)
|
||||
return m === undefined ? null : { staffel: m.staffel, discNr: m.discNr }
|
||||
},
|
||||
// Staffel-Laufzeiten von TMDb — Grundlage der Episoden-Zuordnung (§ 4.2).
|
||||
episodenLaufzeiten: async (tmdbId, staffel) => {
|
||||
const tmdb = new TmdbClient(db?.einstellungLesen('tmdbApiKey') ?? '')
|
||||
if (!tmdb.verfuegbar) return []
|
||||
const antwort = await tmdb.tvStaffel(tmdbId, staffel)
|
||||
const paare: Array<[number, number]> = []
|
||||
for (const ep of antwort?.episodes ?? []) {
|
||||
if (typeof ep.runtime === 'number' && ep.runtime > 0) paare.push([ep.episode_number, ep.runtime * 60])
|
||||
}
|
||||
return paare
|
||||
},
|
||||
fingerprint: (id) => discFingerprints.get(id) ?? null,
|
||||
bibliothekEintragen: (eintrag) => {
|
||||
try {
|
||||
db?.bibliothekEintragen(eintrag)
|
||||
anFenster({ art: 'bibliothek', eintraege: db?.bibliothekListe() ?? [] })
|
||||
} catch (fehler) {
|
||||
// R4: Ein Bibliotheks-Fehler kippt keinen gelungenen Rip, wird aber
|
||||
// gesagt.
|
||||
anFenster({ art: 'kern-fehler', text: `Bibliothek: ${String(fehler)}` })
|
||||
bibliothekEintragen: ablageUmgebung.bibliothekEintragen,
|
||||
vorgangBeginnt: (v) => {
|
||||
const vorgang: Vorgang = {
|
||||
id: v.id,
|
||||
laufwerk: v.laufwerk,
|
||||
typ: v.typ,
|
||||
titel: v.titel,
|
||||
jahr: v.jahr,
|
||||
phase: 'rippt',
|
||||
begonnen: v.begonnen,
|
||||
beendet: null,
|
||||
rohOrdner: v.rohOrdner,
|
||||
rohBytes: 0,
|
||||
rohGeloeschtAm: null,
|
||||
zielOrdner: '',
|
||||
fertigBytes: 0,
|
||||
komprimiert: false,
|
||||
preset: '',
|
||||
fehler: '',
|
||||
auftrag: '{}',
|
||||
lesefehler: false,
|
||||
fertigeDateien: '[]',
|
||||
}
|
||||
db?.vorgangAnlegen(vorgang)
|
||||
vorgaengeSenden()
|
||||
},
|
||||
vorgangEinreihen: (auftrag, titel) => {
|
||||
db?.vorgangAktualisieren(auftrag.vorgangId, {
|
||||
phase: 'wartet',
|
||||
titel,
|
||||
jahr: auftrag.zuordnung?.jahr ?? null,
|
||||
rohBytes: auftrag.dateien.reduce((s, d) => s + d.groesseBytes, 0),
|
||||
lesefehler: auftrag.lesefehler,
|
||||
auftrag: auftragZuJson(auftrag),
|
||||
})
|
||||
const platz = warteschlange.einreihen(auftrag, titel)
|
||||
vorgaengeSenden()
|
||||
return platz
|
||||
},
|
||||
vorgangEndet: (id, phase, text) => {
|
||||
db?.vorgangAktualisieren(id, { phase, beendet: new Date().toISOString(), fehler: text })
|
||||
vorgaengeSenden()
|
||||
},
|
||||
nachRipAuswerfen: (id) => {
|
||||
// Vorgabe: auswerfen — die Warteschlange will ein freies Laufwerk.
|
||||
if (db?.einstellungLesen('autoAuswurf') === 'false') return
|
||||
void auswerfen(id)
|
||||
},
|
||||
})
|
||||
|
||||
// ── Laufwerks-Gesundheit (5.4.0, Punkt 8) ───────────────────────────────
|
||||
const GESUNDHEIT_STUNDEN = 24
|
||||
|
||||
async function laufwerkPruefen(id: string): Promise<void> {
|
||||
if (process.platform !== 'win32') return
|
||||
const ergebnis = await ereignisseLesen(GESUNDHEIT_STUNDEN)
|
||||
if ('fehler' in ergebnis) {
|
||||
anFenster({
|
||||
art: 'laufwerk-gesundheit',
|
||||
id,
|
||||
bericht: {
|
||||
urteil: 'nicht-lesbar',
|
||||
text: `Das Windows-Protokoll ließ sich nicht lesen: ${ergebnis.fehler}`,
|
||||
fehlerhafteBloecke: 0,
|
||||
controllerfehler: 0,
|
||||
sonstige: 0,
|
||||
letzte: [],
|
||||
stunden: GESUNDHEIT_STUNDEN,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
anFenster({ art: 'laufwerk-gesundheit', id, bericht: beurteilen(ergebnis.ereignisse, GESUNDHEIT_STUNDEN) })
|
||||
}
|
||||
|
||||
// ── Einstellungen (§ 6.7): das Fenster liest und schreibt NUR über den
|
||||
// Kern; erlaubt sind genau diese Schlüssel ───────────────────────────
|
||||
const ERLAUBTE_EINSTELLUNGEN = new Set([
|
||||
@@ -479,6 +868,7 @@ const ERLAUBTE_EINSTELLUNGEN = new Set([
|
||||
'werkzeug.makemkv',
|
||||
'werkzeug.handbrake',
|
||||
'werkzeug.flac',
|
||||
AUFBEWAHRUNG_SCHLUESSEL,
|
||||
])
|
||||
|
||||
function einstellungenSenden(): void {
|
||||
@@ -515,6 +905,25 @@ async function schluesselLauf(): Promise<void> {
|
||||
// Täglich neu prüfen (§ 9.1) — ohne den Prozess-Exit aufzuhalten.
|
||||
setInterval(() => void schluesselLauf(), 24 * 60 * 60 * 1000).unref()
|
||||
|
||||
/** MakeMKVs Spur-Auswahlregel sicherstellen (5.4.0) — sonst fehlen
|
||||
* Sprachen in der Roh-Datei, die der Dialog anbot. */
|
||||
async function spurauswahlLauf(): Promise<void> {
|
||||
if (process.platform !== 'win32') return
|
||||
try {
|
||||
const { spurauswahlSicherstellen } = await import('./werkzeuge/spurauswahl')
|
||||
const stand = spurauswahlSicherstellen()
|
||||
if (stand.stand === 'eigen' || stand.stand === 'fehler') {
|
||||
anFenster({ art: 'kern-fehler', text: `MakeMKV-Auswahlregel: ${stand.text}` })
|
||||
} else if (stand.stand === 'gesetzt') {
|
||||
hinweis(stand.text)
|
||||
} else {
|
||||
console.log(`[kern] ${stand.text}`)
|
||||
}
|
||||
} catch (fehler) {
|
||||
anFenster({ art: 'kern-fehler', text: `MakeMKV-Auswahlregel: ${String(fehler)}` })
|
||||
}
|
||||
}
|
||||
|
||||
// ── MakeMKV-Update (§ 9): holen, prüfen, installieren lassen ────────────
|
||||
// Der Kern lädt und PRÜFT die Datei; STARTEN kann sie nur das Haupt
|
||||
// (shell → UAC-Abfrage; der erhöhte Installer hängt dadurch nicht an der
|
||||
@@ -702,7 +1111,7 @@ process.parentPort.on('message', (ereignis) => {
|
||||
} else if (frage.art === 'titel-lesen') {
|
||||
void titelLesen(frage.id)
|
||||
} else if (frage.art === 'rip-start') {
|
||||
void laufwerkeBereit.then(() => ripVerwaltung.starten(frage.id, frage.auftrag ?? []))
|
||||
void laufwerkeBereit.then(() => ripVerwaltung.starten(frage.id, frage.auftrag ?? [], frage.optionen ?? {}))
|
||||
} else if (frage.art === 'rip-abbruch') {
|
||||
ripVerwaltung.abbrechen(frage.id)
|
||||
} else if (frage.art === 'rip-ueberspringen') {
|
||||
@@ -719,6 +1128,8 @@ process.parentPort.on('message', (ereignis) => {
|
||||
} else {
|
||||
db.einstellungSchreiben(frage.schluessel, frage.wert)
|
||||
einstellungenSenden()
|
||||
// Eine neue Aufbewahrungs-Regel wirkt sofort auf die Anzeige.
|
||||
if (frage.schluessel === AUFBEWAHRUNG_SCHLUESSEL) vorgaengeSenden()
|
||||
}
|
||||
} else if (frage.art === 'einstellungen-laden') {
|
||||
einstellungenSenden()
|
||||
@@ -730,6 +1141,22 @@ process.parentPort.on('message', (ereignis) => {
|
||||
void makemkvUpdateLauf()
|
||||
} else if (frage.art === 'schluessel-pruefen') {
|
||||
void schluesselLauf().then(() => werkzeugeSenden())
|
||||
} else if (frage.art === 'vorgaenge-laden') {
|
||||
vorgaengeSenden()
|
||||
} else if (frage.art === 'vorgang-roh-loeschen') {
|
||||
vorgangRohLoeschen(frage.vorgangId)
|
||||
} else if (frage.art === 'vorgang-neu-komprimieren') {
|
||||
vorgangNeuKomprimieren(frage.vorgangId, frage.preset ?? '')
|
||||
} else if (frage.art === 'kompression-abbrechen') {
|
||||
if (!warteschlange.abbrechen(frage.vorgangId)) {
|
||||
anFenster({ art: 'kern-fehler', text: `Vorgang ${frage.vorgangId} ist nicht in der Warteschlange.` })
|
||||
}
|
||||
} else if (frage.art === 'roh-aufraeumen') {
|
||||
rohAufraeumenLauf()
|
||||
} else if (frage.art === 'fremden-ordner-loeschen') {
|
||||
fremdenOrdnerLoeschen(frage.ordner)
|
||||
} else if (frage.art === 'laufwerk-pruefen') {
|
||||
void laufwerkPruefen(frage.id)
|
||||
}
|
||||
})
|
||||
fensterPort.start()
|
||||
@@ -744,6 +1171,7 @@ process.parentPort.on('message', (ereignis) => {
|
||||
anFenster({ art: 'schluessel-status', stand: schluesselStand })
|
||||
}
|
||||
})
|
||||
vorgaengeSenden()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -759,11 +1187,20 @@ function bereitNachricht(): KernNachricht {
|
||||
process.parentPort.postMessage(bereitNachricht())
|
||||
// Das Haupt braucht die Einstellungen (Update-Adresse) auch ohne Fenster.
|
||||
einstellungenSenden()
|
||||
// Was nach dem letzten Beenden noch offen war (5.4.0).
|
||||
vorgaengeWiederaufnehmen()
|
||||
// Nicht im Smoke: Der Schlüssel-Lauf spricht Forum + makemkvcon an —
|
||||
// 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 (!smokeKern && process.env['RIPPY_OHNE_SCHLUESSEL'] !== '1') {
|
||||
void schluesselLauf()
|
||||
void spurauswahlLauf()
|
||||
// Werkzeuge einmal beim Start messen — die Preset-Empfehlung der
|
||||
// Pipeline braucht Backends und Preset-Liste auch ohne offenes Fenster.
|
||||
void werkzeugeSenden()
|
||||
}
|
||||
// Roh-Aufräumen (5.4.0): eine Minute nach dem Start, dann täglich — und
|
||||
// nach jeder gelungenen Kompression (siehe Warteschlange).
|
||||
if (!smokeKern) {
|
||||
setTimeout(() => rohAufraeumenLauf(), 60_000).unref()
|
||||
setInterval(() => rohAufraeumenLauf(), 24 * 60 * 60 * 1000).unref()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user