- 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>
69 lines
2.3 KiB
Python
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}
|