refactor(main): gallery routes use ?tag_id=int

Drops the ?tag=<name> lookup. URLs become unambiguous (distinct
same-name characters each have their own id-keyed URL). Eager-loads
Tag.fandom for the active-tag header rendering.

Applied to all four gallery routes: /gallery, /api/gallery/scroll,
/api/gallery/timeline, /api/gallery/jump — they all shared the same
name-keyed filter pattern. (Plan named only the first two; extended
to all four for behavioral consistency so partial JS migration to
tag_id doesn't silently drop filter on the sub-APIs.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 15:38:22 -04:00
parent 51c13129dd
commit 1e19048b09
+18 -18
View File
@@ -113,17 +113,17 @@ def gallery():
from app.models import PostMetadata
initial_limit = 30
tag_name = request.args.get('tag')
tag_id = request.args.get('tag_id', type=int)
active_tag_obj = (
Tag.query.options(joinedload(Tag.fandom)).get(tag_id)
if tag_id else None
)
# Base query + eager-load tags
q = ImageRecord.query.options(joinedload(ImageRecord.tags))
# Optional tag filter
active_tag_obj = None
if tag_name:
active_tag_obj = Tag.query.filter_by(name=tag_name).first()
if active_tag_obj:
q = q.join(ImageRecord.tags).filter(Tag.name == tag_name)
if active_tag_obj:
q = q.join(ImageRecord.tags).filter(Tag.id == active_tag_obj.id)
# Fetch initial batch ordered by date descending
images = q.order_by(
@@ -169,14 +169,14 @@ def gallery():
page_count = SeriesPage.query.filter_by(series_tag_id=active_tag_obj.id).count()
series_info = {
'tag_id': active_tag_obj.id,
'name': active_tag_obj.name.split(':', 1)[1] if ':' in active_tag_obj.name else active_tag_obj.name,
'name': active_tag_obj.name,
'page_count': page_count,
}
return render_template(
'gallery.html',
grouped_images=grouped_images,
active_tag=tag_name,
active_tag=active_tag_obj.display_name if active_tag_obj else None,
active_tag_obj=active_tag_obj,
initial_cursor=initial_cursor,
has_more=has_more,
@@ -264,14 +264,14 @@ def gallery_scroll():
"""
cursor_str = request.args.get('cursor')
limit = request.args.get('limit', 30, type=int)
tag_name = request.args.get('tag')
tag_id = request.args.get('tag_id', type=int)
# Build base query
q = ImageRecord.query.options(joinedload(ImageRecord.tags))
# Optional tag filter
if tag_name:
q = q.join(ImageRecord.tags).filter(Tag.name == tag_name)
if tag_id:
q = q.join(ImageRecord.tags).filter(Tag.id == tag_id)
# Apply cursor filter (fetch images older than cursor)
if cursor_str:
@@ -330,7 +330,7 @@ def gallery_timeline():
Returns list of {key, label, count, year} ordered newest to oldest.
"""
tag_name = request.args.get('tag')
tag_id = request.args.get('tag_id', type=int)
# Use SQL to aggregate by year-month
date_expr = func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at)
@@ -341,8 +341,8 @@ def gallery_timeline():
func.count(ImageRecord.id).label('count')
)
if tag_name:
q = q.join(ImageRecord.tags).filter(Tag.name == tag_name)
if tag_id:
q = q.join(ImageRecord.tags).filter(Tag.id == tag_id)
q = q.group_by(
extract('year', date_expr),
@@ -391,7 +391,7 @@ def gallery_jump():
"""
year_month = request.args.get('year_month', '')
limit = request.args.get('limit', 30, type=int)
tag_name = request.args.get('tag')
tag_id = request.args.get('tag_id', type=int)
if not year_month:
return jsonify(error='year_month is required'), 400
@@ -410,8 +410,8 @@ def gallery_jump():
# Build query for images in that month and earlier
q = ImageRecord.query.options(joinedload(ImageRecord.tags))
if tag_name:
q = q.join(ImageRecord.tags).filter(Tag.name == tag_name)
if tag_id:
q = q.join(ImageRecord.tags).filter(Tag.id == tag_id)
# Get images from start of target month onwards (but ordered descending)
date_expr = func.coalesce(ImageRecord.taken_at, ImageRecord.imported_at)