diora-web/tui/diora_tui/cache.py
mrwnslz 51b7316398 TUI: Lesestand-Vertrag (zuletzt gelesen gewinnt), Login wie diora_client, Bücher beim Öffnen laden
progress.py setzt docs/progress-sync.md um (read_at in Serverzeit mit Uhrversatz, sent_at beim
Senden, force, Warteschlange je Buch, Neuanwendung nach dem Abgleich; dieselben Regeln wie
Ops.kt/ops.js). Der Reader speichert nur, was der Leser bewegt hat; wiederherstellen,
Umbruch und Sprünge schreiben nichts. Neuere Stellen anderer Geräte werden angeboten
("Auf X weitergelesen – springen?"), nie von allein angewendet; "w" springt zur weitesten
Stelle. Fortschritt als Textanteil in Zeichen statt 0.0.

Abgleich über GET /books/ (ohne Markierungs-Blobs), Bücher erst beim Öffnen geladen
(☁ in der Liste). `diora-tui login` meldet wie die Apps an, liest den vorhandenen Token,
leitet den Schlüssel aus dem Passwort ab und speichert beides geschützt (DPAPI).
DIORA_TUI_HOME legt alle Dateien in einen Ordner (platformdirs folgt unter Windows den
Umgebungsvariablen nicht). 35 Tests, tools/smoke_server.py gegen einen lokalen Server.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:08:59 +01:00

52 lines
1.9 KiB
Python

"""On-disk cache for the (FlatBook, BookLayout) pair a book open computes —
extraction + row-layout for very large books (tens of thousands of blocks)
can take several seconds each; reopening the same book at the same terminal
width should be near-instant instead of paying that cost again every time.
Not a correctness-critical cache: any miss (new book, different width, edited
file) just falls back to recomputing from scratch, so a stale/corrupt cache
entry is handled by overwriting it, never by crashing the reader.
"""
from __future__ import annotations
import hashlib
import pickle
from pathlib import Path
from . import paths
from .blocks import FlatBook
from .layout import BookLayout
_CACHE_VERSION = 2 # 2: blocks carry char_start/char_len
_CACHE_DIR = paths.cache_dir() / "layout_cache"
def _cache_key(path: Path, width: int) -> str:
stat = path.stat()
raw = f"v{_CACHE_VERSION}|{path.resolve()}|{stat.st_size}|{stat.st_mtime_ns}|{width}"
return hashlib.sha256(raw.encode()).hexdigest()[:32]
def load(path: Path, width: int) -> tuple[FlatBook, BookLayout] | None:
cache_file = _CACHE_DIR / f"{_cache_key(path, width)}.pickle"
if not cache_file.exists():
return None
try:
with cache_file.open("rb") as f:
book, book_layout = pickle.load(f)
if not isinstance(book, FlatBook) or not isinstance(book_layout, BookLayout):
return None
return book, book_layout
except Exception:
return None
def save(path: Path, width: int, book: FlatBook, book_layout: BookLayout) -> None:
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
cache_file = _CACHE_DIR / f"{_cache_key(path, width)}.pickle"
try:
with cache_file.open("wb") as f:
pickle.dump((book, book_layout), f, protocol=pickle.HIGHEST_PROTOCOL)
except Exception:
pass # best-effort — a failed cache write shouldn't break reading