feat(fc2a): add /api/gallery endpoints (scroll, timeline, jump, image detail)
Thin Quart blueprint that delegates to GalleryService. Cursor-based scroll returns images + a date_groups summary so the SPA can render year-month headers without re-grouping client-side. Timeline returns year-month buckets for the sidebar jump nav. Jump returns a cursor positioned at a specific year-month so the gallery can scroll to historical periods. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import logging
|
||||
|
||||
from quart import Quart
|
||||
|
||||
from .api import api_bp
|
||||
from .api import all_blueprints
|
||||
from .config import get_config
|
||||
from .frontend import frontend_bp
|
||||
|
||||
@@ -15,7 +15,9 @@ def create_app() -> Quart:
|
||||
|
||||
app = Quart(__name__)
|
||||
app.secret_key = cfg.secret_key
|
||||
app.register_blueprint(api_bp)
|
||||
|
||||
for bp in all_blueprints():
|
||||
app.register_blueprint(bp)
|
||||
# Registered last so /api/* routes win over the SPA catch-all.
|
||||
app.register_blueprint(frontend_bp)
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"""API blueprint registration."""
|
||||
"""API blueprint registration.
|
||||
|
||||
This module is imported by the Quart app factory; it just exposes the
|
||||
top-level `api_bp` for backward compatibility with FC-1's health route,
|
||||
and ALL_BLUEPRINTS for the factory to register sibling blueprints.
|
||||
"""
|
||||
|
||||
from quart import Blueprint
|
||||
|
||||
@@ -6,3 +11,9 @@ from . import health
|
||||
|
||||
api_bp = Blueprint("api", __name__, url_prefix="/api")
|
||||
api_bp.add_url_rule("/health", view_func=health.get_health, methods=["GET"])
|
||||
|
||||
|
||||
def all_blueprints() -> list[Blueprint]:
|
||||
from .gallery import gallery_bp
|
||||
# FC-2a additions: tags, settings, import_admin land in later tasks
|
||||
return [api_bp, gallery_bp]
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Gallery API: cursor scroll, timeline, jump, image detail."""
|
||||
|
||||
from quart import Blueprint, jsonify, request
|
||||
|
||||
from ..extensions import make_engine, make_session_factory
|
||||
from ..services.gallery_service import GalleryService
|
||||
|
||||
gallery_bp = Blueprint("gallery", __name__, url_prefix="/api/gallery")
|
||||
|
||||
_engine = None
|
||||
_Session = None
|
||||
|
||||
|
||||
def _session_factory():
|
||||
global _engine, _Session
|
||||
if _engine is None:
|
||||
_engine = make_engine()
|
||||
_Session = make_session_factory(_engine)
|
||||
return _Session
|
||||
|
||||
|
||||
@gallery_bp.route("/scroll", methods=["GET"])
|
||||
async def scroll():
|
||||
cursor = request.args.get("cursor") or None
|
||||
try:
|
||||
limit = int(request.args.get("limit", "50"))
|
||||
except ValueError:
|
||||
return jsonify({"error": "limit must be an integer"}), 400
|
||||
tag_id_raw = request.args.get("tag_id")
|
||||
tag_id = int(tag_id_raw) if tag_id_raw else None
|
||||
|
||||
Session = _session_factory()
|
||||
async with Session() as session:
|
||||
svc = GalleryService(session)
|
||||
try:
|
||||
page = await svc.scroll(cursor=cursor, limit=limit, tag_id=tag_id)
|
||||
except ValueError as exc:
|
||||
return jsonify({"error": str(exc)}), 400
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"id": i.id,
|
||||
"sha256": i.sha256,
|
||||
"mime": i.mime,
|
||||
"width": i.width,
|
||||
"height": i.height,
|
||||
"created_at": i.created_at.isoformat(),
|
||||
"thumbnail_url": i.thumbnail_url,
|
||||
}
|
||||
for i in page.images
|
||||
],
|
||||
"next_cursor": page.next_cursor,
|
||||
"date_groups": [
|
||||
{"year": y, "month": m, "image_ids": ids} for y, m, ids in page.date_groups
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@gallery_bp.route("/timeline", methods=["GET"])
|
||||
async def timeline():
|
||||
tag_id_raw = request.args.get("tag_id")
|
||||
tag_id = int(tag_id_raw) if tag_id_raw else None
|
||||
Session = _session_factory()
|
||||
async with Session() as session:
|
||||
svc = GalleryService(session)
|
||||
buckets = await svc.timeline(tag_id=tag_id)
|
||||
return jsonify([{"year": b.year, "month": b.month, "count": b.count} for b in buckets])
|
||||
|
||||
|
||||
@gallery_bp.route("/jump", methods=["GET"])
|
||||
async def jump():
|
||||
try:
|
||||
year = int(request.args["year"])
|
||||
month = int(request.args["month"])
|
||||
except (KeyError, ValueError):
|
||||
return jsonify({"error": "year and month query params required"}), 400
|
||||
tag_id_raw = request.args.get("tag_id")
|
||||
tag_id = int(tag_id_raw) if tag_id_raw else None
|
||||
Session = _session_factory()
|
||||
async with Session() as session:
|
||||
svc = GalleryService(session)
|
||||
cursor = await svc.jump_cursor(year=year, month=month, tag_id=tag_id)
|
||||
return jsonify({"cursor": cursor})
|
||||
|
||||
|
||||
@gallery_bp.route("/image/<int:image_id>", methods=["GET"])
|
||||
async def image_detail(image_id: int):
|
||||
Session = _session_factory()
|
||||
async with Session() as session:
|
||||
svc = GalleryService(session)
|
||||
payload = await svc.get_image_with_tags(image_id)
|
||||
if payload is None:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
return jsonify(payload)
|
||||
Reference in New Issue
Block a user