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)