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>
103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
"""diora-tui: terminal EPUB reader for diora, syncing books + reading position
|
|
against a diora server (see project README)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
from textual.app import App
|
|
|
|
from . import config as config_mod
|
|
from . import login as login_mod, remote
|
|
from .library_screen import LibraryScreen
|
|
|
|
DEFAULT_LIBRARY = Path.home() / "Books"
|
|
|
|
|
|
class DioraTuiApp(App):
|
|
CSS_PATH = "app.tcss"
|
|
TITLE = "diora-tui"
|
|
|
|
def __init__(self, library_dir: Path) -> None:
|
|
super().__init__()
|
|
self.library_dir = library_dir
|
|
|
|
def on_mount(self) -> None:
|
|
self.push_screen(LibraryScreen(self.library_dir))
|
|
|
|
async def action_quit(self) -> None:
|
|
cfg = config_mod.load()
|
|
if cfg is not None:
|
|
try:
|
|
# Sends what is queued, then pulls: nothing read stays only here.
|
|
await asyncio.to_thread(remote.sync_library, self.library_dir, cfg)
|
|
except remote.SyncError:
|
|
pass # best-effort — don't block quitting on a sync failure
|
|
self.exit()
|
|
|
|
|
|
def run_login(server: str | None, *, paste_key: bool) -> None:
|
|
try:
|
|
cfg = login_mod.login(server, paste_key=paste_key)
|
|
except login_mod.LoginError as e:
|
|
raise SystemExit(f"Fehler: {e}")
|
|
config_mod.save(cfg)
|
|
print("Gespeichert (Token und Schlüssel geschützt). Weiter mit diora-tui oder diora-tui sync.")
|
|
|
|
|
|
def run_sync(library_dir: Path) -> None:
|
|
cfg = config_mod.load()
|
|
if cfg is None:
|
|
raise SystemExit("Nicht angemeldet: erst diora-tui login")
|
|
print(f"Verbinde zu {cfg.server_url} …")
|
|
try:
|
|
result = remote.sync_library(library_dir, cfg)
|
|
except remote.SyncError as e:
|
|
raise SystemExit(f"Fehler: {e}")
|
|
|
|
print(f"{result.books} EPUB(s) auf dem Server (geladen wird beim Öffnen)." )
|
|
if result.pushed:
|
|
print(f"Lesestand von {result.pushed} Buch/Bücher an den Server geschickt.")
|
|
for title in result.new_titles:
|
|
print(f" + {title}")
|
|
if result.skipped_pdf:
|
|
print(f"{len(result.skipped_pdf)} PDF(s) übersprungen (TUI liest nur EPUB): {', '.join(result.skipped_pdf)}")
|
|
if result.failed:
|
|
print(f"{len(result.failed)} Buch/Bücher nicht entschlüsselbar (falscher Schlüssel?): {', '.join(result.failed)}")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="diora-tui — EPUB-Reader im Terminal, synct mit diora")
|
|
parser.add_argument(
|
|
"--library",
|
|
type=Path,
|
|
default=DEFAULT_LIBRARY,
|
|
help=f"Verzeichnis mit EPUB-Dateien (Standard: {DEFAULT_LIBRARY})",
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
login_parser = subparsers.add_parser("login", help="Einmal anmelden (Token + Schlüssel, geschützt gespeichert)")
|
|
login_parser.add_argument("--server", help=f"Server-URL (Standard: {login_mod.DEFAULT_SERVER})")
|
|
login_parser.add_argument("--paste-key", action="store_true", help="Buchschlüssel einfügen statt aus dem Passwort ableiten")
|
|
subparsers.add_parser("logout", help="Gespeicherte Zugangsdaten löschen")
|
|
subparsers.add_parser("sync", help="Bücherliste und Lesestand abgleichen (ohne die Oberfläche)")
|
|
|
|
args = parser.parse_args()
|
|
library_dir = args.library.expanduser()
|
|
|
|
if args.command == "login":
|
|
run_login(args.server, paste_key=args.paste_key)
|
|
return
|
|
if args.command == "logout":
|
|
print("Zugangsdaten gelöscht." if config_mod.forget() else "Keine Zugangsdaten gespeichert.")
|
|
return
|
|
if args.command == "sync":
|
|
run_sync(library_dir)
|
|
return
|
|
|
|
DioraTuiApp(library_dir).run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|