feat(v5): Oberflaeche 5.4.0 — Sprachen je Titel, Staffel/Folge, Vorschau, Kino-Banner v2, Roh-Dateien, Warteschlange

WAS: Der Auswahl-Dialog zeigt je Titel die gemessenen Ton- und
Untertitelspuren als Chips (vorbelegt aus der Einstellung, sonst alles,
was der Titel hat), Felder fuer Staffel und erste Folge, und zwei Zeilen
Zielpfad-Vorschau (gemeinsam/ablagepfad.ts — dieselbe Namensregel wie der
Kern). Die Dialoge leben jetzt im App-Zustand statt in der Kachel: ein
leerer Takt der Wache raeumte sonst Kachel UND Dialog ab.
Die Laufwerks-Kachel ist das Kino-Banner v2: Logo statt Text, Tagline,
Zeile Jahr/FSK/Laufzeit/Genre/Bewertung, Darsteller-Streifen, die
Hinweise der Nachpruefung, ein Knopf „Laufwerk pruefen" mit Urteil aus
dem Windows-Protokoll, und nach dem Rip der Stand der Kompression.
Neu in der Uebersicht: die Kompressions-Karte (laeuft/wartet, Abbruch).
Neu in den Einstellungen: „Roh-Dateien" mit Regel, Belegung, jedem
Vorgang (Neu komprimieren mit Preset, Roh loeschen) und Ordnern ohne
Vorgang; der Auswurf-Schalter in „Ablage"; der Datenbank-Pfad unter
„Programm". Das Haupt zeigt die Kompression in Taskleiste und Tray und
laesst Updates auch auf sie warten. Playwright kommt als Bau-Werkzeug
dazu (Commander-Ja nach Regel B; MIT, wird nicht ausgeliefert).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-01 22:13:22 +02:00
co-authored by Claude Fable 5.1
parent 3f7f962dff
commit e3ce8220f1
7 changed files with 1046 additions and 171 deletions
+48
View File
@@ -24,6 +24,7 @@
"electron": "44.0.0", "electron": "44.0.0",
"electron-builder": "^26.15.3", "electron-builder": "^26.15.3",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
"playwright": "^1.62.1",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
@@ -5612,6 +5613,53 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/playwright": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/plist": { "node_modules/plist": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
+1
View File
@@ -32,6 +32,7 @@
"electron": "44.0.0", "electron": "44.0.0",
"electron-builder": "^26.15.3", "electron-builder": "^26.15.3",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
"playwright": "^1.62.1",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
File diff suppressed because it is too large Load Diff
+106 -52
View File
@@ -11,12 +11,16 @@ import {
type BeschaffungStand, type BeschaffungStand,
type BibliothekEintrag, type BibliothekEintrag,
type DiscInfoStand, type DiscInfoStand,
type FensterNachricht,
type GeraeteInfo, type GeraeteInfo,
type KernNachricht, type KernNachricht,
type LaufwerkBericht,
type MetaVorschlag, type MetaVorschlag,
type TitelStand, type RipAuftragOptionen,
type RipAuftragTitel, type RipAuftragTitel,
type RipStatus, type RipStatus,
type TitelStand,
type VorgaengeStand,
type WerkzeugAuskunft, type WerkzeugAuskunft,
} from '../gemeinsam/nachrichten' } from '../gemeinsam/nachrichten'
@@ -54,13 +58,18 @@ export interface Verbindung {
} | null } | null
/** Stand der MakeMKV-Beschaffung (§ 9) — null, solange nie eine lief. */ /** Stand der MakeMKV-Beschaffung (§ 9) — null, solange nie eine lief. */
beschaffung: BeschaffungStand | null beschaffung: BeschaffungStand | null
/** Vorgänge, Roh-Belegung und Kompressions-Warteschlange (5.4.0) — null,
* solange der Kern noch nichts geschickt hat. */
vorgaenge: VorgaengeStand | null
/** Was das Windows-Protokoll je Laufwerk sagt (5.4.0) — bleibt stehen. */
gesundheit: Record<string, LaufwerkBericht>
/** Kurze Ereigniszeilen (neueste zuerst): Disc rein/raus, Auswurf, Fehler. */ /** Kurze Ereigniszeilen (neueste zuerst): Disc rein/raus, Auswurf, Fehler. */
meldungen: string[] meldungen: string[]
} }
type Horcher = (stand: Verbindung) => void type Horcher = (stand: Verbindung) => void
const MAX_MELDUNGEN = 20 const MAX_MELDUNGEN = 30
const stand: Verbindung = { const stand: Verbindung = {
verbunden: false, verbunden: false,
@@ -78,27 +87,32 @@ const stand: Verbindung = {
bibliothek: [], bibliothek: [],
schluessel: null, schluessel: null,
beschaffung: null, beschaffung: null,
vorgaenge: null,
gesundheit: {},
meldungen: [], meldungen: [],
} }
const horcher = new Set<Horcher>() const horcher = new Set<Horcher>()
let gestartet = false let gestartet = false
let port: MessagePort | null = null let port: MessagePort | null = null
function melden(): void { function kopie(): Verbindung {
for (const h of horcher) { return {
h({ ...stand,
...stand, laufwerke: [...stand.laufwerke],
laufwerke: [...stand.laufwerke], rips: { ...stand.rips },
rips: { ...stand.rips }, discInfos: { ...stand.discInfos },
discInfos: { ...stand.discInfos }, vorschlaege: { ...stand.vorschlaege },
vorschlaege: { ...stand.vorschlaege }, einstellungen: { ...stand.einstellungen },
einstellungen: { ...stand.einstellungen }, bibliothek: [...stand.bibliothek],
bibliothek: [...stand.bibliothek], gesundheit: { ...stand.gesundheit },
meldungen: [...stand.meldungen], meldungen: [...stand.meldungen],
})
} }
} }
function melden(): void {
for (const h of horcher) h(kopie())
}
function meldung(text: string): void { function meldung(text: string): void {
stand.meldungen = [text, ...stand.meldungen].slice(0, MAX_MELDUNGEN) stand.meldungen = [text, ...stand.meldungen].slice(0, MAX_MELDUNGEN)
} }
@@ -116,6 +130,9 @@ function kernNachricht(nachricht: KernNachricht): void {
case 'kern-fehler': case 'kern-fehler':
meldung(`${nachricht.text}`) meldung(`${nachricht.text}`)
break break
case 'kern-hinweis':
meldung(nachricht.text)
break
case 'laufwerke': case 'laufwerke':
stand.laufwerke = nachricht.liste stand.laufwerke = nachricht.liste
for (const ereignis of nachricht.ereignisse) { for (const ereignis of nachricht.ereignisse) {
@@ -137,11 +154,20 @@ function kernNachricht(nachricht: KernNachricht): void {
break break
} }
case 'disc-info': { case 'disc-info': {
const vorher = stand.discInfos[nachricht.info.id]
stand.discInfos = { ...stand.discInfos, [nachricht.info.id]: nachricht.info } stand.discInfos = { ...stand.discInfos, [nachricht.info.id]: nachricht.info }
if (nachricht.info.titel.length > 0) { if (nachricht.info.titel.length > 0) {
meldung( // Dieselbe Erkennung nicht zweimal melden — die Nachprüfung nach
`Laufwerk ${nachricht.info.id}: erkannt als „${nachricht.info.titel}${nachricht.info.jahr !== null ? ` (${nachricht.info.jahr})` : ''}“ · ${Math.round(nachricht.info.confidence * 100)} %`, // dem Info-Lauf schickt den Stand erneut, mit Hinweisen.
) const neuErkannt = vorher === undefined || vorher.titel !== nachricht.info.titel
if (neuErkannt) {
meldung(
`Laufwerk ${nachricht.info.id}: erkannt als „${nachricht.info.titel}${nachricht.info.jahr !== null ? ` (${nachricht.info.jahr})` : ''}“ · ${Math.round(nachricht.info.confidence * 100)} %`,
)
}
for (const h of nachricht.info.hinweise) {
if (vorher === undefined || !vorher.hinweise.includes(h)) meldung(`Laufwerk ${nachricht.info.id}: ${h}`)
}
} }
break break
} }
@@ -169,6 +195,18 @@ function kernNachricht(nachricht: KernNachricht): void {
stand.beschaffung = nachricht.stand stand.beschaffung = nachricht.stand
if (nachricht.stand.fehler) meldung(`⚠ MakeMKV-Update: ${nachricht.stand.text}`) if (nachricht.stand.fehler) meldung(`⚠ MakeMKV-Update: ${nachricht.stand.text}`)
break break
case 'vorgaenge':
stand.vorgaenge = nachricht.stand
break
case 'kompression-ergebnis':
meldung(`${nachricht.ok ? '' : '⚠ '}Kompression „${nachricht.titel}": ${nachricht.text}`)
break
case 'laufwerk-gesundheit':
stand.gesundheit = { ...stand.gesundheit, [nachricht.id]: nachricht.bericht }
if (nachricht.bericht.urteil === 'hardware' || nachricht.bericht.urteil === 'disc') {
meldung(`Laufwerk ${nachricht.id}: ${nachricht.bericht.text}`)
}
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) const laeuft = ['liest-info', 'rippt', 'komprimiert', 'legt-ab'].includes(nachricht.status.phase)
@@ -201,6 +239,9 @@ function kernNachricht(nachricht: KernNachricht): void {
} }
break break
} }
case 'installer-starten':
// Nur fürs Haupt gedacht — im Fenster nichts zu tun.
break
} }
melden() melden()
} }
@@ -213,10 +254,11 @@ function portAnnehmen(neuerPort: MessagePort): void {
if (istKernNachricht(nachricht)) kernNachricht(nachricht) if (istKernNachricht(nachricht)) kernNachricht(nachricht)
} }
neuerPort.start() neuerPort.start()
neuerPort.postMessage({ art: 'ping', zeit: Date.now() }) senden({ art: 'ping', zeit: Date.now() })
neuerPort.postMessage({ art: 'einstellungen-laden' }) senden({ art: 'einstellungen-laden' })
neuerPort.postMessage({ art: 'werkzeuge-laden' }) senden({ art: 'werkzeuge-laden' })
neuerPort.postMessage({ art: 'bibliothek-laden' }) senden({ art: 'bibliothek-laden' })
senden({ art: 'vorgaenge-laden' })
melden() melden()
} }
@@ -241,20 +283,16 @@ export function verbindungStarten(): void {
export function aufVerbindung(h: Horcher): () => void { export function aufVerbindung(h: Horcher): () => void {
horcher.add(h) horcher.add(h)
h({ h(kopie())
...stand,
laufwerke: [...stand.laufwerke],
rips: { ...stand.rips },
discInfos: { ...stand.discInfos },
vorschlaege: { ...stand.vorschlaege },
einstellungen: { ...stand.einstellungen },
bibliothek: [...stand.bibliothek],
meldungen: [...stand.meldungen],
})
return () => horcher.delete(h) return () => horcher.delete(h)
} }
function anKern(nachricht: { art: string; id: string; auftrag?: RipAuftragTitel[] }, wasWar: string): void { function senden(nachricht: FensterNachricht): void {
port?.postMessage(nachricht)
}
/** Schickt eine Nachricht — und sagt es, wenn kein Kern da ist (R4). */
function anKern(nachricht: FensterNachricht, wasWar: string): void {
if (port === null) { if (port === null) {
meldung(`${wasWar}: keine Verbindung zum Kern`) meldung(`${wasWar}: keine Verbindung zum Kern`)
melden() melden()
@@ -273,8 +311,8 @@ export function titelLesen(id: string): void {
} }
/** `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[] = []): void { export function ripStarten(id: string, auftrag: RipAuftragTitel[] = [], optionen: RipAuftragOptionen = {}): void {
anKern({ art: 'rip-start', id, auftrag }, `Rip ${id}`) anKern({ art: 'rip-start', id, auftrag, optionen }, `Rip ${id}`)
} }
/** Den laufenden Titel aufgeben und mit dem nächsten weitermachen. */ /** Den laufenden Titel aufgeben und mit dem nächsten weitermachen. */
@@ -287,34 +325,50 @@ export function ripAbbrechen(id: string): void {
} }
export function metadatenSuchen(id: string, text: string): void { export function metadatenSuchen(id: string, text: string): void {
if (port === null) return senden({ art: 'metadaten-suchen', id, text })
port.postMessage({ art: 'metadaten-suchen', id, text })
} }
export function zuordnungSetzen(id: string, vorschlag: MetaVorschlag): void { export function zuordnungSetzen(id: string, vorschlag: MetaVorschlag): void {
if (port === null) return senden({ art: 'zuordnung-setzen', id, vorschlag })
port.postMessage({ art: 'zuordnung-setzen', id, vorschlag })
} }
export function makemkvUpdate(): void { export function makemkvUpdate(): void {
if (port === null) { anKern({ art: 'makemkv-update' }, 'MakeMKV-Update')
meldung('⚠ MakeMKV-Update: keine Verbindung zum Kern')
melden()
return
}
port.postMessage({ art: 'makemkv-update' })
} }
export function schluesselPruefen(): void { export function schluesselPruefen(): void {
if (port === null) return senden({ art: 'schluessel-pruefen' })
port.postMessage({ art: 'schluessel-pruefen' })
} }
export function einstellungSetzen(schluessel: string, wert: string): void { export function einstellungSetzen(schluessel: string, wert: string): void {
if (port === null) { anKern({ art: 'einstellung-setzen', schluessel, wert }, `Einstellung ${schluessel}`)
meldung(`⚠ Einstellung ${schluessel}: keine Verbindung zum Kern`) }
melden()
return // ── 5.4.0: Vorgänge, Roh-Dateien, Warteschlange, Laufwerks-Gesundheit ──
} export function vorgaengeLaden(): void {
port.postMessage({ art: 'einstellung-setzen', schluessel, wert }) senden({ art: 'vorgaenge-laden' })
}
export function vorgangRohLoeschen(vorgangId: string): void {
anKern({ art: 'vorgang-roh-loeschen', vorgangId }, 'Roh-Dateien löschen')
}
export function vorgangNeuKomprimieren(vorgangId: string, preset: string): void {
anKern({ art: 'vorgang-neu-komprimieren', vorgangId, preset }, 'Neu komprimieren')
}
export function kompressionAbbrechen(vorgangId: string): void {
anKern({ art: 'kompression-abbrechen', vorgangId }, 'Kompression abbrechen')
}
export function rohAufraeumen(): void {
anKern({ art: 'roh-aufraeumen' }, 'Roh-Dateien aufräumen')
}
export function fremdenOrdnerLoeschen(ordner: string): void {
anKern({ art: 'fremden-ordner-loeschen', ordner }, 'Ordner löschen')
}
export function laufwerkPruefen(id: string): void {
anKern({ art: 'laufwerk-pruefen', id }, `Laufwerk ${id} prüfen`)
} }
+74
View File
@@ -0,0 +1,74 @@
// Ablage-Namen und die Zielpfad-Vorschau — PUR, ohne Dateisystem, damit
// Kern UND Fenster dieselbe Regel benutzen (5.4.0: „Vorschau, welche Datei
// landet wo" im Auswahl-Dialog). Die Kern-Seite (kern/ablage/struktur.ts)
// baut auf diesen Namen ihre echten Ordner.
// Zeichen, die in Ordnernamen auf SMB/NTFS/ext4 Ärger machen.
// eslint-Hinweis unnötig: bewusst inklusive Steuerzeichen.
// eslint-disable-next-line no-control-regex
const VERBOTEN = /[<>:"/\\|?*\x00-\x1f]/g
/** Titel → dateisystem-tauglicher Ordnername (pur). */
export function sichererName(titel: string, jahr?: number | string | null): string {
let name = (titel ?? '').replace(VERBOTEN, '').trim().replace(/\.+$/, '')
name = name.replace(/\s+/g, ' ').slice(0, 150).trim()
if (name.length === 0) return ''
// Kein doppeltes Jahr: OMDb-Titel tragen es teils schon im Namen.
if (jahr !== undefined && jahr !== null && String(jahr).length > 0 && !name.endsWith(`(${jahr})`)) {
name = `${name} (${jahr})`
}
return name
}
export function staffelOrdnerName(staffel: number): string {
return `Season ${String(Math.trunc(staffel)).padStart(2, '0')}`
}
export function folgenDateiname(serie: string, staffel: number, folge: number): string {
const s = String(Math.trunc(staffel)).padStart(2, '0')
const e = String(Math.trunc(folge)).padStart(2, '0')
return `${sichererName(serie) || 'Serie'} S${s}E${e}.mkv`
}
export interface VorschauEingabe {
ablage: string
alsSerie: boolean
titel: string
jahr: number | null
staffel: number | null
ersteFolge: number | null
/** Zahl der gewählten Folgen (Serie) bzw. Hauptfilme (Film). */
hauptTitel: number
extras: number
/** Der Vorgangsname — gilt, wenn kein Titel erkannt ist. */
vorgangsName: string
}
/** Zwei Zeilen: Zielordner und was darin landet (pur). Windows-Trenner,
* weil die Vorschau eine Windows-Ablage beschreibt. */
export function zielVorschau(e: VorschauEingabe): [string, string] {
const wurzel = e.ablage.replace(/[\\/]+$/, '')
const titel = e.titel.trim().length > 0 ? e.titel.trim() : e.vorgangsName
if (e.alsSerie) {
const staffel = e.staffel ?? 1
const ordner = `${wurzel}\\Serien\\${sichererName(titel) || 'Unbekannte Serie'}\\${staffelOrdnerName(staffel)}\\`
const n = Math.max(1, e.hauptTitel)
let inhalt: string
if (e.ersteFolge !== null && e.ersteFolge > 0) {
const erste = folgenDateiname(titel, staffel, e.ersteFolge)
inhalt =
n === 1
? erste
: `${erste}${folgenDateiname(titel, staffel, e.ersteFolge + n - 1)} (${n} Folgen, fortlaufend)`
} else {
inhalt = `${n} ${n === 1 ? 'Folge' : 'Folgen'} — Nummern aus den TMDb-Laufzeiten, sonst bleiben die MakeMKV-Namen`
}
if (e.extras > 0) inhalt += ` · extras\\ (${e.extras})`
return [ordner, inhalt]
}
const ordner = `${wurzel}\\Filme\\${sichererName(titel, e.jahr) || e.vorgangsName}\\`
const teile: string[] = []
if (e.hauptTitel > 0) teile.push(e.hauptTitel === 1 ? 'der Hauptfilm' : `${e.hauptTitel} Hauptfilme`)
if (e.extras > 0) teile.push(`extras\\ (${e.extras} ${e.extras === 1 ? 'Zugabe' : 'Zugaben'})`)
return [ordner, teile.length > 0 ? teile.join(' · ') : 'nichts gewählt']
}
+46 -17
View File
@@ -94,6 +94,12 @@ function start(): void {
} }
} else if (nachricht.art === 'installer-starten') { } else if (nachricht.art === 'installer-starten') {
void installerStarten(nachricht.pfad) void installerStarten(nachricht.pfad)
} else if (nachricht.art === 'vorgaenge') {
// 5.4.0: Die Kompression läuft OHNE Laufwerk weiter — Updates
// warten auch auf sie, und die Taskleiste zeigt ihren Fortschritt.
kompressionVerarbeiten(nachricht.stand.kompression.laeuft)
} else if (nachricht.art === 'kompression-ergebnis' && !smokeModus) {
toast(nachricht.ok ? 'Rippy ist fertig' : 'Rippy: Kompression fehlgeschlagen', `${nachricht.titel}: ${nachricht.text}`)
} else if (nachricht.art === 'disc-info' && nachricht.info.titel.length > 0 && !smokeModus) { } else if (nachricht.art === 'disc-info' && nachricht.info.titel.length > 0 && !smokeModus) {
// Windows-Toast beim Erkennen (§ 7) — Klick holt das Fenster hoch. // Windows-Toast beim Erkennen (§ 7) — Klick holt das Fenster hoch.
toast( toast(
@@ -109,6 +115,43 @@ function start(): void {
smokeModus, smokeModus,
) )
// Die laufende Kompression (5.4.0) — ein eigener Schlüssel in den
// aktiven Vorgängen, weil sie an keinem Laufwerk mehr hängt.
const KOMPRESSION = 'kompression'
let kompressionStand: { titel: string; prozent: number } | null = null
function kompressionVerarbeiten(laeuft: { titel: string; prozent: number } | null): void {
kompressionStand = laeuft === null ? null : { titel: laeuft.titel, prozent: laeuft.prozent }
if (laeuft === null) aktiveVorgaenge.delete(KOMPRESSION)
else aktiveVorgaenge.add(KOMPRESSION)
anzeigeAktualisieren()
}
/** Taskleiste und Tray aus dem, was gerade läuft: ein Rip zuerst, sonst
* die Kompression — sonst nichts. */
let letzterRip: Extract<KernNachricht, { art: 'rip-status' }>['status'] | null = null
function anzeigeAktualisieren(): void {
const rip = letzterRip !== null && letzterRip.phase === 'rippt' ? letzterRip : null
if (fenster !== null && !fenster.isDestroyed()) {
if (rip !== null && rip.prozent >= 0) {
fenster.setProgressBar(rip.prozent / 100)
} else if (letzterRip !== null && letzterRip.phase === 'fehler') {
fenster.setProgressBar(1, { mode: 'error' })
} else if (kompressionStand !== null && kompressionStand.prozent >= 0) {
fenster.setProgressBar(kompressionStand.prozent / 100)
} else {
fenster.setProgressBar(-1)
}
}
if (rip !== null) {
trayHinweis(`rippt Laufwerk ${rip.id}${rip.prozent >= 0 ? ` (${rip.prozent} %)` : ''}`)
} else if (kompressionStand !== null) {
trayHinweis(`komprimiert „${kompressionStand.titel}"${kompressionStand.prozent >= 0 ? ` (${kompressionStand.prozent} %)` : ''}`)
} else {
trayHinweis('')
}
}
// § 7: Toasts, Taskleisten-Fortschritt und Tray-Zustand aus dem // § 7: Toasts, Taskleisten-Fortschritt und Tray-Zustand aus dem
// Rip-Status — die Information, die man will, ohne das Fenster zu öffnen. // Rip-Status — die Information, die man will, ohne das Fenster zu öffnen.
function ripStatusVerarbeiten(status: Extract<KernNachricht, { art: 'rip-status' }>['status']): void { function ripStatusVerarbeiten(status: Extract<KernNachricht, { art: 'rip-status' }>['status']): void {
@@ -117,25 +160,11 @@ function start(): void {
} else { } else {
aktiveVorgaenge.delete(status.id) aktiveVorgaenge.delete(status.id)
} }
if (fenster !== null && !fenster.isDestroyed()) { letzterRip = status
if ((status.phase === 'rippt' || status.phase === 'komprimiert') && status.prozent >= 0) { anzeigeAktualisieren()
fenster.setProgressBar(status.prozent / 100)
} else if (status.phase === 'fehler') {
fenster.setProgressBar(1, { mode: 'error' })
} else {
fenster.setProgressBar(-1)
}
}
if (status.phase === 'rippt') {
trayHinweis(`rippt Laufwerk ${status.id}${status.prozent >= 0 ? ` (${status.prozent} %)` : ''}`)
} else if (status.phase === 'komprimiert') {
trayHinweis(`komprimiert Laufwerk ${status.id}${status.prozent >= 0 ? ` (${status.prozent} %)` : ''}`)
} else {
trayHinweis('')
}
if (smokeModus) return if (smokeModus) return
if (status.phase === 'fertig') { if (status.phase === 'fertig') {
toast('Rippy ist fertig', status.text) toast('Rippy: Rip fertig', status.text)
} else if (status.phase === 'fehler') { } else if (status.phase === 'fehler') {
toast('Rippy: Fehler', status.text) toast('Rippy: Fehler', status.text)
} }
+5 -17
View File
@@ -11,23 +11,11 @@ import { existsSync, readdirSync, renameSync } from 'node:fs'
// müssen dort physisch funktionieren. Feste Windows-Muster (Katalog, // müssen dort physisch funktionieren. Feste Windows-Muster (Katalog,
// %ProgramFiles%) bleiben dagegen bei path.win32. // %ProgramFiles%) bleiben dagegen bei path.win32.
import { join } from 'node:path' import { join } from 'node:path'
// EINE Namensregel für Kern und Fenster (5.4.0: die Zielpfad-Vorschau im
// Auswahl-Dialog rechnet mit derselben Funktion).
import { sichererName, staffelOrdnerName } from '../../gemeinsam/ablagepfad'
// Zeichen, die in Ordnernamen auf SMB/NTFS/ext4 Ärger machen. export { sichererName }
// eslint-Hinweis unnötig: bewusst inklusive Steuerzeichen.
// eslint-disable-next-line no-control-regex
const VERBOTEN = /[<>:"/\\|?*\x00-\x1f]/g
/** Titel → dateisystem-tauglicher Ordnername (pur). */
export function sichererName(titel: string, jahr?: number | string | null): string {
let name = (titel ?? '').replace(VERBOTEN, '').trim().replace(/\.+$/, '')
name = name.replace(/\s+/g, ' ').slice(0, 150).trim()
if (name.length === 0) return ''
// Kein doppeltes Jahr: OMDb-Titel tragen es teils schon im Namen.
if (jahr !== undefined && jahr !== null && String(jahr).length > 0 && !name.endsWith(`(${jahr})`)) {
name = `${name} (${jahr})`
}
return name
}
/** Zielordner: sprechender Name; existiert er schon (Disc doppelt /** Zielordner: sprechender Name; existiert er schon (Disc doppelt
* gerippt), wird die Job-Kennung angehängt statt zu überschreiben. */ * gerippt), wird die Job-Kennung angehängt statt zu überschreiben. */
@@ -48,7 +36,7 @@ export function zielordner(
/** Jellyfin-Konvention: <basis>/<Serie>/Season NN (pur). */ /** Jellyfin-Konvention: <basis>/<Serie>/Season NN (pur). */
export function serienOrdner(basis: string, serie: string, staffel: number): string { export function serienOrdner(basis: string, serie: string, staffel: number): string {
const name = sichererName(serie) || 'Unbekannte Serie' const name = sichererName(serie) || 'Unbekannte Serie'
return join(basis, name, `Season ${String(Math.trunc(staffel)).padStart(2, '0')}`) return join(basis, name, staffelOrdnerName(staffel))
} }
/** Ordnet Dateien Episoden zu — über Laufzeiten, ordnungserhaltend. /** Ordnet Dateien Episoden zu — über Laufzeiten, ordnungserhaltend.