Files
calibresync/main.py
T
2026-05-10 15:45:01 +02:00

170 lines
5.1 KiB
Python

import logging
from contextlib import asynccontextmanager
from pathlib import Path
from apscheduler.schedulers.background import BackgroundScheduler
from fastapi import BackgroundTasks, FastAPI, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import config
import db
import sync
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s%(message)s")
log = logging.getLogger(__name__)
_scheduler = BackgroundScheduler(timezone="UTC")
def _reschedule_auto_sync() -> None:
_scheduler.remove_all_jobs()
try:
interval = int(db.get_setting("scheduler_interval_minutes", "0") or "0")
except ValueError:
interval = 0
if interval > 0:
_scheduler.add_job(sync.run_sync, "interval", minutes=interval, id="auto_sync")
log.info("Auto-sync scheduled every %d minute(s)", interval)
else:
log.info("Auto-sync disabled")
def next_run_time() -> str | None:
job = _scheduler.get_job("auto_sync")
if job and job.next_run_time:
return job.next_run_time.strftime("%Y-%m-%d %H:%M UTC")
return None
@asynccontextmanager
async def lifespan(app: FastAPI):
db.init_db()
_scheduler.start()
_reschedule_auto_sync()
yield
_scheduler.shutdown(wait=False)
app = FastAPI(title="CalibreSync", lifespan=lifespan)
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
templates = Jinja2Templates(directory=Path(__file__).parent / "templates")
# --- Dashboard ---
@app.get("/", response_class=HTMLResponse)
async def dashboard(request: Request):
stats = db.get_stats()
runs = [dict(r) for r in db.get_recent_runs(10)]
zips = [dict(z) for z in db.get_recent_zips(20)]
interval = int(db.get_setting("scheduler_interval_minutes", "0") or "0")
batch_size = int(db.get_setting("sync_batch_size", "0") or "0")
return templates.TemplateResponse(
"index.html",
{
"request": request,
"stats": stats,
"runs": runs,
"zips": zips,
"sync_running": sync.is_running(),
"next_run": next_run_time(),
"interval": interval,
"batch_size": batch_size,
},
)
# --- Books ---
@app.get("/books", response_class=HTMLResponse)
async def books_page(request: Request, page: int = 1):
per_page = 50
offset = (page - 1) * per_page
books = [dict(b) for b in db.get_books(limit=per_page, offset=offset)]
total = db.get_books_count()
pages = max(1, (total + per_page - 1) // per_page)
return templates.TemplateResponse(
"books.html",
{"request": request, "books": books, "page": page, "pages": pages, "total": total},
)
# --- Settings ---
@app.get("/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
s = db.get_all_settings()
has_key = bool(s.get("sftp_key", "").strip())
return templates.TemplateResponse(
"settings.html",
{"request": request, "s": s, "has_key": has_key},
)
@app.post("/settings")
async def save_settings(
request: Request,
sftp_host: str = Form(""),
sftp_port: str = Form("22"),
sftp_user: str = Form(""),
sftp_auth_method: str = Form("key"),
sftp_key: str = Form(""),
sftp_password: str = Form(""),
sftp_remote_path: str = Form(""),
calibre_url: str = Form(""),
calibre_user: str = Form(""),
calibre_pass: str = Form(""),
local_work_dir: str = Form("/tmp/calibresync"),
scheduler_interval_minutes: str = Form("0"),
sync_batch_size: str = Form("0"),
):
config.save({
"sftp_host": sftp_host,
"sftp_port": sftp_port,
"sftp_user": sftp_user,
"sftp_auth_method": sftp_auth_method,
"sftp_key": sftp_key,
"sftp_password": sftp_password,
"sftp_remote_path": sftp_remote_path,
"calibre_url": calibre_url,
"calibre_user": calibre_user,
"calibre_pass": calibre_pass,
"local_work_dir": local_work_dir,
"scheduler_interval_minutes": scheduler_interval_minutes,
"sync_batch_size": sync_batch_size,
})
_reschedule_auto_sync()
return RedirectResponse("/settings?saved=1", status_code=303)
# --- Sync triggers ---
@app.post("/sync")
async def trigger_sync(background_tasks: BackgroundTasks):
if sync.is_running():
return RedirectResponse("/?already_running=1", status_code=303)
background_tasks.add_task(sync.run_sync)
return RedirectResponse("/?started=1", status_code=303)
@app.post("/sync/test")
async def trigger_test_sync(background_tasks: BackgroundTasks):
if sync.is_running():
return RedirectResponse("/?already_running=1", status_code=303)
background_tasks.add_task(sync.run_sync, 1)
return RedirectResponse("/?test_started=1", status_code=303)
# --- JSON status API ---
@app.get("/api/status")
async def api_status():
return {
"sync_running": sync.is_running(),
"next_run": next_run_time(),
"stats": db.get_stats(),
"last_run": [dict(r) for r in db.get_recent_runs(1)],
}