feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse
Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie veröffentlicht wurde): Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge, Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts, Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts. Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/, uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte), einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/ (ErstEinrichtung, PosterHintergrund). Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs); LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update; Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown; Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag; Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem Poster-Grid. Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung, fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit 29 Schritten inklusive Erst-Einrichtung (zweiter Start mit RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6678c81592
commit
3dc6798c79
@@ -22,11 +22,16 @@ import { fileURLToPath } from 'node:url'
|
||||
const wurzel = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const profil = join(tmpdir(), 'rippy-klick')
|
||||
const ablage = join(profil, 'ablage')
|
||||
// 5.5.0: Arbeitsordner getrennt von der Ablage — Roh und Encode liegen
|
||||
// lokal, nur das Fertige wandert (hier: von Ordner zu Ordner im Profil).
|
||||
const arbeit = join(profil, 'arbeit')
|
||||
const bilder = resolve(wurzel, '..', 'beweise', 'klick')
|
||||
|
||||
rmSync(profil, { recursive: true, force: true })
|
||||
mkdirSync(profil, { recursive: true })
|
||||
mkdirSync(bilder, { recursive: true })
|
||||
// Alte Bilder weg — sonst bleiben Nummern früherer Fassungen liegen.
|
||||
for (const name of readdirSync(bilder)) if (name.endsWith('.png')) rmSync(join(bilder, name))
|
||||
|
||||
// Das Profil vorbereiten: Einrichtung erledigt, Ablage im Profil, die
|
||||
// Werkzeuge sind die Nachbauten (kern/werkzeuge/aufruf.ts startet Skripte
|
||||
@@ -44,20 +49,29 @@ mkdirSync(bilder, { recursive: true })
|
||||
setzen.run('untertitelSprachen', 'deu')
|
||||
setzen.run('rohAufbewahrung', 'behalten')
|
||||
setzen.run('autoAuswurf', 'false')
|
||||
setzen.run('arbeitsordner', arbeit)
|
||||
// Die Automatik bleibt aus: Der Beweis klickt selbst.
|
||||
setzen.run('automatik', 'aus')
|
||||
db.close()
|
||||
}
|
||||
|
||||
const app = await electron.launch({
|
||||
const start = (env) =>
|
||||
electron.launch({
|
||||
executablePath: join(wurzel, 'node_modules', 'electron', 'dist', 'electron.exe'),
|
||||
args: [wurzel, `--profil=${profil}`, '--klick-smoke'],
|
||||
env: { ...process.env, RIPPY_FAKE_LAUFWERK: 'Q', RIPPY_OHNE_SCHLUESSEL: '1' },
|
||||
timeout: 60_000,
|
||||
})
|
||||
const seite = await app.firstWindow()
|
||||
seite.setDefaultTimeout(30_000)
|
||||
seite.on('console', (m) => {
|
||||
if (m.type() === 'error') console.log(`KLICK fenster: ${m.text().slice(0, 200)}`)
|
||||
})
|
||||
env: { ...process.env, RIPPY_FAKE_LAUFWERK: 'Q', RIPPY_OHNE_SCHLUESSEL: '1', ...env },
|
||||
timeout: 60_000,
|
||||
})
|
||||
let app = await start({})
|
||||
async function fenster() {
|
||||
const s = await app.firstWindow()
|
||||
s.setDefaultTimeout(30_000)
|
||||
s.on('console', (m) => {
|
||||
if (m.type() === 'error') console.log(`KLICK fenster: ${m.text().slice(0, 200)}`)
|
||||
})
|
||||
return s
|
||||
}
|
||||
let seite = await fenster()
|
||||
|
||||
let nr = 0
|
||||
async function bild(name) {
|
||||
@@ -87,6 +101,18 @@ await schritt('Fenster offen, nachgebautes Laufwerk Q: sichtbar', async () => {
|
||||
})
|
||||
await bild('uebersicht')
|
||||
|
||||
await schritt('Speicher-Karte misst Arbeitsordner und Ablage getrennt, die Fußleiste zeigt FREI', async () => {
|
||||
await seite.getByText('SPEICHER UND BILANZ').waitFor()
|
||||
await seite.getByText('ARBEITSORDNER (LOKAL)').waitFor()
|
||||
await seite.getByText('ABLAGE (SERVER)').waitFor()
|
||||
await seite.getByText(/^FREI [0-9]/).waitFor()
|
||||
})
|
||||
|
||||
await schritt('Keine Bibliothek mehr — Übersicht und Einstellungen sind die Bereiche', async () => {
|
||||
const n = await seite.getByRole('button', { name: 'Bibliothek' }).count()
|
||||
if (n !== 0) throw new Error('Bibliothek-Knopf ist noch da')
|
||||
})
|
||||
|
||||
await schritt('Rippen → Dialog öffnet sich', async () => {
|
||||
await seite.getByRole('button', { name: 'Rippen', exact: true }).click()
|
||||
await seite.getByText('Was soll gerippt werden?').waitFor()
|
||||
@@ -130,6 +156,10 @@ await schritt('Vorschau nennt Serien\\…\\Season 01 und S01E03', async () => {
|
||||
await dialog().getByText(/Season 01/).waitFor()
|
||||
await dialog().getByText(/S01E03/).waitFor()
|
||||
})
|
||||
|
||||
await schritt('Platz-Prüfung im Dialog: der Arbeitsordner reicht', async () => {
|
||||
await dialog().getByText(/^Platz: .* frei im Arbeitsordner/).waitFor()
|
||||
})
|
||||
await bild('dialog')
|
||||
|
||||
await schritt('Rip starten → Phase RIPPT', async () => {
|
||||
@@ -168,6 +198,26 @@ await schritt('Ergebnis liegt als S01E03/S01E04 unter Serien\\…\\Season 01', a
|
||||
console.log(`KLICK dateien: ${gefunden.join(' | ')}`)
|
||||
})
|
||||
|
||||
await schritt('Arbeitsordner: Roh liegt lokal, der Encode-Zwischenordner ist aufgeräumt', async () => {
|
||||
const roh = join(arbeit, 'roh')
|
||||
if (!existsSync(roh) || readdirSync(roh).length === 0) throw new Error(`kein Roh-Ordner unter ${roh}`)
|
||||
const fertig = join(arbeit, 'fertig')
|
||||
if (existsSync(fertig) && readdirSync(fertig).length > 0) throw new Error(`Zwischenordner nicht geräumt: ${readdirSync(fertig).join(', ')}`)
|
||||
const rohInAblage = join(ablage, 'roh')
|
||||
if (existsSync(rohInAblage)) throw new Error('Roh-Ordner liegt in der Ablage statt im Arbeitsordner')
|
||||
})
|
||||
|
||||
await schritt('Verlauf in der Übersicht nennt den Vorgang als fertig', async () => {
|
||||
await seite.getByText('VERLAUF', { exact: true }).waitFor()
|
||||
await seite.getByText(/die letzten 1 Vorgänge/).waitFor()
|
||||
await seite.getByRole('button', { name: 'Ordner öffnen' }).first().waitFor()
|
||||
})
|
||||
|
||||
await schritt('Bilanz zählt eine Disc', async () => {
|
||||
await seite.getByText('DISCS', { exact: true }).waitFor()
|
||||
await seite.getByText('1 fertig', { exact: true }).waitFor()
|
||||
})
|
||||
|
||||
await schritt('Einstellungen → Roh-Dateien zeigt den Vorgang mit Belegung', async () => {
|
||||
await seite.getByRole('button', { name: 'Einstellungen' }).click()
|
||||
await seite.locator('#roh').waitFor()
|
||||
@@ -176,6 +226,25 @@ await schritt('Einstellungen → Roh-Dateien zeigt den Vorgang mit Belegung', as
|
||||
})
|
||||
await bild('roh-dateien')
|
||||
|
||||
await schritt('Kompression: kein Allgemeines Preset, Sprachen als Chips (Deutsch, Englisch)', async () => {
|
||||
if ((await seite.getByText(/Allgemeines Preset/).count()) !== 0) throw new Error('Allgemeines Preset ist noch da')
|
||||
await seite.locator('#kompression').getByText('Deutsch', { exact: true }).first().waitFor()
|
||||
await seite.locator('#kompression').getByText('Englisch', { exact: true }).first().waitFor()
|
||||
await seite.locator('#kompression').locator('select').first().waitFor()
|
||||
})
|
||||
|
||||
await schritt('Werkzeuge haben Durchsuchen, Allgemein und Update sind eigene Karten, MakeMKV-Karte trägt den Schlüssel', async () => {
|
||||
const n = await seite.locator('#werkzeuge').getByRole('button', { name: /Durchsuchen/ }).count()
|
||||
if (n < 3) throw new Error(`nur ${n} Durchsuchen-Knöpfe`)
|
||||
await seite.locator('#allgemein').waitFor()
|
||||
await seite.locator('#update').waitFor()
|
||||
await seite.locator('#automatik').waitFor()
|
||||
await seite.locator('#benachrichtigung').waitFor()
|
||||
await seite.locator('#makemkv').getByText(/Eigener Schlüssel/).waitFor()
|
||||
await seite.getByRole('button', { name: 'Protokoll öffnen' }).waitFor()
|
||||
})
|
||||
await bild('einstellungen')
|
||||
|
||||
await schritt('Roh-Dateien löschen → „gelöscht"', async () => {
|
||||
await seite.getByRole('button', { name: 'Roh-Dateien löschen' }).first().click()
|
||||
await seite.getByText(/Roh: gelöscht/).first().waitFor()
|
||||
@@ -187,6 +256,62 @@ await schritt('Ereignisse nennen die Kompression', async () => {
|
||||
})
|
||||
await bild('ereignisse')
|
||||
|
||||
console.log('KLICK: OK — Rippen, Dialog, Sprachen, Folgen, Rip, Warteschlange, Kompression, Ablage, Roh-Dateien: alles geklickt.')
|
||||
// ── Zweiter Start: die Erst-Einrichtung (5.5.0) ─────────────────────────
|
||||
// RIPPY_START_BEREICH=einrichtung erzwingt den Assistenten (#einrichtung),
|
||||
// auch wenn setup.done schon gesetzt ist — derselbe Weg wie im Smoke.
|
||||
await app.close().catch(() => {})
|
||||
app = await start({ RIPPY_START_BEREICH: 'einrichtung' })
|
||||
seite = await fenster()
|
||||
const weiter = () => seite.getByRole('button', { name: 'Weiter', exact: true }).click()
|
||||
|
||||
await schritt('Erst-Einrichtung: Willkommen vor der Poster-Kulisse (7 Spalten Platzhalter)', async () => {
|
||||
await seite.getByText('Willkommen bei Rippy').waitFor()
|
||||
const spalten = await seite.locator('.poster-spalte').count()
|
||||
if (spalten !== 7) throw new Error(`${spalten} Poster-Spalten`)
|
||||
})
|
||||
await bild('einrichtung-willkommen')
|
||||
|
||||
await schritt('Prüfung: MakeMKV (Nachbau) gefunden, HandBrake dabei, Laufwerk Q: gesehen', async () => {
|
||||
await weiter()
|
||||
await seite.getByText(/MakeMKV gefunden/).waitFor()
|
||||
await seite.getByText(/HandBrakeCLI dabei/).waitFor()
|
||||
await seite.getByText(/1 optisches Laufwerk gefunden/).waitFor()
|
||||
})
|
||||
|
||||
await schritt('Ordner: Ablage und Arbeitsordner mit Durchsuchen und freiem Platz', async () => {
|
||||
await weiter()
|
||||
await seite.getByText('Ablage-Ordner', { exact: true }).waitFor()
|
||||
await seite.getByText('Arbeitsordner (lokal, optional)').waitFor()
|
||||
const n = await seite.getByRole('button', { name: /Durchsuchen/ }).count()
|
||||
if (n < 2) throw new Error(`nur ${n} Durchsuchen-Knöpfe`)
|
||||
await seite.getByText(/frei von/).first().waitFor()
|
||||
})
|
||||
|
||||
await schritt('Qualität: gemessene Encoder, Empfehlung je Disc-Typ, Sprach-Chips', async () => {
|
||||
await weiter()
|
||||
await seite.getByText('AUF DIESER MASCHINE GEMESSEN:').waitFor()
|
||||
await seite.getByText('4K-UHD', { exact: true }).waitFor()
|
||||
await seite.getByText('Deutsch', { exact: true }).first().waitFor()
|
||||
})
|
||||
await bild('einrichtung-qualitaet')
|
||||
|
||||
await schritt('Titel, Meldungen, Automatik: TMDb-Schlüssel, Discord/Telegram mit Testknopf, Countdown und Roh-Regel', async () => {
|
||||
await weiter()
|
||||
await seite.getByText('TMDb-API-Key', { exact: true }).waitFor()
|
||||
await weiter()
|
||||
await seite.getByText('Discord-Webhook', { exact: true }).waitFor()
|
||||
await seite.getByRole('button', { name: 'Testnachricht schicken' }).waitFor()
|
||||
await weiter()
|
||||
await seite.getByText('Countdown', { exact: true }).waitFor()
|
||||
await seite.getByText(/Roh-Dateien nach gelungener Kompression/).waitFor()
|
||||
})
|
||||
|
||||
await schritt('Fertig: der Knopf Los geht’s', async () => {
|
||||
await weiter()
|
||||
await seite.getByRole('button', { name: "Los geht's" }).waitFor()
|
||||
})
|
||||
await bild('einrichtung-fertig')
|
||||
|
||||
console.log('KLICK: OK — Speicher, Rippen, Dialog, Sprachen, Folgen, Platz, Rip, Warteschlange, Kompression, Arbeitsordner, Ablage, Verlauf, Bilanz, Roh-Dateien, Einstellungen, Erst-Einrichtung: alles geklickt.')
|
||||
await app.close().catch(() => {})
|
||||
process.exit(0)
|
||||
|
||||
@@ -1,3 +1,57 @@
|
||||
Version 5.5.0
|
||||
|
||||
Uebersicht und Einstellungen neu sortiert.
|
||||
Die Bibliothek ist weg (Jellyfin IST die Bibliothek). Dafuer zeigt die
|
||||
Uebersicht den Verlauf der letzten Vorgaenge, den freien Platz auf
|
||||
Arbeitsordner und Ablage samt Bilanz (Discs, gesicherte GB, Dauer je Disc)
|
||||
und eine Fussleiste mit Kern, Datenbank, Werkzeugen, Schluessel, freiem
|
||||
Platz und Update-Stand. Der MakeMKV-Kopf ist in die Einstellungen gezogen.
|
||||
Dort gibt es eigene Karten fuer Automatik, Benachrichtigung, Allgemein und
|
||||
Update; "Allgemeines Preset" ist weg, Ton- und Untertitelsprachen sind
|
||||
Auswahl-Chips mit Dropdown in Wunschreihenfolge; Werkzeuge haben
|
||||
"Durchsuchen ...".
|
||||
|
||||
Arbeitsordner lokal, Ablage auf dem Server.
|
||||
Roh-Dateien und Kompression liegen im Arbeitsordner (lokale Platte), nur
|
||||
das fertige Ergebnis wandert in die Ablage. Vor jedem Rip prueft Rippy den
|
||||
Platz: zu wenig fuer den verlustfreien Rip heisst kein Start, knapp heisst
|
||||
Hinweis.
|
||||
|
||||
Automatik mit Countdown.
|
||||
Ist Rippy sicher (ab 90 %) und die Disc noch nicht gerippt, liest es die
|
||||
Titel und startet nach 30 s (einstellbar) mit seinem Vorschlag - mit
|
||||
"Stopp" und "Jetzt" in der Kachel. Bei Unsicherheit wartet es und sagt
|
||||
warum.
|
||||
|
||||
Serien-Merker ueber Discs hinweg.
|
||||
Disc 1 endet mit Folge 4, Disc 2 zaehlt ab 5 weiter - der Dialog schlaegt
|
||||
es vor, aenderbar.
|
||||
|
||||
Rettungs-Abbild fuer defekte Discs.
|
||||
Hat Rippy Lesefehler gesehen (Defekt-Waechter, Lesefehler-Meldung), bietet
|
||||
die Kachel "Rettungs-Abbild" an: Rippys eigener Sektor-Leser liest die Disc
|
||||
Stueck fuer Stueck, wiederholt in immer kleineren Stuecken und fuellt nur
|
||||
das wirklich Unlesbare mit Nullen; danach laeuft derselbe Rip aus dem
|
||||
Abbild, an dem MakeMKV nicht mehr haengen bleibt. Ein Bericht daneben
|
||||
nennt jede genullte Stelle. Bei einer heilen Disc passiert das nie.
|
||||
|
||||
Warteschlange pausieren, Benachrichtigung, Protokoll.
|
||||
"Pausieren" laesst den laufenden Film fertig rechnen und haelt die Schlange
|
||||
an. Discord-Webhook oder Telegram-Bot melden fertige Rips, Kompressionen
|
||||
und Fehler aufs Handy (Testknopf). Haupt und Kern schreiben eine
|
||||
Protokoll-Datei je Tag (14 Tage), "Protokoll oeffnen" unter Allgemein;
|
||||
Ordner oeffnen sich aus Verlauf, Roh-Dateien und Speicher-Karte im Explorer.
|
||||
|
||||
Erst-Einrichtung mit Poster-Kulisse.
|
||||
Acht Schritte mit denselben Bausteinen wie die Einstellungen (Ordner,
|
||||
Qualitaet mit Sprachen, Titel, Meldungen, Automatik) vor einem langsam
|
||||
laufenden Poster-Raster - Platzhalter bis zum TMDb-Schluessel, danach
|
||||
echte Poster.
|
||||
|
||||
Dazu: Das Fenster oeffnet mit 80 % des Bildschirms, mittig, mindestens
|
||||
1100 x 720, und merkt sich Lage und Groesse (nur, wenn sie noch auf einem
|
||||
Bildschirm liegt). Oberflaeche und Kern sind in Bereiche aufgeteilt.
|
||||
|
||||
Version 5.4.0
|
||||
|
||||
Das Laufwerk ist frei, sobald die Roh-Dateien liegen.
|
||||
|
||||
Reference in New Issue
Block a user