Files

202 lines
6.7 KiB
Python

import random
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from .. import auth, schemas
from ..config import settings
from ..database import get_db
from ..jobs import enqueue_generation
from ..models import Like, Song, User
router = APIRouter(prefix="/songs", tags=["songs"])
def _serialize(song: Song, current_user: User | None, liked_ids: set[uuid.UUID] | None = None) -> schemas.SongOut:
liked = False
if current_user is not None:
if liked_ids is not None:
liked = song.id in liked_ids
return schemas.SongOut(
id=song.id,
user_id=song.user_id,
owner_display_name=song.owner.display_name if song.owner else None,
title=song.title,
prompt=song.prompt,
lyrics=song.lyrics,
mode=song.mode,
status=song.status,
error_message=song.error_message,
duration_seconds=song.duration_seconds,
cover_seed=song.cover_seed,
is_public=song.is_public,
like_count=song.like_count,
liked_by_me=liked,
audio_url=f"/media/{song.audio_path}" if song.audio_path else None,
created_at=song.created_at,
)
@router.post("", response_model=schemas.SongOut, status_code=status.HTTP_201_CREATED)
def create_song(
payload: schemas.SongCreateRequest,
current_user: User = Depends(auth.get_current_user),
db: Session = Depends(get_db),
):
if payload.mode == "vocals" and not payload.lyrics:
raise HTTPException(status_code=422, detail="Lyrics are required when mode is 'vocals'")
song = Song(
user_id=current_user.id,
title=payload.title,
prompt=payload.prompt,
lyrics=payload.lyrics,
mode=payload.mode,
is_public=payload.is_public,
status="pending",
cover_seed=random.randint(0, 2_147_483_647),
)
db.add(song)
db.commit()
db.refresh(song)
enqueue_generation(song.id)
return _serialize(song, current_user, set())
@router.get("/mine", response_model=schemas.SongListResponse)
def my_songs(
current_user: User = Depends(auth.get_current_user),
db: Session = Depends(get_db),
):
songs = (
db.query(Song)
.filter(Song.user_id == current_user.id)
.order_by(Song.created_at.desc())
.all()
)
liked_ids = _liked_song_ids(db, current_user, [s.id for s in songs])
return schemas.SongListResponse(items=[_serialize(s, current_user, liked_ids) for s in songs], total=len(songs))
@router.get("/feed", response_model=schemas.SongListResponse)
def feed(
limit: int = Query(default=30, le=100),
offset: int = Query(default=0, ge=0),
current_user: User | None = Depends(auth.get_current_user_optional),
db: Session = Depends(get_db),
):
base_query = db.query(Song).filter(Song.is_public.is_(True), Song.status == "completed")
total = base_query.with_entities(func.count(Song.id)).scalar() or 0
songs = base_query.order_by(Song.created_at.desc()).offset(offset).limit(limit).all()
liked_ids = _liked_song_ids(db, current_user, [s.id for s in songs]) if current_user else set()
return schemas.SongListResponse(items=[_serialize(s, current_user, liked_ids) for s in songs], total=total)
@router.get("/{song_id}", response_model=schemas.SongOut)
def get_song(
song_id: uuid.UUID,
current_user: User | None = Depends(auth.get_current_user_optional),
db: Session = Depends(get_db),
):
song = db.get(Song, song_id)
if not song:
raise HTTPException(status_code=404, detail="Song not found")
if not song.is_public and (current_user is None or song.user_id != current_user.id):
raise HTTPException(status_code=404, detail="Song not found")
liked_ids = _liked_song_ids(db, current_user, [song.id]) if current_user else set()
return _serialize(song, current_user, liked_ids)
@router.patch("/{song_id}", response_model=schemas.SongOut)
def update_song(
song_id: uuid.UUID,
payload: schemas.SongUpdateRequest,
current_user: User = Depends(auth.get_current_user),
db: Session = Depends(get_db),
):
song = db.get(Song, song_id)
if not song or song.user_id != current_user.id:
raise HTTPException(status_code=404, detail="Song not found")
if payload.title is not None:
song.title = payload.title
if payload.is_public is not None:
song.is_public = payload.is_public
db.commit()
db.refresh(song)
liked_ids = _liked_song_ids(db, current_user, [song.id])
return _serialize(song, current_user, liked_ids)
@router.delete("/{song_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_song(
song_id: uuid.UUID,
current_user: User = Depends(auth.get_current_user),
db: Session = Depends(get_db),
):
song = db.get(Song, song_id)
if not song or song.user_id != current_user.id:
raise HTTPException(status_code=404, detail="Song not found")
db.delete(song)
db.commit()
@router.post("/{song_id}/like", response_model=schemas.SongOut)
def like_song(
song_id: uuid.UUID,
current_user: User = Depends(auth.get_current_user),
db: Session = Depends(get_db),
):
song = db.get(Song, song_id)
if not song or not song.is_public:
raise HTTPException(status_code=404, detail="Song not found")
existing = db.query(Like).filter(Like.user_id == current_user.id, Like.song_id == song_id).first()
if not existing:
db.add(Like(user_id=current_user.id, song_id=song_id))
song.like_count += 1
db.commit()
db.refresh(song)
liked_ids = _liked_song_ids(db, current_user, [song.id])
return _serialize(song, current_user, liked_ids)
@router.delete("/{song_id}/like", response_model=schemas.SongOut)
def unlike_song(
song_id: uuid.UUID,
current_user: User = Depends(auth.get_current_user),
db: Session = Depends(get_db),
):
song = db.get(Song, song_id)
if not song:
raise HTTPException(status_code=404, detail="Song not found")
existing = db.query(Like).filter(Like.user_id == current_user.id, Like.song_id == song_id).first()
if existing:
db.delete(existing)
song.like_count = max(0, song.like_count - 1)
db.commit()
db.refresh(song)
liked_ids = _liked_song_ids(db, current_user, [song.id])
return _serialize(song, current_user, liked_ids)
def _liked_song_ids(db: Session, current_user: User | None, song_ids: list[uuid.UUID]) -> set[uuid.UUID]:
if not current_user or not song_ids:
return set()
rows = (
db.execute(
select(Like.song_id).where(Like.user_id == current_user.id, Like.song_id.in_(song_ids))
)
.scalars()
.all()
)
return set(rows)