import hashlib import logging import mimetypes from pathlib import Path import requests import db from config import CalibreConfig log = logging.getLogger(__name__) MIME_TYPES = { ".epub": "application/epub+zip", ".pdf": "application/pdf", } class CalibreClient: def __init__(self, cfg: CalibreConfig): self._cfg = cfg self._session = requests.Session() self._authenticated = False def _ensure_auth(self) -> None: if self._authenticated: return resp = self._session.post( f"{self._cfg.url}/login", data={"username": self._cfg.user, "password": self._cfg.password}, allow_redirects=True, timeout=30, ) resp.raise_for_status() # Calibre-Web redirects to / on success; a 200 on /login means bad creds if resp.url.endswith("/login"): raise RuntimeError("Calibre-Web authentication failed — check credentials") self._authenticated = True log.info("Authenticated to Calibre-Web at %s", self._cfg.url) def upload(self, book_path: Path, zip_source: str) -> str: """Upload a book file. Returns status: 'uploaded' | 'skipped_duplicate' | 'error'.""" file_hash = _sha256(book_path) if db.is_book_uploaded(file_hash): log.info("Skipping duplicate: %s (hash %s)", book_path.name, file_hash[:8]) db.record_book(book_path.name, file_hash, zip_source, "skipped_duplicate") return "skipped_duplicate" try: self._ensure_auth() mime = MIME_TYPES.get(book_path.suffix.lower(), "application/octet-stream") with book_path.open("rb") as fh: resp = self._session.post( f"{self._cfg.url}/upload", files={"btn-upload": (book_path.name, fh, mime)}, timeout=120, ) resp.raise_for_status() log.info("Uploaded: %s", book_path.name) db.record_book(book_path.name, file_hash, zip_source, "uploaded") return "uploaded" except Exception as e: log.error("Upload failed for %s: %s", book_path.name, e) db.record_book(book_path.name, file_hash, zip_source, "error") return "error" def _sha256(path: Path) -> str: h = hashlib.sha256() with path.open("rb") as f: for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) return h.hexdigest()