feat(gallery): composable scroll filter (multi-tag AND, media, sort)
Phase 1 backend for the gallery filter bar. Extends scroll/timeline/jump
from a single mutually-exclusive filter to a composable one:
- tag_ids: image must carry ALL of them (one correlated EXISTS per tag —
AND, no row multiplication), replacing the single-tag JOIN.
- artist_id composes with tags; media_type ('image'|'video') narrows by
mime; post_id stays the exclusive post-detail path.
- sort ('newest'|'oldest') flips the effective_date/id cursor comparison
and ordering; the cursor value is unchanged (direction comes from the
request). jump_cursor honors sort too.
- Shared _apply_scope helper applied across scroll/timeline/jump so the
timeline sidebar reflects the filtered set. API _parse_filters parses
tag_id (comma list), artist_id, media, sort.
Tests: multi-tag AND, media filter, sort reversal (service + API);
post_id-excludes-others; single tag_id back-compat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-22
@@ -8,26 +8,42 @@ 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": "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
|
||||
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
|
||||
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_id=tag_id,
|
||||
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
|
||||
@@ -58,17 +74,16 @@ async def scroll():
|
||||
|
||||
@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
|
||||
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
|
||||
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_id=tag_id, post_id=post_id, artist_id=artist_id
|
||||
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
|
||||
@@ -82,20 +97,16 @@ 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
|
||||
tag_id_raw = request.args.get("tag_id")
|
||||
tag_id = int(tag_id_raw) if tag_id_raw else 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
|
||||
async with get_session() as session:
|
||||
svc = GalleryService(session)
|
||||
try:
|
||||
cursor = await svc.jump_cursor(
|
||||
year=year, month=month, tag_id=tag_id,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user