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": "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",
|
||||||
|
|||||||
@@ -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
@@ -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`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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') {
|
} 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user