"""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, CreatorTechniqueItem 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() ) last_technique_sq = ( select(func.max(TechniquePage.created_at)) .where(TechniquePage.creator_id == Creator.id) .correlate(Creator) .scalar_subquery() ) stmt = select( Creator, technique_count_sq.label("technique_count"), video_count_sq.label("video_count"), last_technique_sq.label("last_technique_at"), ).where(Creator.hidden != True) # noqa: E712 # 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 lta = row[3] # None when creator has 0 techniques base = CreatorRead.model_validate(creator) items.append( CreatorBrowseItem( **base.model_dump(), technique_count=tc, video_count=vc, last_technique_at=lta, ) ) # Get total count (without offset/limit) count_stmt = select(func.count()).select_from(Creator).where(Creator.hidden != True) # noqa: E712 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 with full profile data. Returns video/technique counts, full technique list, genre breakdown, bio, social links, and avatar for the creator landing page. """ 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") # Video count video_count = (await db.execute( select(func.count()).select_from(SourceVideo) .where(SourceVideo.creator_id == creator.id) )).scalar() or 0 # Technique pages for this creator technique_rows = (await db.execute( select( TechniquePage.title, TechniquePage.slug, TechniquePage.topic_category, TechniquePage.created_at, ) .where(TechniquePage.creator_id == creator.id) .order_by(TechniquePage.created_at.desc()) )).all() techniques = [ CreatorTechniqueItem( title=t.title, slug=t.slug, topic_category=t.topic_category, created_at=t.created_at, ) for t in technique_rows ] # Genre breakdown across this creator's techniques genre_breakdown: dict[str, int] = {} for t in technique_rows: cat = t.topic_category genre_breakdown[cat] = genre_breakdown.get(cat, 0) + 1 creator_data = CreatorRead.model_validate(creator) return CreatorDetail( **creator_data.model_dump(), bio=creator.bio, social_links=creator.social_links, featured=creator.featured, video_count=video_count, technique_count=len(techniques), techniques=techniques, genre_breakdown=genre_breakdown, )