diff --git a/backend/routers/creators.py b/backend/routers/creators.py index 53f2b67..33617b6 100644 --- a/backend/routers/creators.py +++ b/backend/routers/creators.py @@ -20,14 +20,14 @@ logger = logging.getLogger("chrysopedia.creators") router = APIRouter(prefix="/creators", tags=["creators"]) -@router.get("", response_model=list[CreatorBrowseItem]) +@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[CreatorBrowseItem]: +): """List creators with sort, genre filter, and technique/video counts. - **sort**: ``random`` (default, R014 creator equity), ``alpha``, ``views`` @@ -80,11 +80,17 @@ async def list_creators( 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 + return {"items": items, "total": total, "offset": offset, "limit": limit} @router.get("/{slug}", response_model=CreatorDetail) diff --git a/backend/routers/review.py b/backend/routers/review.py index 3e32d64..53b4e8e 100644 --- a/backend/routers/review.py +++ b/backend/routers/review.py @@ -57,7 +57,7 @@ def _moment_to_queue_item( async def list_queue( status: Annotated[str, Query()] = "pending", offset: Annotated[int, Query(ge=0)] = 0, - limit: Annotated[int, Query(ge=1, le=100)] = 50, + limit: Annotated[int, Query(ge=1, le=1000)] = 50, db: AsyncSession = Depends(get_session), ) -> ReviewQueueResponse: """List key moments in the review queue, filtered by status.""" @@ -313,6 +313,27 @@ async def merge_moments( return KeyMomentRead.model_validate(source) + + +@router.get("/moments/{moment_id}", response_model=ReviewQueueItem) +async def get_moment( + moment_id: uuid.UUID, + db: AsyncSession = Depends(get_session), +) -> ReviewQueueItem: + """Get a single key moment by ID with video and creator info.""" + stmt = ( + select(KeyMoment, SourceVideo.file_path, Creator.name) + .join(SourceVideo, KeyMoment.source_video_id == SourceVideo.id) + .join(Creator, SourceVideo.creator_id == Creator.id) + .where(KeyMoment.id == moment_id) + ) + result = await db.execute(stmt) + row = result.one_or_none() + if row is None: + raise HTTPException(status_code=404, detail=f"Moment {moment_id} not found") + moment, file_path, creator_name = row + return _moment_to_queue_item(moment, file_path or "", creator_name) + @router.get("/mode", response_model=ReviewModeResponse) async def get_mode() -> ReviewModeResponse: """Get the current review mode (review vs auto).""" diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index eda227f..1dd62da 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -121,6 +121,12 @@ export async function fetchQueue( ); } +export async function fetchMoment( + momentId: string, +): Promise { + return request(`${BASE}/moments/${momentId}`); +} + export async function fetchStats(): Promise { return request(`${BASE}/stats`); } diff --git a/frontend/src/pages/MomentDetail.tsx b/frontend/src/pages/MomentDetail.tsx index dbfc600..c21f07e 100644 --- a/frontend/src/pages/MomentDetail.tsx +++ b/frontend/src/pages/MomentDetail.tsx @@ -11,6 +11,7 @@ import { useCallback, useEffect, useState } from "react"; import { useParams, useNavigate, Link } from "react-router-dom"; import { + fetchMoment, fetchQueue, approveMoment, rejectMoment, @@ -59,16 +60,11 @@ export default function MomentDetail() { setError(null); try { // Fetch all moments and find the one matching our ID - const res = await fetchQueue({ limit: 500 }); - const found = res.items.find((m) => m.id === momentId); - if (!found) { - setError("Moment not found"); - } else { - setMoment(found); - setEditTitle(found.title); - setEditSummary(found.summary); - setEditContentType(found.content_type); - } + const found = await fetchMoment(momentId); + setMoment(found); + setEditTitle(found.title); + setEditSummary(found.summary); + setEditContentType(found.content_type); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load moment"); } finally { @@ -174,7 +170,7 @@ export default function MomentDetail() { setActionError(null); try { // Load moments from the same video for merge candidates - const res = await fetchQueue({ limit: 500 }); + const res = await fetchQueue({ limit: 100 }); const candidates = res.items.filter( (m) => m.source_video_id === moment.source_video_id && m.id !== moment.id );