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>
51 lines
1.5 KiB
Python
51 lines
1.5 KiB
Python
"""What the TUI knows about the server's books beyond the files: title, author, read status
|
|
and which books exist at all (so a book can be listed before it is downloaded). Set only by
|
|
`sync`; the reader's position is progress.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from . import paths
|
|
|
|
_DATA_DIR = paths.data_dir()
|
|
_META_FILE = _DATA_DIR / "library.json"
|
|
|
|
|
|
def _load_all() -> dict[str, dict]:
|
|
try:
|
|
data = json.loads(_META_FILE.read_text())
|
|
except (OSError, ValueError):
|
|
return {}
|
|
return data if isinstance(data, dict) else {}
|
|
|
|
|
|
def _save_all(data: dict[str, dict]) -> None:
|
|
_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
tmp = _META_FILE.with_suffix(".tmp")
|
|
tmp.write_text(json.dumps(data, indent=1))
|
|
os.replace(tmp, _META_FILE)
|
|
|
|
|
|
def is_read(book_key: str) -> bool:
|
|
entry = _load_all().get(book_key)
|
|
return bool(entry and entry.get("is_read"))
|
|
|
|
|
|
def get(book_key: str) -> dict | None:
|
|
return _load_all().get(book_key)
|
|
|
|
|
|
def remote_entries() -> dict[str, dict]:
|
|
"""Every server book: key -> {server_id, title, author, is_read, uploaded_at}."""
|
|
return {k: v for k, v in _load_all().items() if isinstance(v, dict) and v.get("server_id") is not None}
|
|
|
|
|
|
def set_remote(entries: dict[str, dict]) -> None:
|
|
"""Replace the server books with `entries` (a book deleted on the server drops out)."""
|
|
data = {k: v for k, v in _load_all().items() if not (isinstance(v, dict) and v.get("server_id") is not None)}
|
|
data.update(entries)
|
|
_save_all(data)
|