diff --git a/sync.py b/sync.py index 0939934..cf68200 100644 --- a/sync.py +++ b/sync.py @@ -7,7 +7,7 @@ import config import db import extractor import sftp as sftp_module -from uploader import CalibreClient +from uploader import CalibreClient, CalibreUnavailableError log = logging.getLogger(__name__) @@ -86,6 +86,7 @@ def run_sync(limit: int | None = None) -> None: t2 = time.monotonic() status = client.upload(book, zip_source=remote_zip.remote_path) log.info("Upload '%s' → %s (%.1fs)", book.name, status, time.monotonic() - t2) + time.sleep(2) if status == "uploaded": counters["books_uploaded"] += 1 elif status == "skipped_duplicate": @@ -99,6 +100,11 @@ def run_sync(limit: int | None = None) -> None: zip_error = f"{books_errored_this_zip} book upload(s) failed — will retry next sync" extractor.cleanup(work_dir / "extracted" / local_zip.stem) + except CalibreUnavailableError as e: + log.error("Calibre-Web unavailable — aborting sync run: %s", e) + db.mark_zip_processed(remote_zip.remote_path, remote_zip.file_size, "error", str(e)) + db.finish_sync_run(run_id, status="error", error_msg=str(e), **counters) + return except Exception as e: log.error("Error processing %s: %s", remote_zip.remote_path, e) zip_status = "error" diff --git a/uploader.py b/uploader.py index 9bf0429..ea66a6d 100644 --- a/uploader.py +++ b/uploader.py @@ -25,12 +25,17 @@ _JUNK_WORDS = { } +class CalibreUnavailableError(RuntimeError): + """Raised when Calibre-Web returns repeated 502/503/504 — sync run should abort.""" + + class CalibreClient: def __init__(self, cfg: CalibreConfig): self._cfg = cfg self._session = requests.Session() self._authenticated = False self._upload_csrf: str | None = None + self._consecutive_failures = 0 def _ensure_auth(self) -> None: if self._authenticated: @@ -62,6 +67,7 @@ class CalibreClient: try: resp = self._session.get( f"{self._cfg.url}/opds/search/{quote(query, safe='')}", + auth=(self._cfg.user, self._cfg.password), timeout=15, ) if resp.status_code == 404: @@ -102,7 +108,6 @@ class CalibreClient: return "skipped_duplicate" mime = MIME_TYPES.get(book_path.suffix.lower(), "application/octet-stream") - last_err: Exception | None = None for attempt in range(1, 4): try: with book_path.open("rb") as fh: @@ -116,15 +121,23 @@ class CalibreClient: log.error("Upload HTTP %s (attempt %d/3) — body: %s", resp.status_code, attempt, resp.text[:300]) resp.raise_for_status() log.info("Uploaded: %s", book_path.name) + self._consecutive_failures = 0 db.record_book(book_path.name, file_hash, zip_source, "uploaded") return "uploaded" - except requests.HTTPError as e: - last_err = e - if resp.status_code in (502, 503, 504) and attempt < 3: - delay = 60 - log.warning("HTTP %s on attempt %d/3 — retrying in %ds ...", resp.status_code, attempt, delay) - time.sleep(delay) - continue + except requests.HTTPError: + if resp.status_code in (502, 503, 504): + if attempt < 3: + log.warning("HTTP %s on attempt %d/3 — retrying in 60s ...", resp.status_code, attempt) + time.sleep(60) + continue + # All retries exhausted + self._consecutive_failures += 1 + if self._consecutive_failures >= 3: + raise CalibreUnavailableError( + f"Calibre-Web returned {resp.status_code} on {self._consecutive_failures} " + "consecutive books — aborting sync run" + ) + break if resp.status_code == 400 and attempt == 1: log.warning("HTTP 400 — CSRF token likely expired, re-authenticating ...") self._authenticated = False @@ -135,6 +148,9 @@ class CalibreClient: db.record_book(book_path.name, file_hash, zip_source, "error") return "error" + except CalibreUnavailableError: + db.record_book(book_path.name, file_hash, zip_source, "error") + raise 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")