Files
anime-qize/backend/app/routers/openings.py

224 lines
6.6 KiB
Python
Raw Normal View History

2025-12-30 17:37:14 +03:00
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