smlflg's picture
Initial public upload from Projekte/BookmarkBrain
3ac10cb verified
Raw History Blame Contribute Delete
6.99 kB
import json
from pathlib import Path
from fastapi import FastAPI, Request, UploadFile, File, Query, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from backend.database import get_db, init_db, add_bookmark, get_bookmark, update_bookmark, delete_bookmark, list_bookmarks, get_all_tags, get_stats
from backend.search import hybrid_search
from backend.models import BookmarkCreate, BookmarkUpdate
from backend.embeddings import encode, build_embedding_text
from backend.tagger import generate_tags
from backend.importer import import_bookmarks
app = FastAPI(title="BookmarkBrain", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # chrome-extension://* and moz-extension://*
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
TEMPLATES_DIR = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
templates.env.filters["from_json"] = json.loads
@app.on_event("startup")
def startup():
init_db()
# --- Web UI ---
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
with get_db() as conn:
tags = get_all_tags(conn)
stats = get_stats(conn)
return templates.TemplateResponse("index.html", {
"request": request,
"tags": tags,
"stats": stats,
})
# --- Search ---
@app.get("/api/search")
async def search(request: Request, q: str = Query(..., min_length=1), limit: int = Query(10, ge=1, le=50)):
results = hybrid_search(q, limit=limit)
# If htmx request, return HTML partial
if "HX-Request" in request.headers:
return templates.TemplateResponse("components/search_results.html", {
"request": request,
"results": results,
"query": q,
})
return {"results": results, "query": q, "total": len(results)}
# --- CRUD ---
@app.get("/api/bookmarks")
async def api_list_bookmarks(page: int = Query(1, ge=1), per_page: int = Query(20, ge=1, le=100)):
with get_db() as conn:
bookmarks, total = list_bookmarks(conn, page=page, per_page=per_page)
return {"bookmarks": bookmarks, "total": total, "page": page, "per_page": per_page}
@app.get("/api/bookmarks/{bookmark_id}")
async def api_get_bookmark(bookmark_id: int):
with get_db() as conn:
bm = get_bookmark(conn, bookmark_id)
if not bm:
raise HTTPException(status_code=404, detail="Bookmark not found")
return bm
@app.post("/api/bookmarks")
async def api_create_bookmark(data: BookmarkCreate):
from urllib.parse import urlparse
domain = urlparse(data.url).netloc
auto_tags = generate_tags(data.url, data.title, domain=domain)
embedding_text = build_embedding_text(data.title, data.description, data.context, auto_tags, domain)
embedding_blob = encode(embedding_text)
with get_db() as conn:
bookmark_id = add_bookmark(
conn, url=data.url, title=data.title, description=data.description,
context=data.context, folder_path="", domain=domain,
auto_tags=auto_tags, user_tags=data.user_tags or [],
content_snippet="", embedding_text=embedding_text, embedding_blob=embedding_blob,
)
conn.commit()
bm = get_bookmark(conn, bookmark_id)
return bm
@app.put("/api/bookmarks/{bookmark_id}")
async def api_update_bookmark(bookmark_id: int, data: BookmarkUpdate):
with get_db() as conn:
existing = get_bookmark(conn, bookmark_id)
if not existing:
raise HTTPException(status_code=404, detail="Bookmark not found")
updates = data.model_dump(exclude_none=True)
if "user_tags" in updates:
updates["user_tags"] = json.dumps(updates["user_tags"])
update_bookmark(conn, bookmark_id, **updates)
# Re-embed if content changed
if any(k in updates for k in ("title", "description", "context")):
bm = get_bookmark(conn, bookmark_id)
auto_tags = json.loads(bm["auto_tags"]) if isinstance(bm["auto_tags"], str) else bm["auto_tags"]
embedding_text = build_embedding_text(bm["title"], bm["description"], bm["context"], auto_tags, bm["domain"])
embedding_blob = encode(embedding_text)
# Update vec_bookmarks
conn.execute("DELETE FROM vec_bookmarks WHERE id = ?", (bookmark_id,))
conn.execute("INSERT INTO vec_bookmarks (id, embedding) VALUES (?, ?)", (bookmark_id, embedding_blob))
conn.execute("UPDATE bookmarks SET embedding_text = ? WHERE id = ?", (embedding_text, bookmark_id))
conn.commit()
return get_bookmark(conn, bookmark_id)
@app.delete("/api/bookmarks/{bookmark_id}")
async def api_delete_bookmark(bookmark_id: int):
with get_db() as conn:
existing = get_bookmark(conn, bookmark_id)
if not existing:
raise HTTPException(status_code=404, detail="Bookmark not found")
delete_bookmark(conn, bookmark_id)
conn.commit()
return {"status": "deleted", "id": bookmark_id}
# --- Import ---
@app.post("/api/import")
async def api_import(file: UploadFile = File(...)):
import tempfile
suffix = ".html" if file.filename and file.filename.endswith(".html") else ".html"
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False, mode="wb") as tmp:
content = await file.read()
tmp.write(content)
tmp_path = tmp.name
result = import_bookmarks(tmp_path)
Path(tmp_path).unlink(missing_ok=True)
return result
# --- Tags & Stats ---
@app.get("/api/tags")
async def api_tags():
with get_db() as conn:
tags = get_all_tags(conn)
return [{"tag": t, "count": c} for t, c in tags]
@app.get("/api/stats")
async def api_stats():
with get_db() as conn:
return get_stats(conn)
# --- Reindex ---
@app.post("/api/reindex")
async def api_reindex():
from backend.embeddings import encode_batch, build_embedding_text
with get_db() as conn:
rows = conn.execute("SELECT id, title, description, context, auto_tags, domain FROM bookmarks").fetchall()
texts = []
ids = []
for row in rows:
auto_tags = json.loads(row["auto_tags"]) if isinstance(row["auto_tags"], str) else row["auto_tags"]
text = build_embedding_text(row["title"], row["description"], row["context"], auto_tags, row["domain"])
texts.append(text)
ids.append(row["id"])
blobs = encode_batch(texts)
# Clear and rebuild vec table
conn.execute("DELETE FROM vec_bookmarks")
for id, blob, text in zip(ids, blobs, texts):
conn.execute("INSERT INTO vec_bookmarks (id, embedding) VALUES (?, ?)", (id, blob))
conn.execute("UPDATE bookmarks SET embedding_text = ? WHERE id = ?", (text, id))
conn.commit()
return {"reindexed": len(ids)}