library.py hält die Regeln der Web-Bibliothek (53-library.js) als reine Funktionen: Status Angefangen/Ungelesen/Gelesen, Ordner (mehrere je Buch, Reihenfolge aus den verschlüsselten Bibliothekseinstellungen, "Ohne Ordner"), Suche ohne Akzente, sechs Sortierungen mit natürlicher Zahlenordnung, Weiterlesen-Karte aus den zuletzt gelesenen, Bücher/Zeitschriften. library_screen.py zeigt sie an (Tasten 1-4, o, /, s, m, c; Chips und Karte klickbar). Metadaten (Ordner, Seitenzahl, Zeitschrift) und die Ordnerreihenfolge kommen beim Abgleich; fehlt die Länge, rechnet die TUI sie für geladene Bücher aus dem Text (1800 Zeichen je Seite). crypto.encrypt für Stufe 3. 54 Tests, Smoke prüft Ordner/Länge/Prefs gegen den lokalen Server. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
160 lines
8.6 KiB
Markdown
160 lines
8.6 KiB
Markdown
# diora-tui
|
||
|
||
EPUB-Reader im Terminal — die erste Stufe einer TUI-Version von diora.
|
||
|
||
Zeigt ein Buch als **eine durchgehende Ansicht** über alle Kapitel hinweg (wie diora's
|
||
Web-Reader), nicht Kapitel für Kapitel. Lesefortschritt wird als dieselbe
|
||
`"blockIndex:innerFraction"`-Positionsangabe geführt wie der Web-Reader (`books/models.py`,
|
||
`EBookProgress`) — `blockIndex` zählt dabei exakt wie `static/js/app.js`s
|
||
`EPUB_BLOCK_SELECTOR` (`p, h1-h6, li, blockquote, dt, dd, figcaption` + textbasierte
|
||
`div`s ohne Element-Kinder), fortlaufend über das ganze Buch. Dadurch ist die Position
|
||
zwischen TUI und Web-Reader direkt vergleichbar, und `diora-tui sync` kann Fortschritt in
|
||
beide Richtungen synchronisieren (siehe unten).
|
||
|
||
Die Lesestelle folgt dem Vertrag aus `docs/progress-sync.md`: **die zuletzt gelesene Stelle
|
||
gewinnt** (auf welchem Gerät auch immer), die weiteste bleibt als Hinweis erhalten. Die
|
||
Regeln stehen in `progress.py` und sind dieselben wie auf dem Server und in den Apps.
|
||
|
||
## Setup
|
||
|
||
```bash
|
||
cd tui
|
||
pip install -e .
|
||
```
|
||
|
||
## Nutzung
|
||
|
||
```bash
|
||
diora-tui --library ~/Books # Standard: ~/Books
|
||
```
|
||
|
||
Tastenkürzel:
|
||
|
||
- `↑`/`k`, `↓`/`j` — zeilenweise scrollen
|
||
- `n` — nächstes Kapitel, `p` — vorheriges Kapitel
|
||
- `w` — zur weitesten Stelle des Buchs springen (auf Wunsch; beim Öffnen weist ein Hinweis darauf hin)
|
||
- `f` — Fußnote in der Nähe der aktuellen Position anzeigen (Peek-Overlay, `Escape`/`f`/`q`
|
||
zum Schließen); Erkennung folgt derselben Heuristik wie `app.js`s
|
||
`_looksLikeFootnoteLink` (Link in/um `<sup>`, Klassenname mit note/footnote/fn, oder
|
||
`epub:type="noteref"`)
|
||
- Bibliothek: `Enter` öffnet das markierte Buch, `c` das Buch der „Weiterlesen“-Karte, `1`–`4`
|
||
Status (Alle · Angefangen · Ungelesen · Gelesen), `o` Ordner, `/` Suche, `s` Sortierung,
|
||
`m` Bücher ⇄ Zeitschriften, `Escape` setzt Suche und Filter zurück. Chips und Karte lassen
|
||
sich auch anklicken.
|
||
- `Escape` / `q` — zurück zur Bibliothek (im Reader) bzw. beenden (in der Bibliothek)
|
||
|
||
Der Fließtext ist auf 120 Zeichen Breite begrenzt und horizontal zentriert (lesbarer als
|
||
volle Terminalbreite bei breiten Fenstern).
|
||
|
||
Unten rechts zeigt eine Statusleiste Akkustand (falls vorhanden, via `psutil`) und Uhrzeit,
|
||
sekündlich aktualisiert, auf Bibliotheks- und Reader-Ansicht.
|
||
|
||
## Die Bibliothek
|
||
|
||
Wie die Bibliothek im Web (`docs/library.md`), nur im Terminal:
|
||
|
||
- **Weiterlesen-Karte** oben (Titel, Autor, Balken, „Seite 132 von 186“, wann und auf welchem
|
||
Gerät zuletzt gelesen) und darunter die Bücher, die davor gelesen wurden. Das Buch der Karte
|
||
steht nicht noch einmal in der Liste. Bei Suche und Filtern entfällt die Karte.
|
||
- **Chips:** Alle · Angefangen · Ungelesen · Gelesen (mit Anzahl), dann die Ordner in der
|
||
Reihenfolge, die im Web eingestellt ist (verschlüsselte Bibliothekseinstellungen), und
|
||
„Ohne Ordner“. Ein Buch kann in mehreren Ordnern stehen.
|
||
- **Suche** in Titel und Autor, ohne Beachtung von Groß-/Kleinschreibung und Akzenten.
|
||
- **Sortierung:** Zuletzt · Titel · Autor · Neu hinzugefügt · Kürzeste/Längste zuerst (Zahlen
|
||
nach Wert: „Band 2“ vor „Band 10“). Sie bleibt gespeichert.
|
||
- **Länge:** die Seitenzahl aus den Metadaten (`pages`); fehlt sie, rechnet die TUI sie für
|
||
schon geladene Bücher einmal aus dem Text aus (1800 Zeichen je Seite, wie der Web-Reader), wenn
|
||
nach Länge sortiert wird („Länge wird ermittelt …“). Nicht geladene Bücher ohne Länge stehen am
|
||
Ende. Zurück auf den Server schreibt die TUI die Länge nicht; das macht der Web-Reader.
|
||
- **Bücher ⇄ Zeitschriften** (`m`): Zeitschriften sind, was in den Metadaten so markiert ist
|
||
oder im Ordner „Zeitschriften“/„Magazine“ liegt. Die Heft-Ansicht mit Artikeln gibt es in der
|
||
TUI nicht; ein Heft ist ein Buch.
|
||
- ☁ vor dem Titel: auf dem Server, noch nicht geladen. ✓: gelesen.
|
||
|
||
Nicht in der TUI: Serien-Stapel, Ausgaben (mehrere Fassungen eines Werks stehen einzeln),
|
||
Cover, Ordner anlegen/zuweisen, Eingang aus der Cloud.
|
||
|
||
## Anmelden und synchronisieren
|
||
|
||
```bash
|
||
diora-tui login # einmal; fragt Benutzername und Passwort
|
||
diora-tui # die Oberfläche, synct beim Start und beim Beenden
|
||
diora-tui sync # nur abgleichen, ohne Oberfläche
|
||
diora-tui logout # gespeicherte Zugangsdaten löschen
|
||
```
|
||
|
||
`login` meldet sich wie die Apps über das Web-Formular an, liest den vorhandenen API-Token
|
||
von der Einstellungsseite (es wird nie ein neuer erzeugt: das würde die Apps abmelden) und
|
||
leitet den Buchschlüssel aus dem Passwort ab (PBKDF2, wie „Mit Passwort entsperren“ im Web).
|
||
Das Passwort wird nicht gespeichert. Passt der abgeleitete Schlüssel nicht (Passwort seit dem
|
||
Hochladen geändert, Schlüssel zufällig erzeugt), fragt `login` nach dem Schlüssel
|
||
(diora: Einstellungen → Schlüssel kopieren; `--paste-key` fragt gleich danach).
|
||
|
||
Token und Schlüssel liegen geschützt in `config.json`: unter Windows per DPAPI (nur dieser
|
||
Windows-Benutzer auf diesem Rechner kann sie lesen), sonst als Base64 in einer 0600-Datei.
|
||
Eine ältere Konfiguration mit Klartextfeldern wird beim ersten Lesen geschützt neu geschrieben.
|
||
Alle Dateien liegen unter `platformdirs`' Verzeichnissen; `DIORA_TUI_HOME=<Ordner>` legt sie
|
||
stattdessen gemeinsam in einen Ordner (zweites Konto, Tests).
|
||
|
||
**Was synchronisiert wird:** die Bücherliste (`GET /books/`, ohne die Blobs der Markierungen)
|
||
samt Lesestand, Gelesen-Status und der Uhr des Servers. Die Bücher selbst werden erst beim
|
||
Öffnen geladen und entschlüsselt (`<id> - <Titel>.epub` im Bibliotheksordner). PDFs werden
|
||
übersprungen.
|
||
|
||
**Lesestand** (`progress.py`, `reader_screen.py`):
|
||
|
||
- Gespeichert wird nur, was der Leser selbst bewegt hat (Tasten, Mausrad), mit dem Zeitpunkt
|
||
des Ankommens (`read_at`). Wiederherstellen, Umbruch bei Größenänderung und Sprünge auf
|
||
Angebote ändern nichts. Ein offenes, ruhendes Buch kann deshalb nie eine neuere Stelle von
|
||
woanders überschreiben.
|
||
- Zeiten sind Serverzeit: die TUI lernt aus jeder Antwort den Versatz ihrer Uhr, und der
|
||
Server korrigiert mit `sent_at` nach. Ein falsch gehender Rechner ordnet trotzdem richtig.
|
||
- Jede Stelle wird lokal angewendet und vorgemerkt (`pending`, je Buch die später gelesene),
|
||
bis der Server sie hat, auch offline; nach einem Abgleich wird sie nach derselben Regel auf
|
||
den Stand des Servers gelegt.
|
||
- Alle zwei Minuten fragt ein offenes Buch den Server. Liegt dort eine neuere Stelle von einem
|
||
anderen Gerät, fragt die TUI „Auf *X* weitergelesen — bei n %. Springen?“ (`y`/`n`), und
|
||
bewegt sich nie von allein.
|
||
- Der Fortschritt in % ist wie im Web der Anteil am Buchtext in Zeichen
|
||
(`blocks.fraction_for_anchor`), nicht mehr 0.
|
||
|
||
## Performance bei großen Büchern
|
||
|
||
Sehr große Bücher (mehrstellige Tausend Absätze — z.B. Sammelbände) können beim
|
||
*ersten* Öffnen mehrere Sekunden bis niedrige zweistellige Sekunden brauchen (Parsing +
|
||
Zeilenumbruch-Berechnung für die durchgehende Ansicht). Ein zweites Öffnen desselben
|
||
Buchs bei gleicher Terminalbreite ist dank Cache (`~/.cache/diora-tui/layout_cache/`)
|
||
deutlich schneller. Das Rendering selbst skaliert nicht mit der Buchgröße — nur die
|
||
tatsächlich sichtbaren Zeilen werden gezeichnet (Textual Line API), nicht das ganze Buch
|
||
auf einmal.
|
||
|
||
## Tests und Anker-Parität mit dem Web-Reader
|
||
|
||
```bash
|
||
pip install -e . pytest
|
||
pytest tests # Blockliste, Anker, App-Start (Textual-Pilot)
|
||
```
|
||
|
||
Die Position ist ein Block-Index, den alle Clients gleich zählen müssen. Ob die TUI das tut,
|
||
prüft `tools/parity/` gegen den echten Web-Code (`parseEpub` aus `static/js/app/40-epub.js`
|
||
in headless Chromium):
|
||
|
||
```bash
|
||
NODE_PATH=<diora-web>/node_modules PLAYWRIGHT_BROWSERS_PATH=... node tools/parity/web_blocks.cjs <diora-web> web.json buch1.epub buch2.epub
|
||
python tools/parity/compare_blocks.py web.json
|
||
```
|
||
|
||
Stand 2026-10-26 (6 Bücher, bis 25.798 Blöcke): Anzahl, Tags, Text, Zeichenposition und
|
||
-länge aller Blöcke sowie die Gesamtlänge sind identisch. Das setzt voraus, dass die TUI den
|
||
EPUB-Inhalt wie der Browser liest: UTF-8-dekodiert, mit dem HTML5-Parser (`html5lib`, nicht
|
||
libxml2) und aus den Rohdaten (`item.content`; `get_content()` von ebooklib formatiert um und
|
||
fügt Leerraum ein).
|
||
|
||
## Grenzen der aktuellen Version
|
||
|
||
- Nur EPUB, kein PDF (der Abgleich überspringt PDFs und meldet sie).
|
||
- Text wird als Fließtext ohne Bild-/Layout-Rendering dargestellt.
|
||
- `innerFraction` (die Position *innerhalb* eines Blocks) ist eine Terminal-Näherung
|
||
(zeilenbasiert statt pixelbasiert wie im Browser) — für die Fortschritts-Sortierung
|
||
zählt primär `blockIndex`, der exakt mit dem Web-Reader übereinstimmt; `innerFraction`
|
||
ist nur ein Tiebreaker innerhalb desselben Blocks.
|