"""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)