novela/containers/novela/routers/following.py
Ivo Oskamp 5d83bfccab Performance: lazy covers, ETag caching, single DOM pass, SQL tag aggregation
- IntersectionObserver defers both cover images and placeholder canvas
  drawing until cards enter viewport — eliminates 1000+ upfront ops
- ETag on /library/list: browser gets 304 Not Modified when nothing changed
- Single DOM pass in renderBooksGrid/renderDuplicatesView/renderSeriesDetail:
  card.querySelector replaces second iteration with 500+ getElementById calls
- book_tags joined via json_agg in main query, removing separate SELECT + Python merge
- loadLibrary: error handling prevents silent failures showing as infinite loading
- Delete TODO-PERF-library-load.md (all four bottlenecks resolved)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 01:04:32 +01:00

69 lines
2.3 KiB
Python

from urllib.parse import unquote
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from db import get_db_conn
templates = Jinja2Templates(directory="templates")
router = APIRouter()
@router.get("/following", response_class=HTMLResponse)
async def following_page(request: Request):
return templates.TemplateResponse(request, "following.html", {"active": "following"})
@router.get("/api/following")
async def get_following():
"""Return all distinct library authors with their URL (if any) and book stats."""
with get_db_conn() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
l.author,
COUNT(l.filename)::int AS book_count,
MAX(l.created_at) AS last_added,
a.url
FROM library l
LEFT JOIN authors a ON a.name = l.author
WHERE l.author IS NOT NULL AND l.author <> '' AND NOT l.archived
GROUP BY l.author, a.url
ORDER BY l.author
"""
)
return [
{
"name": r[0],
"book_count": r[1],
"last_added": r[2].isoformat() if r[2] else None,
"url": r[3],
}
for r in cur.fetchall()
]
@router.post("/api/following/{author_name:path}")
async def set_author_url(author_name: str, request: Request):
"""Set or clear the URL for an author (empty url removes the entry)."""
author_name = unquote(author_name)
body = await request.json()
url = (body.get("url") or "").strip()
with get_db_conn() as conn:
with conn:
with conn.cursor() as cur:
if url:
cur.execute(
"""
INSERT INTO authors (name, url)
VALUES (%s, %s)
ON CONFLICT (name) DO UPDATE SET url = EXCLUDED.url, updated_at = NOW()
""",
(author_name, url),
)
else:
cur.execute("DELETE FROM authors WHERE name = %s", (author_name,))
return {"ok": True}