Bindery mounts no CWA database. It searches the OPDS feed by title and reads the story URL inside each candidate epub to find the library book. Updates go to the ingest folder with a .cwa.json sidecar, so CWA replaces the epub of the existing book.
69 lines
2.8 KiB
Python
69 lines
2.8 KiB
Python
"""Hand-off to CWA through its ingest folder. CWA's own calibredb does every write.
|
|
|
|
- A new story goes in as a plain epub, and CWA imports it as a new book.
|
|
- An update goes in with a sidecar file, <name>.epub.cwa.json, that holds
|
|
{"action": "add_format", "book_id": N}. CWA then replaces the epub of book N.
|
|
CWA's own web UI uses the same sidecar, in cps/editbooks.py.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# CWA shortens ingest file names over 150 characters. That would break the
|
|
# match between an epub and its sidecar file, so names stay well under it.
|
|
MAX_NAME_LENGTH = 100
|
|
|
|
|
|
def sanitize_filename(name: str) -> str:
|
|
name = re.sub(r'[\\/:*?"<>|\x00-\x1f]', "_", name).strip(" .")
|
|
return name[:MAX_NAME_LENGTH].strip(" .") or "story"
|
|
|
|
|
|
class IngestFolder:
|
|
def __init__(self, path: Path) -> None:
|
|
self.path = Path(path)
|
|
|
|
def _place(self, epub: Path, final_path: Path) -> Path:
|
|
"""Copy an epub in under a temporary .part name, then rename it.
|
|
|
|
CWA ignores .part files, and reacts to the rename. A file without an
|
|
extension is not safe: CWA deletes files it cannot import.
|
|
"""
|
|
partial = final_path.with_name(final_path.name + ".part")
|
|
shutil.copyfile(epub, partial)
|
|
os.replace(partial, final_path)
|
|
return final_path
|
|
|
|
def ingest(self, epub: Path, name: str) -> Path:
|
|
"""Send a new book to CWA."""
|
|
self.path.mkdir(parents=True, exist_ok=True)
|
|
# A short unique suffix, so a file that CWA has not imported yet is never replaced.
|
|
stem = sanitize_filename(f"{name[:80]} {uuid.uuid4().hex[:6]}")
|
|
final_path = self._place(epub, self.path / f"{stem}.epub")
|
|
logger.info(f"Sent {final_path.name} to the ingest folder as a new book")
|
|
return final_path
|
|
|
|
def send_update(self, epub: Path, book_id: int, name: str) -> Path:
|
|
"""Send a new epub for an existing book. CWA replaces the book's epub."""
|
|
self.path.mkdir(parents=True, exist_ok=True)
|
|
# A unique name, so two updates of one book never share a sidecar file.
|
|
stem = sanitize_filename(f"{name[:60]} {book_id}-{uuid.uuid4().hex[:8]}")
|
|
final_path = self.path / f"{stem}.epub"
|
|
manifest = final_path.with_name(final_path.name + ".cwa.json")
|
|
# The sidecar must exist before the epub arrives. CWA skips .cwa.json files.
|
|
manifest.write_text(json.dumps({"action": "add_format", "book_id": int(book_id)}), encoding="utf-8")
|
|
try:
|
|
self._place(epub, final_path)
|
|
except Exception:
|
|
manifest.unlink(missing_ok=True)
|
|
raise
|
|
logger.info(f"Sent {final_path.name} to the ingest folder as an update of book {book_id}")
|
|
return final_path
|