diora-web/tui/diora_tui/remote.py

87 lines
2.8 KiB
Python
Raw Normal View History

"""Fetch + decrypt books from a diora server into the local TUI library.
Reading progress is intentionally NOT synced yet: the server tracks EPUB
position as a "blockIndex:innerFraction" anchor into the *web* reader's flat
paragraph numbering (see books/models.py, EBookProgress), which doesn't line
up with this TUI's per-chapter scroll_fraction (diora_tui/progress.py)
without an actual translation layer between the two block-numbering
schemes. Downloaded books simply start fresh in the local progress store;
wiring up that translation is future work.
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass, field
from pathlib import Path
import requests
from . import api, crypto
from .config import RemoteConfig
class SyncError(Exception):
pass
@dataclass
class SyncResult:
downloaded: list[str] = field(default_factory=list)
unchanged: int = 0
skipped_pdf: list[str] = field(default_factory=list)
failed: list[str] = field(default_factory=list)
def _sanitize_filename(name: str) -> str:
name = re.sub(r"[^\w\s.-]", "_", name).strip()
return (name or "buch")[:80]
def _parse_meta(raw: bytes) -> dict:
return json.loads(raw.decode("utf-8"))
def sync_library(library_dir: Path, cfg: RemoteConfig) -> SyncResult:
try:
snapshot = api.fetch_sync_snapshot(cfg.server_url, cfg.api_token)
except api.ApiError as e:
raise SyncError(str(e)) from e
except requests.RequestException as e:
raise SyncError(f"Verbindung zu {cfg.server_url} fehlgeschlagen: {e}") from e
library_dir.mkdir(parents=True, exist_ok=True)
result = SyncResult()
for book in snapshot.get("books", []):
book_id = book["id"]
try:
meta = _parse_meta(crypto.decrypt(cfg.enc_key_b64, book["meta_iv"], book["meta_ct"]))
except crypto.DecryptError:
result.failed.append(f"#{book_id}")
continue
if meta.get("type") == "pdf":
result.skipped_pdf.append(meta.get("title") or f"#{book_id}")
continue
existing = next(library_dir.glob(f"{book_id:04d} - *.epub"), None)
if existing is not None:
result.unchanged += 1
continue
try:
data = api.fetch_book_data(cfg.server_url, cfg.api_token, book_id)
raw = crypto.decrypt(cfg.enc_key_b64, data["data_iv"], data["data_ct"])
except (api.ApiError, crypto.DecryptError, requests.RequestException):
result.failed.append(meta.get("title") or f"#{book_id}")
continue
title = meta.get("title") or f"book-{book_id}"
dest = library_dir / f"{book_id:04d} - {_sanitize_filename(title)}.epub"
dest.write_bytes(raw)
result.downloaded.append(dest.name)
return result