""" Chase Songs and Profile Stats Router """ from fastapi import APIRouter, Depends, HTTPException, status from sqlmodel import Session, select, func from typing import List, Optional from pydantic import BaseModel from datetime import datetime from database import get_session from models import ChaseSong, Song, Attendance, Show, Performance, Rating, User from routers.auth import get_current_user router = APIRouter(prefix="/chase", tags=["chase"]) # --- Schemas --- class ChaseSongCreate(BaseModel): song_id: int priority: int = 1 notes: Optional[str] = None class ChaseSongResponse(BaseModel): id: int song_id: int song_title: str priority: int notes: Optional[str] created_at: datetime caught_at: Optional[datetime] caught_show_id: Optional[int] caught_show_date: Optional[str] = None class ChaseSongUpdate(BaseModel): priority: Optional[int] = None notes: Optional[str] = None class ProfileStats(BaseModel): shows_attended: int unique_songs_seen: int debuts_witnessed: int heady_versions_attended: int # Top 10 rated performances top_10_performances: int total_ratings: int total_reviews: int chase_songs_count: int chase_songs_caught: int most_seen_song: Optional[str] = None most_seen_count: int = 0 # --- Routes --- @router.get("/songs", response_model=List[ChaseSongResponse]) async def get_my_chase_songs( current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Get all chase songs for the current user""" statement = ( select(ChaseSong) .where(ChaseSong.user_id == current_user.id) .order_by(ChaseSong.priority, ChaseSong.created_at.desc()) ) chase_songs = session.exec(statement).all() result = [] for cs in chase_songs: song = session.get(Song, cs.song_id) caught_show_date = None if cs.caught_show_id: show = session.get(Show, cs.caught_show_id) if show: caught_show_date = show.date.strftime("%Y-%m-%d") if show.date else None result.append(ChaseSongResponse( id=cs.id, song_id=cs.song_id, song_title=song.title if song else "Unknown", priority=cs.priority, notes=cs.notes, created_at=cs.created_at, caught_at=cs.caught_at, caught_show_id=cs.caught_show_id, caught_show_date=caught_show_date )) return result @router.post("/songs", response_model=ChaseSongResponse, status_code=status.HTTP_201_CREATED) async def add_chase_song( data: ChaseSongCreate, current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Add a song to user's chase list""" # Check if song exists song = session.get(Song, data.song_id) if not song: raise HTTPException(status_code=404, detail="Song not found") # Check if already chasing existing = session.exec( select(ChaseSong) .where(ChaseSong.user_id == current_user.id, ChaseSong.song_id == data.song_id) ).first() if existing: raise HTTPException(status_code=400, detail="Song already in chase list") chase_song = ChaseSong( user_id=current_user.id, song_id=data.song_id, priority=data.priority, notes=data.notes ) session.add(chase_song) session.commit() session.refresh(chase_song) return ChaseSongResponse( id=chase_song.id, song_id=chase_song.song_id, song_title=song.title, priority=chase_song.priority, notes=chase_song.notes, created_at=chase_song.created_at, caught_at=None, caught_show_id=None ) @router.patch("/songs/{chase_id}", response_model=ChaseSongResponse) async def update_chase_song( chase_id: int, data: ChaseSongUpdate, current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Update a chase song""" chase_song = session.get(ChaseSong, chase_id) if not chase_song or chase_song.user_id != current_user.id: raise HTTPException(status_code=404, detail="Chase song not found") if data.priority is not None: chase_song.priority = data.priority if data.notes is not None: chase_song.notes = data.notes session.add(chase_song) session.commit() session.refresh(chase_song) song = session.get(Song, chase_song.song_id) return ChaseSongResponse( id=chase_song.id, song_id=chase_song.song_id, song_title=song.title if song else "Unknown", priority=chase_song.priority, notes=chase_song.notes, created_at=chase_song.created_at, caught_at=chase_song.caught_at, caught_show_id=chase_song.caught_show_id ) @router.delete("/songs/{chase_id}", status_code=status.HTTP_204_NO_CONTENT) async def remove_chase_song( chase_id: int, current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Remove a song from chase list""" chase_song = session.get(ChaseSong, chase_id) if not chase_song or chase_song.user_id != current_user.id: raise HTTPException(status_code=404, detail="Chase song not found") session.delete(chase_song) session.commit() @router.post("/songs/{chase_id}/caught") async def mark_song_caught( chase_id: int, show_id: int, current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Mark a chase song as caught at a specific show""" chase_song = session.get(ChaseSong, chase_id) if not chase_song or chase_song.user_id != current_user.id: raise HTTPException(status_code=404, detail="Chase song not found") show = session.get(Show, show_id) if not show: raise HTTPException(status_code=404, detail="Show not found") chase_song.caught_at = datetime.utcnow() chase_song.caught_show_id = show_id session.add(chase_song) session.commit() return {"message": "Song marked as caught!"} # Profile stats endpoint @router.get("/profile/stats", response_model=ProfileStats) async def get_profile_stats( current_user: User = Depends(get_current_user), session: Session = Depends(get_session) ): """Get comprehensive profile stats for the current user""" # Shows attended shows_attended = session.exec( select(func.count(Attendance.id)) .where(Attendance.user_id == current_user.id) ).one() or 0 # Get show IDs user attended attended_show_ids = session.exec( select(Attendance.show_id) .where(Attendance.user_id == current_user.id) ).all() # Unique songs seen (performances at attended shows) unique_songs_seen = session.exec( select(func.count(func.distinct(Performance.song_id))) .where(Performance.show_id.in_(attended_show_ids) if attended_show_ids else False) ).one() or 0 # Debuts witnessed (times_played = 1 at show they attended) # This would require joining with song data - simplified for now debuts_witnessed = 0 if attended_show_ids: debuts_q = session.exec( select(Performance) .where(Performance.show_id.in_(attended_show_ids)) ).all() # Count performances where this was the debut for perf in debuts_q: # Check if this was the first performance of the song earlier_perfs = session.exec( select(func.count(Performance.id)) .join(Show, Performance.show_id == Show.id) .where(Performance.song_id == perf.song_id) .where(Show.date < session.get(Show, perf.show_id).date if session.get(Show, perf.show_id) else False) ).one() if earlier_perfs == 0: debuts_witnessed += 1 # Top performances attended (with avg rating >= 8.0) top_performances_attended = 0 heady_versions_attended = 0 if attended_show_ids: # Get average ratings for performances at attended shows perf_ratings = session.exec( select( Rating.performance_id, func.avg(Rating.score).label("avg_rating") ) .where(Rating.performance_id.isnot(None)) .group_by(Rating.performance_id) .having(func.avg(Rating.score) >= 8.0) ).all() # Filter to performances at attended shows high_rated_perf_ids = [pr[0] for pr in perf_ratings] if high_rated_perf_ids: attended_high_rated = session.exec( select(func.count(Performance.id)) .where(Performance.id.in_(high_rated_perf_ids)) .where(Performance.show_id.in_(attended_show_ids)) ).one() or 0 top_performances_attended = attended_high_rated heady_versions_attended = attended_high_rated # Total ratings/reviews total_ratings = session.exec( select(func.count(Rating.id)).where(Rating.user_id == current_user.id) ).one() or 0 total_reviews = session.exec( select(func.count()).select_from(session.exec( select(1).where(Rating.user_id == current_user.id) # placeholder ).subquery()) ).one() if False else 0 # Will fix this # Chase songs chase_count = session.exec( select(func.count(ChaseSong.id)).where(ChaseSong.user_id == current_user.id) ).one() or 0 chase_caught = session.exec( select(func.count(ChaseSong.id)) .where(ChaseSong.user_id == current_user.id) .where(ChaseSong.caught_at.isnot(None)) ).one() or 0 # Most seen song most_seen_song = None most_seen_count = 0 if attended_show_ids: song_counts = session.exec( select( Performance.song_id, func.count(Performance.id).label("count") ) .where(Performance.show_id.in_(attended_show_ids)) .group_by(Performance.song_id) .order_by(func.count(Performance.id).desc()) .limit(1) ).first() if song_counts: song = session.get(Song, song_counts[0]) if song: most_seen_song = song.title most_seen_count = song_counts[1] return ProfileStats( shows_attended=shows_attended, unique_songs_seen=unique_songs_seen, debuts_witnessed=min(debuts_witnessed, 50), # Cap to prevent timeout heady_versions_attended=heady_versions_attended, top_10_performances=top_performances_attended, total_ratings=total_ratings, total_reviews=0, # TODO: implement chase_songs_count=chase_count, chase_songs_caught=chase_caught, most_seen_song=most_seen_song, most_seen_count=most_seen_count )