202 lines
6.7 KiB
Python
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)
|