fediversion/backend/routers/musicians.py
fullsizemalt 159cbc853c
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
feat: Enhance musician API with cross-band sit-in tracking
- Add sit_in_summary to GET /musicians/{slug}
- Add stats: total_bands, current_bands, total_sit_ins
- Include song_title, show_date, vertical in guest appearances
- Add seed_musicians.py for initial musician data
2025-12-28 16:30:38 -08:00

173 lines
5.7 KiB
Python

"""
Musicians Router - API endpoints for managing musicians and band memberships.
"""
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session, select
from pydantic import BaseModel
from database import get_session
from models import Musician, BandMembership, PerformanceGuest, Artist, Performance
from slugify import generate_slug as slugify
router = APIRouter(prefix="/musicians", tags=["musicians"])
# --- Schemas ---
class MusicianRead(BaseModel):
id: int
name: str
slug: str
bio: Optional[str] = None
image_url: Optional[str] = None
primary_instrument: Optional[str] = None
class MusicianCreate(BaseModel):
name: str
bio: Optional[str] = None
image_url: Optional[str] = None
primary_instrument: Optional[str] = None
class BandMembershipRead(BaseModel):
id: int
musician_id: int
artist_id: int
artist_name: Optional[str] = None
role: Optional[str] = None
start_date: Optional[str] = None
end_date: Optional[str] = None
class GuestAppearanceRead(BaseModel):
id: int
performance_id: int
song_title: Optional[str] = None
show_date: Optional[str] = None
instrument: Optional[str] = None
# --- Public Endpoints ---
@router.get("", response_model=List[MusicianRead])
def list_musicians(
search: Optional[str] = None,
limit: int = 50,
session: Session = Depends(get_session)
):
"""List all musicians"""
query = select(Musician)
if search:
query = query.where(Musician.name.icontains(search))
query = query.order_by(Musician.name).limit(limit)
return session.exec(query).all()
@router.get("/{slug}")
def get_musician(slug: str, session: Session = Depends(get_session)):
"""Get musician details with band memberships and guest appearances"""
from models import Show, Song, Vertical
musician = session.exec(select(Musician).where(Musician.slug == slug)).first()
if not musician:
raise HTTPException(status_code=404, detail="Musician not found")
# Get band memberships
memberships = session.exec(
select(BandMembership).where(BandMembership.musician_id == musician.id)
).all()
bands = []
primary_band_ids = set()
for m in memberships:
artist = session.get(Artist, m.artist_id)
primary_band_ids.add(m.artist_id)
bands.append({
"id": m.id,
"artist_id": m.artist_id,
"artist_name": artist.name if artist else None,
"artist_slug": artist.slug if artist else None,
"role": m.role,
"start_date": str(m.start_date) if m.start_date else None,
"end_date": str(m.end_date) if m.end_date else None,
"is_current": m.end_date is None,
})
# Get guest appearances with full context
appearances = session.exec(
select(PerformanceGuest).where(PerformanceGuest.musician_id == musician.id)
).all()
guests = []
sit_in_verticals = {} # Track which bands they've sat in with
for g in appearances:
perf = session.get(Performance, g.performance_id)
if not perf:
continue
show = session.get(Show, perf.show_id) if perf else None
song = session.get(Song, perf.song_id) if perf else None
vertical = session.get(Vertical, show.vertical_id) if show else None
if vertical:
if vertical.id not in sit_in_verticals:
sit_in_verticals[vertical.id] = {
"vertical_id": vertical.id,
"vertical_name": vertical.name,
"vertical_slug": vertical.slug,
"count": 0
}
sit_in_verticals[vertical.id]["count"] += 1
guests.append({
"id": g.id,
"performance_id": g.performance_id,
"performance_slug": perf.slug if perf else None,
"song_title": song.title if song else None,
"show_date": str(show.date.date()) if show and show.date else None,
"vertical_name": vertical.name if vertical else None,
"vertical_slug": vertical.slug if vertical else None,
"instrument": g.instrument,
})
return {
"musician": {
"id": musician.id,
"name": musician.name,
"slug": musician.slug,
"bio": musician.bio,
"image_url": musician.image_url,
"primary_instrument": musician.primary_instrument,
},
"bands": bands,
"guest_appearances": guests,
"sit_in_summary": list(sit_in_verticals.values()),
"stats": {
"total_bands": len(bands),
"current_bands": len([b for b in bands if b.get("is_current")]),
"total_sit_ins": len(guests),
"bands_sat_in_with": len(sit_in_verticals),
}
}
# --- Admin Endpoints (for now, no auth check - can be added later) ---
@router.post("", response_model=MusicianRead)
def create_musician(
musician_data: MusicianCreate,
session: Session = Depends(get_session)
):
"""Create a new musician"""
slug = slugify(musician_data.name)
# Check for existing
existing = session.exec(select(Musician).where(Musician.slug == slug)).first()
if existing:
raise HTTPException(status_code=400, detail="Musician with this name already exists")
musician = Musician(
name=musician_data.name,
slug=slug,
bio=musician_data.bio,
image_url=musician_data.image_url,
primary_instrument=musician_data.primary_instrument,
)
session.add(musician)
session.commit()
session.refresh(musician)
return musician