name change and local sync added
This commit is contained in:
@@ -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
@@ -1,5 +1,5 @@
|
||||
services:
|
||||
calibresync:
|
||||
grimmorysync:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user