Files
anime-qize/backend/app/routers/openings.py
2025-12-30 17:37:14 +03:00

224 lines
6.6 KiB
Python

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