diora-web/tui/diora_tui/api.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

64 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""HTTP client for the diora server, authenticated by the account's API token
(Authorization: Bearer; the server sets request.user for every endpoint).
Only what the reader needs: the book list with the reading state (`GET /books/` – the same
rows the apps pull, without the notes' blobs that `/api/sync/` carries), a book's encrypted
file, and a position write (`POST /books/<id>/progress/`, docs/progress-sync.md).
"""
from __future__ import annotations
import time
import requests
USER_AGENT = "diora-tui"
class ApiError(Exception):
pass
def _headers(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}", "User-Agent": USER_AGENT}
def _check(resp: requests.Response, what: str) -> None:
if resp.status_code == 401:
raise ApiError("Anmeldung abgelehnt — Token ungültig oder neu erzeugt? `diora-tui login` ausführen.")
if resp.status_code == 404:
raise ApiError(f"{what} nicht gefunden (gelöscht oder anderes Konto?)")
resp.raise_for_status()
def fetch_books(server_url: str, token: str) -> tuple[list[dict], int | None, int]:
"""(books, the server's clock in ms from X-Server-Now or None, this machine's clock in ms when
the answer came). Each book: id, meta_ct, meta_iv, uploaded_at, is_read, has_cover and the
reading state (scroll_fraction, position_anchor, read_at, device, furthest_*)."""
resp = requests.get(f"{server_url}/books/", headers=_headers(token), timeout=60)
device_now = int(time.time() * 1000)
_check(resp, "Bücherliste")
try:
server_now = int(resp.headers.get("X-Server-Now", ""))
except ValueError:
server_now = None
return resp.json(), server_now, device_now
def fetch_book_data(server_url: str, token: str, book_id: int) -> dict:
resp = requests.get(f"{server_url}/books/{book_id}/data/", headers=_headers(token), timeout=300)
_check(resp, f"Buch {book_id}")
return resp.json()
def post_progress(server_url: str, token: str, book_id: int, *, anchor: str, fraction: float, read_at: int,
device: str, force: bool = False) -> dict:
"""Send a queued position. `sent_at` is the moment it leaves: the server corrects this
machine's clock error with it. The answer is the state after the write."""
body = {"scroll_fraction": fraction, "position_anchor": anchor, "read_at": read_at,
"sent_at": int(time.time() * 1000), "device": device}
if force:
body["force"] = True
resp = requests.post(f"{server_url}/books/{book_id}/progress/", headers=_headers(token), json=body, timeout=30)
_check(resp, f"Buch {book_id}")
return resp.json()