- Creators: response_model changed from list to {items, total, offset, limit} matching frontend CreatorBrowseResponse
- Review queue: limit raised from 100 to 1000
- New GET /review/moments/{moment_id} endpoint for direct moment fetch
- MomentDetail uses fetchMoment instead of fetching full queue
- Merge candidates fetch uses limit=100
119 lines
3.7 KiB
Python
119 lines
3.7 KiB
Python
"""Creator endpoints for Chrysopedia API.
|
|
|
|
Enhanced with sort (random default per R014), genre filter, and
|
|
technique/video counts for browse pages.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy import func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from database import get_session
|
|
from models import Creator, SourceVideo, TechniquePage
|
|
from schemas import CreatorBrowseItem, CreatorDetail, CreatorRead
|
|
|
|
logger = logging.getLogger("chrysopedia.creators")
|
|
|
|
router = APIRouter(prefix="/creators", tags=["creators"])
|
|
|
|
|
|
@router.get("")
|
|
async def list_creators(
|
|
sort: Annotated[str, Query()] = "random",
|
|
genre: Annotated[str | None, Query()] = None,
|
|
offset: Annotated[int, Query(ge=0)] = 0,
|
|
limit: Annotated[int, Query(ge=1, le=100)] = 50,
|
|
db: AsyncSession = Depends(get_session),
|
|
):
|
|
"""List creators with sort, genre filter, and technique/video counts.
|
|
|
|
- **sort**: ``random`` (default, R014 creator equity), ``alpha``, ``views``
|
|
- **genre**: filter by genre (matches against ARRAY column)
|
|
"""
|
|
# Subqueries for counts
|
|
technique_count_sq = (
|
|
select(func.count())
|
|
.where(TechniquePage.creator_id == Creator.id)
|
|
.correlate(Creator)
|
|
.scalar_subquery()
|
|
)
|
|
video_count_sq = (
|
|
select(func.count())
|
|
.where(SourceVideo.creator_id == Creator.id)
|
|
.correlate(Creator)
|
|
.scalar_subquery()
|
|
)
|
|
|
|
stmt = select(
|
|
Creator,
|
|
technique_count_sq.label("technique_count"),
|
|
video_count_sq.label("video_count"),
|
|
)
|
|
|
|
# Genre filter
|
|
if genre:
|
|
stmt = stmt.where(Creator.genres.any(genre))
|
|
|
|
# Sorting
|
|
if sort == "alpha":
|
|
stmt = stmt.order_by(Creator.name)
|
|
elif sort == "views":
|
|
stmt = stmt.order_by(Creator.view_count.desc())
|
|
else:
|
|
# Default: random (small dataset <100, func.random() is fine)
|
|
stmt = stmt.order_by(func.random())
|
|
|
|
stmt = stmt.offset(offset).limit(limit)
|
|
result = await db.execute(stmt)
|
|
rows = result.all()
|
|
|
|
items: list[CreatorBrowseItem] = []
|
|
for row in rows:
|
|
creator = row[0]
|
|
tc = row[1] or 0
|
|
vc = row[2] or 0
|
|
base = CreatorRead.model_validate(creator)
|
|
items.append(
|
|
CreatorBrowseItem(**base.model_dump(), technique_count=tc, video_count=vc)
|
|
)
|
|
|
|
# Get total count (without offset/limit)
|
|
count_stmt = select(func.count()).select_from(Creator)
|
|
if genre:
|
|
count_stmt = count_stmt.where(Creator.genres.any(genre))
|
|
total = (await db.execute(count_stmt)).scalar() or 0
|
|
|
|
logger.debug(
|
|
"Listed %d creators (sort=%s, genre=%s, offset=%d, limit=%d)",
|
|
len(items), sort, genre, offset, limit,
|
|
)
|
|
return {"items": items, "total": total, "offset": offset, "limit": limit}
|
|
|
|
|
|
@router.get("/{slug}", response_model=CreatorDetail)
|
|
async def get_creator(
|
|
slug: str,
|
|
db: AsyncSession = Depends(get_session),
|
|
) -> CreatorDetail:
|
|
"""Get a single creator by slug, including video count."""
|
|
stmt = select(Creator).where(Creator.slug == slug)
|
|
result = await db.execute(stmt)
|
|
creator = result.scalar_one_or_none()
|
|
|
|
if creator is None:
|
|
raise HTTPException(status_code=404, detail=f"Creator '{slug}' not found")
|
|
|
|
# Count videos for this creator
|
|
count_stmt = (
|
|
select(func.count())
|
|
.select_from(SourceVideo)
|
|
.where(SourceVideo.creator_id == creator.id)
|
|
)
|
|
count_result = await db.execute(count_stmt)
|
|
video_count = count_result.scalar() or 0
|
|
|
|
creator_data = CreatorRead.model_validate(creator)
|
|
return CreatorDetail(**creator_data.model_dump(), video_count=video_count)
|