"""Creator chapter management endpoints — review, edit, reorder, approve chapters. Auth-guarded endpoints for creators to manage auto-detected chapters for their videos before publication. """ import logging import uuid from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from auth import get_current_user from database import get_session from models import ChapterStatus, KeyMoment, SourceVideo, User from schemas import ( ChapterBulkApproveRequest, ChapterMarkerRead, ChapterReorderRequest, ChapterUpdate, ChaptersResponse, ) logger = logging.getLogger("chrysopedia.creator_chapters") router = APIRouter(prefix="/creator", tags=["creator-chapters"]) async def _verify_creator_owns_video( current_user: User, video_id: uuid.UUID, db: AsyncSession, ) -> None: """Verify the user is a creator and owns the specified video.""" if current_user.creator_id is None: raise HTTPException(status_code=403, detail="No creator profile linked") video = (await db.execute( select(SourceVideo).where( SourceVideo.id == video_id, SourceVideo.creator_id == current_user.creator_id, ) )).scalar_one_or_none() if video is None: raise HTTPException(status_code=404, detail="Video not found or not owned by you") @router.get("/{video_id}/chapters", response_model=ChaptersResponse) async def get_creator_chapters( video_id: uuid.UUID, current_user: Annotated[User, Depends(get_current_user)], db: AsyncSession = Depends(get_session), ) -> ChaptersResponse: """Return all chapters for a creator's video (all statuses).""" await _verify_creator_owns_video(current_user, video_id, db) stmt = ( select(KeyMoment) .where(KeyMoment.source_video_id == video_id) .order_by(KeyMoment.sort_order, KeyMoment.start_time) ) result = await db.execute(stmt) moments = result.scalars().all() logger.debug("Creator chapters for %s: %d", video_id, len(moments)) return ChaptersResponse( video_id=video_id, chapters=[ChapterMarkerRead.model_validate(m) for m in moments], ) @router.patch("/chapters/{chapter_id}", response_model=ChapterMarkerRead) async def update_chapter( chapter_id: uuid.UUID, body: ChapterUpdate, current_user: Annotated[User, Depends(get_current_user)], db: AsyncSession = Depends(get_session), ) -> ChapterMarkerRead: """Update a single chapter (title, times, status).""" if current_user.creator_id is None: raise HTTPException(status_code=403, detail="No creator profile linked") # Fetch the chapter and verify ownership via the video chapter = (await db.execute( select(KeyMoment).where(KeyMoment.id == chapter_id) )).scalar_one_or_none() if chapter is None: raise HTTPException(status_code=404, detail="Chapter not found") await _verify_creator_owns_video(current_user, chapter.source_video_id, db) # Apply partial updates update_data = body.model_dump(exclude_unset=True) if "chapter_status" in update_data: update_data["chapter_status"] = ChapterStatus(update_data["chapter_status"]) for field, value in update_data.items(): setattr(chapter, field, value) await db.commit() await db.refresh(chapter) logger.info("Updated chapter %s: %s", chapter_id, list(update_data.keys())) return ChapterMarkerRead.model_validate(chapter) @router.put("/{video_id}/chapters/reorder", response_model=ChaptersResponse) async def reorder_chapters( video_id: uuid.UUID, body: ChapterReorderRequest, current_user: Annotated[User, Depends(get_current_user)], db: AsyncSession = Depends(get_session), ) -> ChaptersResponse: """Reorder chapters for a video by setting sort_order values.""" await _verify_creator_owns_video(current_user, video_id, db) for item in body.chapters: await db.execute( update(KeyMoment) .where(KeyMoment.id == item.id, KeyMoment.source_video_id == video_id) .values(sort_order=item.sort_order) ) await db.commit() # Return updated list stmt = ( select(KeyMoment) .where(KeyMoment.source_video_id == video_id) .order_by(KeyMoment.sort_order, KeyMoment.start_time) ) result = await db.execute(stmt) moments = result.scalars().all() logger.info("Reordered %d chapters for video %s", len(body.chapters), video_id) return ChaptersResponse( video_id=video_id, chapters=[ChapterMarkerRead.model_validate(m) for m in moments], ) @router.post("/{video_id}/chapters/approve", response_model=ChaptersResponse) async def bulk_approve_chapters( video_id: uuid.UUID, body: ChapterBulkApproveRequest, current_user: Annotated[User, Depends(get_current_user)], db: AsyncSession = Depends(get_session), ) -> ChaptersResponse: """Bulk-approve chapters by ID list.""" await _verify_creator_owns_video(current_user, video_id, db) if body.chapter_ids: await db.execute( update(KeyMoment) .where( KeyMoment.id.in_(body.chapter_ids), KeyMoment.source_video_id == video_id, ) .values(chapter_status=ChapterStatus.approved) ) await db.commit() logger.info("Bulk-approved %d chapters for video %s", len(body.chapter_ids), video_id) # Return updated list stmt = ( select(KeyMoment) .where(KeyMoment.source_video_id == video_id) .order_by(KeyMoment.sort_order, KeyMoment.start_time) ) result = await db.execute(stmt) moments = result.scalars().all() return ChaptersResponse( video_id=video_id, chapters=[ChapterMarkerRead.model_validate(m) for m in moments], )