from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from ..database import get_db from ..db_models import Opening, OpeningPoster from ..schemas import ( OpeningCreate, OpeningUpdate, OpeningResponse, OpeningListResponse, OpeningPosterResponse, AddPosterRequest, ) router = APIRouter(prefix="/openings", tags=["openings"]) @router.get("", response_model=OpeningListResponse) async def list_openings( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=500), search: Optional[str] = None, db: AsyncSession = Depends(get_db), ): """List all openings with pagination and search.""" query = select(Opening).options(selectinload(Opening.posters)) if search: query = query.where(Opening.anime_name.ilike(f"%{search}%")) # Count total count_query = select(func.count(Opening.id)) if search: count_query = count_query.where(Opening.anime_name.ilike(f"%{search}%")) total_result = await db.execute(count_query) total = total_result.scalar() # Get items query = query.order_by(Opening.anime_name, Opening.op_number) query = query.offset(skip).limit(limit) result = await db.execute(query) openings = result.scalars().all() return OpeningListResponse(openings=openings, total=total) @router.get("/{opening_id}", response_model=OpeningResponse) async def get_opening(opening_id: int, db: AsyncSession = Depends(get_db)): """Get a single opening by ID.""" query = select(Opening).options(selectinload(Opening.posters)).where(Opening.id == opening_id) result = await db.execute(query) opening = result.scalar_one_or_none() if not opening: raise HTTPException(status_code=404, detail="Opening not found") return opening @router.post("", response_model=OpeningResponse, status_code=201) async def create_opening(data: OpeningCreate, db: AsyncSession = Depends(get_db)): """Create a new opening.""" opening = Opening( anime_name=data.anime_name, op_number=data.op_number, song_name=data.song_name, audio_file=data.audio_file, ) # Add posters for i, poster_file in enumerate(data.poster_files): poster = OpeningPoster( poster_file=poster_file, is_default=(i == 0) # First poster is default ) opening.posters.append(poster) db.add(opening) await db.commit() await db.refresh(opening) # Reload with posters query = select(Opening).options(selectinload(Opening.posters)).where(Opening.id == opening.id) result = await db.execute(query) return result.scalar_one() @router.put("/{opening_id}", response_model=OpeningResponse) async def update_opening( opening_id: int, data: OpeningUpdate, db: AsyncSession = Depends(get_db), ): """Update an opening.""" query = select(Opening).options(selectinload(Opening.posters)).where(Opening.id == opening_id) result = await db.execute(query) opening = result.scalar_one_or_none() if not opening: raise HTTPException(status_code=404, detail="Opening not found") # Update fields if data.anime_name is not None: opening.anime_name = data.anime_name if data.op_number is not None: opening.op_number = data.op_number if data.song_name is not None: opening.song_name = data.song_name if data.audio_file is not None: opening.audio_file = data.audio_file await db.commit() await db.refresh(opening) return opening @router.delete("/{opening_id}", status_code=204) async def delete_opening(opening_id: int, db: AsyncSession = Depends(get_db)): """Delete an opening.""" query = select(Opening).where(Opening.id == opening_id) result = await db.execute(query) opening = result.scalar_one_or_none() if not opening: raise HTTPException(status_code=404, detail="Opening not found") await db.delete(opening) await db.commit() # ============== Poster Management ============== @router.post("/{opening_id}/posters", response_model=OpeningPosterResponse, status_code=201) async def add_poster( opening_id: int, data: AddPosterRequest, db: AsyncSession = Depends(get_db), ): """Add a poster to an opening.""" query = select(Opening).where(Opening.id == opening_id) result = await db.execute(query) opening = result.scalar_one_or_none() if not opening: raise HTTPException(status_code=404, detail="Opening not found") poster = OpeningPoster( opening_id=opening_id, poster_file=data.poster_file, is_default=data.is_default, ) # If this is set as default, unset others if data.is_default: await db.execute( select(OpeningPoster) .where(OpeningPoster.opening_id == opening_id) ) posters_result = await db.execute( select(OpeningPoster).where(OpeningPoster.opening_id == opening_id) ) for p in posters_result.scalars(): p.is_default = False db.add(poster) await db.commit() await db.refresh(poster) return poster @router.delete("/{opening_id}/posters/{poster_id}", status_code=204) async def remove_poster( opening_id: int, poster_id: int, db: AsyncSession = Depends(get_db), ): """Remove a poster from an opening.""" query = select(OpeningPoster).where( OpeningPoster.id == poster_id, OpeningPoster.opening_id == opening_id, ) result = await db.execute(query) poster = result.scalar_one_or_none() if not poster: raise HTTPException(status_code=404, detail="Poster not found") await db.delete(poster) await db.commit() @router.post("/{opening_id}/posters/{poster_id}/set-default", response_model=OpeningPosterResponse) async def set_default_poster( opening_id: int, poster_id: int, db: AsyncSession = Depends(get_db), ): """Set a poster as the default for an opening.""" # Get all posters for this opening query = select(OpeningPoster).where(OpeningPoster.opening_id == opening_id) result = await db.execute(query) posters = result.scalars().all() target_poster = None for poster in posters: if poster.id == poster_id: poster.is_default = True target_poster = poster else: poster.is_default = False if not target_poster: raise HTTPException(status_code=404, detail="Poster not found") await db.commit() await db.refresh(target_poster) return target_poster