- 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
327 lines
11 KiB
Python
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
|
|
)
|