- DB-stored books (Fase 1–6): chapters and images stored in PostgreSQL; grabber writes to DB, EPUB→DB conversion, DB→EPUB export, FTS search page (/search) - Chapter editor: Monaco editor supports DB-stored books; inline title editing - Grabber: DB/EPUB storage toggle on Convert page - Backup: restore from Dropbox snapshot (browse snapshots, restore individual or selected files) - AO3 scraper: initial implementation - Changelog: v0.1.2 and v0.1.3 entries added to changelog.py and changelog.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
2.0 KiB
Python
64 lines
2.0 KiB
Python
"""search.py — Full-text search over DB-stored book chapters."""
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import HTMLResponse, JSONResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
from db import get_db_conn
|
|
|
|
router = APIRouter()
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
|
|
@router.get("/search", response_class=HTMLResponse)
|
|
async def search_page(request: Request):
|
|
return templates.TemplateResponse(request, "search.html", {"active": "search"})
|
|
|
|
|
|
@router.get("/api/search")
|
|
async def api_search(q: str = ""):
|
|
q = q.strip()
|
|
if not q or len(q) > 500:
|
|
return JSONResponse([])
|
|
with get_db_conn() as conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"""
|
|
SELECT
|
|
l.filename,
|
|
l.title,
|
|
l.author,
|
|
bc.chapter_index,
|
|
bc.title AS chapter_title,
|
|
ts_headline(
|
|
'simple', bc.content,
|
|
plainto_tsquery('simple', %s),
|
|
'MaxFragments=1, MaxWords=25, MinWords=8, StartSel=<mark>, StopSel=</mark>'
|
|
) AS snippet,
|
|
ts_rank(bc.content_tsv, plainto_tsquery('simple', %s)) AS rank
|
|
FROM book_chapters bc
|
|
JOIN library l ON l.filename = bc.filename
|
|
WHERE (bc.content_tsv @@ plainto_tsquery('simple', %s)
|
|
OR LOWER(bc.title) LIKE LOWER('%%' || %s || '%%'))
|
|
AND NOT l.archived
|
|
ORDER BY rank DESC, bc.chapter_index ASC
|
|
LIMIT 30
|
|
""",
|
|
(q, q, q, q),
|
|
)
|
|
rows = cur.fetchall()
|
|
|
|
results = [
|
|
{
|
|
"filename": r[0],
|
|
"title": r[1] or "",
|
|
"author": r[2] or "",
|
|
"chapter_index": r[3],
|
|
"chapter_title": r[4] or "",
|
|
"snippet": r[5] or "",
|
|
"rank": float(r[6]),
|
|
}
|
|
for r in rows
|
|
]
|
|
return JSONResponse(results)
|