name change and local sync added

This commit is contained in:
2026-05-14 22:43:58 +02:00
parent 3a84b3344e
commit 8f26c7bc3b
5 changed files with 108 additions and 10 deletions
+3 -3
View File
@@ -26,7 +26,7 @@ class GrimmoryConfig:
class AppConfig:
sftp: SFTPConfig = field(default_factory=SFTPConfig)
grimmory: GrimmoryConfig = field(default_factory=GrimmoryConfig)
work_dir: str = "/tmp/calibresync"
work_dir: str = "/tmp/grimmorysync"
def load() -> AppConfig:
@@ -47,7 +47,7 @@ def load() -> AppConfig:
password=s.get("grimmory_password", ""),
bookdrop_path=s.get("grimmory_bookdrop_path", ""),
),
work_dir=s.get("work_dir", "/tmp/calibresync"),
work_dir=s.get("work_dir", "/tmp/grimmorysync"),
)
@@ -56,7 +56,7 @@ def save(form: dict) -> None:
"sftp_host", "sftp_port", "sftp_user", "sftp_auth_method",
"sftp_remote_path",
"grimmory_url", "grimmory_user", "grimmory_bookdrop_path",
"work_dir",
"work_dir", "local_import_path",
"scheduler_interval_minutes", "sync_batch_size",
]
for key in keys:
+1 -1
View File
@@ -1,5 +1,5 @@
services:
calibresync:
grimmorysync:
build: .
ports:
- "8000:8000"
+15 -2
View File
@@ -49,7 +49,7 @@ async def lifespan(app: FastAPI):
_scheduler.shutdown(wait=False)
app = FastAPI(title="CalibreSync", lifespan=lifespan)
app = FastAPI(title="GrimmorySync", lifespan=lifespan)
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
@@ -102,7 +102,7 @@ class SettingsPayload(BaseModel):
grimmory_user: str = ""
grimmory_password: str = ""
grimmory_bookdrop_path: str = ""
work_dir: str = "/tmp/calibresync"
work_dir: str = "/tmp/grimmorysync"
scheduler_interval_minutes: str = "0"
sync_batch_size: str = "0"
@@ -141,6 +141,19 @@ async def api_trigger_rescan(background_tasks: BackgroundTasks):
return {"ok": True}
# --- Local folder import ---
@app.post("/api/import/local")
async def api_local_import(background_tasks: BackgroundTasks):
if sync.is_running():
return {"ok": False, "reason": "already_running"}
folder = db.get_setting("local_import_path", "")
if not folder:
return {"ok": False, "reason": "no_path_configured"}
background_tasks.add_task(sync.run_local_import, folder)
return {"ok": True}
# --- Connection tests ---
@app.get("/api/test/ssh")
+24 -4
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CalibreSync</title>
<title>GrimmorySync</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet" />
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
@@ -23,7 +23,7 @@
<!-- Navbar -->
<div class="navbar bg-base-200 shadow-md sticky top-0 z-40">
<div class="navbar-start">
<span class="text-xl font-bold px-4 tracking-tight">CalibreSync</span>
<span class="text-xl font-bold px-4 tracking-tight">GrimmorySync</span>
</div>
<div class="navbar-center">
<div role="tablist" class="tabs tabs-boxed">
@@ -56,6 +56,8 @@
@click="triggerSync('/api/sync/rescan')">Rescan remote</button>
<button class="btn btn-sm btn-outline" :disabled="dashboard.sync_running"
@click="triggerSync('/api/sync/test')">Test (1 zip)</button>
<button class="btn btn-sm btn-outline" :disabled="dashboard.sync_running"
@click="triggerSync('/api/import/local')">Import local folder</button>
<button class="btn btn-sm btn-primary" :disabled="dashboard.sync_running"
@click="triggerSync('/api/sync')">
<span x-show="dashboard.sync_running" class="loading loading-spinner loading-xs"></span>
@@ -309,7 +311,7 @@
<div class="form-control">
<label class="label py-1"><span class="label-text text-xs">Work directory</span></label>
<input name="work_dir" type="text" class="input input-bordered input-sm"
:value="settings.settings.work_dir ?? '/tmp/calibresync'" />
:value="settings.settings.work_dir ?? '/tmp/grimmorysync'" />
</div>
</div>
</div>
@@ -342,6 +344,21 @@
<button type="submit" class="btn btn-primary">Save settings</button>
</div>
<!-- Local import -->
<div class="card bg-base-200 shadow mb-4">
<div class="card-body gap-3">
<h2 class="card-title text-base">Local import</h2>
<div class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Folder path</span>
<span class="label-text-alt text-xs opacity-50">directory of epub/pdf files to import</span>
</label>
<input name="local_import_path" type="text" class="input input-bordered input-sm"
:value="settings.settings.local_import_path ?? ''" placeholder="/mnt/books" />
</div>
</div>
</div>
<!-- Danger zone -->
<div class="card border border-error">
<div class="card-body gap-2">
@@ -422,7 +439,10 @@ function app() {
this._prevRunning = true
this.showToast('Started', 'success')
} else {
this.showToast(r.reason === 'already_running' ? 'Sync already running' : 'Failed to start', 'error')
this.showToast(
r.reason === 'already_running' ? 'Sync already running' :
r.reason === 'no_path_configured' ? 'Set a local import path in Settings first' :
'Failed to start', 'error')
}
} catch (_) {
this.showToast('Request failed', 'error')
+65
View File
@@ -14,6 +14,8 @@ log = logging.getLogger(__name__)
_lock = threading.Lock()
_running = False
_BOOK_EXTENSIONS = {".epub", ".pdf", ".mobi", ".cbz"}
def is_running() -> bool:
return _running
@@ -130,6 +132,69 @@ def run_sync(limit: int | None = None) -> None:
_lock.release()
def run_local_import(folder_path: str) -> None:
"""Process a local folder of book files using the same duplicate-check + bookdrop flow."""
global _running
if not _lock.acquire(blocking=False):
log.warning("Sync already running, skipping local import")
return
_running = True
run_id = db.start_sync_run()
counters = dict(zips_found=0, zips_new=0, books_imported=0, books_skipped=0, books_errored=0)
try:
cfg = config.load()
if not cfg.grimmory.bookdrop_path:
raise ValueError("Grimmory bookdrop path not configured")
folder = Path(folder_path)
if not folder.is_dir():
raise ValueError(f"Not a directory: {folder_path}")
books = [p for p in folder.rglob("*") if p.is_file() and p.suffix.lower() in _BOOK_EXTENSIONS]
log.info("Local import: found %d book file(s) in %s", len(books), folder_path)
counters["zips_found"] = len(books)
counters["zips_new"] = len(books)
for book in books:
try:
sha256 = grimmory_module.compute_sha256(book)
if db.is_book_processed(sha256):
log.info("Skipping '%s' — sha256 already processed", book.name)
counters["books_skipped"] += 1
db.record_book(None, book.name, sha256, status="skipped")
continue
result = grimmory_module.place_book(
book,
cfg.grimmory.bookdrop_path,
cfg.grimmory.url,
cfg.grimmory.user,
cfg.grimmory.password,
sha256=sha256,
)
if result.status == "success":
counters["books_imported"] += 1
elif result.status == "skipped":
counters["books_skipped"] += 1
db.record_book(None, book.name, result.sha256,
status=result.status, error_msg=result.error_msg)
except Exception as e:
log.error("Error importing '%s': %s", book.name, e)
counters["books_errored"] += 1
db.record_book(None, book.name, "", status="error", error_msg=str(e))
db.finish_sync_run(run_id, status="success", **counters)
log.info("Local import done. Imported: %d, Skipped: %d, Errors: %d",
counters["books_imported"], counters["books_skipped"], counters["books_errored"])
except Exception as e:
log.exception("Local import failed: %s", e)
db.finish_sync_run(run_id, status="error", error_msg=str(e), **counters)
finally:
_running = False
_lock.release()
def _validate_config(cfg) -> None:
missing = []
if not cfg.sftp.host: