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:
co-authored by
Claude Fable 5.1
parent
3f7f962dff
commit
e3ce8220f1
Generated
+48
@@ -24,6 +24,7 @@
|
||||
"electron": "44.0.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-vite": "^5.0.0",
|
||||
"playwright": "^1.62.1",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"tailwindcss": "^4.3.3",
|
||||
@@ -5612,6 +5613,53 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"electron": "44.0.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"electron-vite": "^5.0.0",
|
||||
"playwright": "^1.62.1",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"tailwindcss": "^4.3.3",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,12 +11,16 @@ import {
|
||||
type BeschaffungStand,
|
||||
type BibliothekEintrag,
|
||||
type DiscInfoStand,
|
||||
type FensterNachricht,
|
||||
type GeraeteInfo,
|
||||
type KernNachricht,
|
||||
type LaufwerkBericht,
|
||||
type MetaVorschlag,
|
||||
type TitelStand,
|
||||
type RipAuftragOptionen,
|
||||
type RipAuftragTitel,
|
||||
type RipStatus,
|
||||
type TitelStand,
|
||||
type VorgaengeStand,
|
||||
type WerkzeugAuskunft,
|
||||
} from '../gemeinsam/nachrichten'
|
||||
|
||||
@@ -54,13 +58,18 @@ export interface Verbindung {
|
||||
} | null
|
||||
/** Stand der MakeMKV-Beschaffung (§ 9) — null, solange nie eine lief. */
|
||||
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. */
|
||||
meldungen: string[]
|
||||
}
|
||||
|
||||
type Horcher = (stand: Verbindung) => void
|
||||
|
||||
const MAX_MELDUNGEN = 20
|
||||
const MAX_MELDUNGEN = 30
|
||||
|
||||
const stand: Verbindung = {
|
||||
verbunden: false,
|
||||
@@ -78,27 +87,32 @@ const stand: Verbindung = {
|
||||
bibliothek: [],
|
||||
schluessel: null,
|
||||
beschaffung: null,
|
||||
vorgaenge: null,
|
||||
gesundheit: {},
|
||||
meldungen: [],
|
||||
}
|
||||
const horcher = new Set<Horcher>()
|
||||
let gestartet = false
|
||||
let port: MessagePort | null = null
|
||||
|
||||
function melden(): void {
|
||||
for (const h of horcher) {
|
||||
h({
|
||||
...stand,
|
||||
laufwerke: [...stand.laufwerke],
|
||||
rips: { ...stand.rips },
|
||||
discInfos: { ...stand.discInfos },
|
||||
vorschlaege: { ...stand.vorschlaege },
|
||||
einstellungen: { ...stand.einstellungen },
|
||||
bibliothek: [...stand.bibliothek],
|
||||
meldungen: [...stand.meldungen],
|
||||
})
|
||||
function kopie(): Verbindung {
|
||||
return {
|
||||
...stand,
|
||||
laufwerke: [...stand.laufwerke],
|
||||
rips: { ...stand.rips },
|
||||
discInfos: { ...stand.discInfos },
|
||||
vorschlaege: { ...stand.vorschlaege },
|
||||
einstellungen: { ...stand.einstellungen },
|
||||
bibliothek: [...stand.bibliothek],
|
||||
gesundheit: { ...stand.gesundheit },
|
||||
meldungen: [...stand.meldungen],
|
||||
}
|
||||
}
|
||||
|
||||
function melden(): void {
|
||||
for (const h of horcher) h(kopie())
|
||||
}
|
||||
|
||||
function meldung(text: string): void {
|
||||
stand.meldungen = [text, ...stand.meldungen].slice(0, MAX_MELDUNGEN)
|
||||
}
|
||||
@@ -116,6 +130,9 @@ function kernNachricht(nachricht: KernNachricht): void {
|
||||
case 'kern-fehler':
|
||||
meldung(`⚠ ${nachricht.text}`)
|
||||
break
|
||||
case 'kern-hinweis':
|
||||
meldung(nachricht.text)
|
||||
break
|
||||
case 'laufwerke':
|
||||
stand.laufwerke = nachricht.liste
|
||||
for (const ereignis of nachricht.ereignisse) {
|
||||
@@ -137,11 +154,20 @@ function kernNachricht(nachricht: KernNachricht): void {
|
||||
break
|
||||
}
|
||||
case 'disc-info': {
|
||||
const vorher = stand.discInfos[nachricht.info.id]
|
||||
stand.discInfos = { ...stand.discInfos, [nachricht.info.id]: nachricht.info }
|
||||
if (nachricht.info.titel.length > 0) {
|
||||
meldung(
|
||||
`Laufwerk ${nachricht.info.id}: erkannt als „${nachricht.info.titel}${nachricht.info.jahr !== null ? ` (${nachricht.info.jahr})` : ''}“ · ${Math.round(nachricht.info.confidence * 100)} %`,
|
||||
)
|
||||
// Dieselbe Erkennung nicht zweimal melden — die Nachprüfung nach
|
||||
// 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
|
||||
}
|
||||
@@ -169,6 +195,18 @@ function kernNachricht(nachricht: KernNachricht): void {
|
||||
stand.beschaffung = nachricht.stand
|
||||
if (nachricht.stand.fehler) meldung(`⚠ MakeMKV-Update: ${nachricht.stand.text}`)
|
||||
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': {
|
||||
stand.rips = { ...stand.rips, [nachricht.status.id]: nachricht.status }
|
||||
const laeuft = ['liest-info', 'rippt', 'komprimiert', 'legt-ab'].includes(nachricht.status.phase)
|
||||
@@ -201,6 +239,9 @@ function kernNachricht(nachricht: KernNachricht): void {
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'installer-starten':
|
||||
// Nur fürs Haupt gedacht — im Fenster nichts zu tun.
|
||||
break
|
||||
}
|
||||
melden()
|
||||
}
|
||||
@@ -213,10 +254,11 @@ function portAnnehmen(neuerPort: MessagePort): void {
|
||||
if (istKernNachricht(nachricht)) kernNachricht(nachricht)
|
||||
}
|
||||
neuerPort.start()
|
||||
neuerPort.postMessage({ art: 'ping', zeit: Date.now() })
|
||||
neuerPort.postMessage({ art: 'einstellungen-laden' })
|
||||
neuerPort.postMessage({ art: 'werkzeuge-laden' })
|
||||
neuerPort.postMessage({ art: 'bibliothek-laden' })
|
||||
senden({ art: 'ping', zeit: Date.now() })
|
||||
senden({ art: 'einstellungen-laden' })
|
||||
senden({ art: 'werkzeuge-laden' })
|
||||
senden({ art: 'bibliothek-laden' })
|
||||
senden({ art: 'vorgaenge-laden' })
|
||||
melden()
|
||||
}
|
||||
|
||||
@@ -241,20 +283,16 @@ export function verbindungStarten(): void {
|
||||
|
||||
export function aufVerbindung(h: Horcher): () => void {
|
||||
horcher.add(h)
|
||||
h({
|
||||
...stand,
|
||||
laufwerke: [...stand.laufwerke],
|
||||
rips: { ...stand.rips },
|
||||
discInfos: { ...stand.discInfos },
|
||||
vorschlaege: { ...stand.vorschlaege },
|
||||
einstellungen: { ...stand.einstellungen },
|
||||
bibliothek: [...stand.bibliothek],
|
||||
meldungen: [...stand.meldungen],
|
||||
})
|
||||
h(kopie())
|
||||
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) {
|
||||
meldung(`⚠ ${wasWar}: keine Verbindung zum Kern`)
|
||||
melden()
|
||||
@@ -273,8 +311,8 @@ export function titelLesen(id: string): void {
|
||||
}
|
||||
|
||||
/** `auftrag` leer heißt: alles rippen (wie vor 5.2). */
|
||||
export function ripStarten(id: string, auftrag: RipAuftragTitel[] = []): void {
|
||||
anKern({ art: 'rip-start', id, auftrag }, `Rip ${id}`)
|
||||
export function ripStarten(id: string, auftrag: RipAuftragTitel[] = [], optionen: RipAuftragOptionen = {}): void {
|
||||
anKern({ art: 'rip-start', id, auftrag, optionen }, `Rip ${id}`)
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
if (port === null) return
|
||||
port.postMessage({ art: 'metadaten-suchen', id, text })
|
||||
senden({ art: 'metadaten-suchen', id, text })
|
||||
}
|
||||
|
||||
export function zuordnungSetzen(id: string, vorschlag: MetaVorschlag): void {
|
||||
if (port === null) return
|
||||
port.postMessage({ art: 'zuordnung-setzen', id, vorschlag })
|
||||
senden({ art: 'zuordnung-setzen', id, vorschlag })
|
||||
}
|
||||
|
||||
export function makemkvUpdate(): void {
|
||||
if (port === null) {
|
||||
meldung('⚠ MakeMKV-Update: keine Verbindung zum Kern')
|
||||
melden()
|
||||
return
|
||||
}
|
||||
port.postMessage({ art: 'makemkv-update' })
|
||||
anKern({ art: 'makemkv-update' }, 'MakeMKV-Update')
|
||||
}
|
||||
|
||||
export function schluesselPruefen(): void {
|
||||
if (port === null) return
|
||||
port.postMessage({ art: 'schluessel-pruefen' })
|
||||
senden({ art: 'schluessel-pruefen' })
|
||||
}
|
||||
|
||||
export function einstellungSetzen(schluessel: string, wert: string): void {
|
||||
if (port === null) {
|
||||
meldung(`⚠ Einstellung ${schluessel}: keine Verbindung zum Kern`)
|
||||
melden()
|
||||
return
|
||||
}
|
||||
port.postMessage({ art: 'einstellung-setzen', schluessel, wert })
|
||||
anKern({ art: 'einstellung-setzen', schluessel, wert }, `Einstellung ${schluessel}`)
|
||||
}
|
||||
|
||||
// ── 5.4.0: Vorgänge, Roh-Dateien, Warteschlange, Laufwerks-Gesundheit ──
|
||||
export function vorgaengeLaden(): void {
|
||||
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`)
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
@@ -94,6 +94,12 @@ function start(): void {
|
||||
}
|
||||
} else if (nachricht.art === 'installer-starten') {
|
||||
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) {
|
||||
// Windows-Toast beim Erkennen (§ 7) — Klick holt das Fenster hoch.
|
||||
toast(
|
||||
@@ -109,6 +115,43 @@ function start(): void {
|
||||
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
|
||||
// Rip-Status — die Information, die man will, ohne das Fenster zu öffnen.
|
||||
function ripStatusVerarbeiten(status: Extract<KernNachricht, { art: 'rip-status' }>['status']): void {
|
||||
@@ -117,25 +160,11 @@ function start(): void {
|
||||
} else {
|
||||
aktiveVorgaenge.delete(status.id)
|
||||
}
|
||||
if (fenster !== null && !fenster.isDestroyed()) {
|
||||
if ((status.phase === 'rippt' || status.phase === 'komprimiert') && status.prozent >= 0) {
|
||||
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('')
|
||||
}
|
||||
letzterRip = status
|
||||
anzeigeAktualisieren()
|
||||
if (smokeModus) return
|
||||
if (status.phase === 'fertig') {
|
||||
toast('Rippy ist fertig', status.text)
|
||||
toast('Rippy: Rip fertig', status.text)
|
||||
} else if (status.phase === 'fehler') {
|
||||
toast('Rippy: Fehler', status.text)
|
||||
}
|
||||
|
||||
@@ -11,23 +11,11 @@ import { existsSync, readdirSync, renameSync } from 'node:fs'
|
||||
// müssen dort physisch funktionieren. Feste Windows-Muster (Katalog,
|
||||
// %ProgramFiles%) bleiben dagegen bei path.win32.
|
||||
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.
|
||||
// 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 { sichererName }
|
||||
|
||||
/** Zielordner: sprechender Name; existiert er schon (Disc doppelt
|
||||
* gerippt), wird die Job-Kennung angehängt statt zu überschreiben. */
|
||||
@@ -48,7 +36,7 @@ export function zielordner(
|
||||
/** Jellyfin-Konvention: <basis>/<Serie>/Season NN (pur). */
|
||||
export function serienOrdner(basis: string, serie: string, staffel: number): string {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user