158 lines
4.6 KiB
Python
158 lines
4.6 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")
|
|
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,
|
|
},
|
|
)
|
|
|
|
|
|
# --- 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"),
|
|
):
|
|
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,
|
|
})
|
|
_reschedule_auto_sync()
|
|
return RedirectResponse("/settings?saved=1", status_code=303)
|
|
|
|
|
|
# --- Sync trigger ---
|
|
|
|
@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)
|
|
|
|
|
|
# --- 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)],
|
|
}
|