"""Gallery API: cursor scroll, timeline, jump, image detail.""" from quart import Blueprint, jsonify, request from ..extensions import get_session from ..services.gallery_service import GalleryService gallery_bp = Blueprint("gallery", __name__, url_prefix="/api/gallery") def _parse_filters(): """Parse the composable gallery filters from query args. Raises ValueError (→ 400) on malformed ids. `tag_id` accepts a single id or a comma-separated list (AND); `media` is image|video; `sort` is newest|oldest.""" tag_raw = request.args.get("tag_id") tag_ids = ( [int(x) for x in tag_raw.split(",") if x.strip()] if tag_raw else None ) or None post_id_raw = request.args.get("post_id") post_id = int(post_id_raw) if post_id_raw else None artist_id_raw = request.args.get("artist_id") artist_id = int(artist_id_raw) if artist_id_raw else None media = request.args.get("media") media_type = media if media in ("image", "video") else None sort = request.args.get("sort") sort = sort if sort in ("newest", "oldest") else "newest" return tag_ids, post_id, artist_id, media_type, sort @gallery_bp.route("/scroll", methods=["GET"]) async def scroll(): cursor = request.args.get("cursor") or None try: limit = int(request.args.get("limit", "50")) tag_ids, post_id, artist_id, media_type, sort = _parse_filters() except ValueError: return jsonify({"error": "invalid filter or limit parameter"}), 400 async with get_session() as session: svc = GalleryService(session) try: page = await svc.scroll( cursor=cursor, limit=limit, tag_ids=tag_ids, post_id=post_id, artist_id=artist_id, media_type=media_type, sort=sort, ) 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(), "posted_at": i.posted_at.isoformat() if i.posted_at else None, "thumbnail_url": i.thumbnail_url, "artist": i.artist, } 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(): try: tag_ids, post_id, artist_id, media_type, _sort = _parse_filters() except ValueError: return jsonify({"error": "invalid filter parameter"}), 400 async with get_session() as session: svc = GalleryService(session) try: buckets = await svc.timeline( tag_ids=tag_ids, post_id=post_id, artist_id=artist_id, media_type=media_type, ) except ValueError as exc: return jsonify({"error": str(exc)}), 400 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"]) tag_ids, post_id, artist_id, media_type, sort = _parse_filters() except (KeyError, ValueError): return jsonify({"error": "year and month query params required"}), 400 async with get_session() as session: svc = GalleryService(session) try: cursor = await svc.jump_cursor( year=year, month=month, tag_ids=tag_ids, post_id=post_id, artist_id=artist_id, media_type=media_type, sort=sort, ) except ValueError as exc: return jsonify({"error": str(exc)}), 400 return jsonify({"cursor": cursor}) @gallery_bp.route("/image/", methods=["GET"]) async def image_detail(image_id: int): async with get_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)