fediversion/backend/routers/chase.py
fullsizemalt b4cddf41ea feat: Initialize Fediversion multi-band platform
- Fork elmeg-demo codebase for multi-band support
- Add data importer infrastructure with base class
- Create band-specific importers:
  - phish.py: Phish.net API v5
  - grateful_dead.py: Grateful Stats API
  - setlistfm.py: Dead & Company, Billy Strings (Setlist.fm)
- Add spec-kit configuration for Gemini
- Update README with supported bands and architecture
2025-12-28 12:39:28 -08:00

327 lines
11 KiB
Python

"""
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
)