fediversion/backend/routers/festivals.py
fullsizemalt b38da24055
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
feat: Cross-band milestone - Festivals, Playlists, Musicians, Venue Timeline
Sprint 2: Added 54 musicians with 78 band memberships
- Phish, Widespread Panic, Umphreys McGee core members
- Notable sit-in artists (Karl Denson, Branford Marsalis, Derek/Susan Trucks)
- Toy Factory Project supergroup (Oteil, Marcus King, Charlie Starr)

Sprint 4: Festival entity for multi-band events
- Festival and ShowFestival models
- /festivals API with list, detail, by-band endpoints

Sprint 5: User Playlists for curated collections
- UserPlaylist and PlaylistPerformance models
- Full CRUD /playlists API

Sprint 6: Venue Timeline endpoint
- /venues/{slug}/timeline for chronological cross-band history

Blockers (need production data):
- Venue linking script (no venues in local DB)
- Canon song linking (no songs in local DB)
2025-12-28 23:34:05 -08:00

168 lines
5.1 KiB
Python

"""
Festival API endpoints for multi-band festival discovery.
"""
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 Festival, ShowFestival, Show, Vertical, Venue
router = APIRouter(prefix="/festivals", tags=["festivals"])
class FestivalRead(BaseModel):
id: int
name: str
slug: str
year: Optional[int]
start_date: Optional[str]
end_date: Optional[str]
website_url: Optional[str]
description: Optional[str]
class FestivalShowRead(BaseModel):
show_id: int
show_slug: Optional[str]
show_date: str
vertical_name: str
vertical_slug: str
stage: Optional[str]
set_time: Optional[str]
class FestivalDetailRead(BaseModel):
festival: FestivalRead
shows: List[FestivalShowRead]
bands_count: int
@router.get("/", response_model=List[FestivalRead])
def list_festivals(
year: Optional[int] = None,
limit: int = Query(default=50, le=100),
offset: int = 0,
session: Session = Depends(get_session)
):
"""List all festivals, optionally filtered by year"""
query = select(Festival)
if year:
query = query.where(Festival.year == year)
query = query.order_by(Festival.year.desc(), Festival.name).offset(offset).limit(limit)
festivals = session.exec(query).all()
return [
FestivalRead(
id=f.id,
name=f.name,
slug=f.slug,
year=f.year,
start_date=f.start_date.strftime("%Y-%m-%d") if f.start_date else None,
end_date=f.end_date.strftime("%Y-%m-%d") if f.end_date else None,
website_url=f.website_url,
description=f.description
)
for f in festivals
]
@router.get("/{slug}", response_model=FestivalDetailRead)
def get_festival(slug: str, session: Session = Depends(get_session)):
"""Get festival details with all shows across bands"""
festival = session.exec(
select(Festival).where(Festival.slug == slug)
).first()
if not festival:
raise HTTPException(status_code=404, detail="Festival not found")
# Get all shows at this festival
show_festivals = session.exec(
select(ShowFestival).where(ShowFestival.festival_id == festival.id)
).all()
shows = []
bands_seen = set()
for sf in show_festivals:
show = session.get(Show, sf.show_id)
if show:
vertical = session.get(Vertical, show.vertical_id)
if vertical:
bands_seen.add(vertical.id)
shows.append(FestivalShowRead(
show_id=show.id,
show_slug=show.slug,
show_date=show.date.strftime("%Y-%m-%d") if show.date else "Unknown",
vertical_name=vertical.name if vertical else "Unknown",
vertical_slug=vertical.slug if vertical else "unknown",
stage=sf.stage,
set_time=sf.set_time
))
# Sort by date
shows.sort(key=lambda x: x.show_date)
return FestivalDetailRead(
festival=FestivalRead(
id=festival.id,
name=festival.name,
slug=festival.slug,
year=festival.year,
start_date=festival.start_date.strftime("%Y-%m-%d") if festival.start_date else None,
end_date=festival.end_date.strftime("%Y-%m-%d") if festival.end_date else None,
website_url=festival.website_url,
description=festival.description
),
shows=shows,
bands_count=len(bands_seen)
)
@router.get("/by-band/{vertical_slug}")
def get_festivals_by_band(vertical_slug: str, session: Session = Depends(get_session)):
"""Get all festivals a band has played"""
vertical = session.exec(
select(Vertical).where(Vertical.slug == vertical_slug)
).first()
if not vertical:
raise HTTPException(status_code=404, detail="Band not found")
# Get shows for this vertical that are linked to festivals
shows = session.exec(
select(Show).where(Show.vertical_id == vertical.id)
).all()
show_ids = [s.id for s in shows]
if not show_ids:
return []
# Get festival links
show_festivals = session.exec(
select(ShowFestival).where(ShowFestival.show_id.in_(show_ids))
).all()
festival_ids = list(set(sf.festival_id for sf in show_festivals))
if not festival_ids:
return []
festivals = session.exec(
select(Festival).where(Festival.id.in_(festival_ids))
).all()
return [
FestivalRead(
id=f.id,
name=f.name,
slug=f.slug,
year=f.year,
start_date=f.start_date.strftime("%Y-%m-%d") if f.start_date else None,
end_date=f.end_date.strftime("%Y-%m-%d") if f.end_date else None,
website_url=f.website_url,
description=f.description
)
for f in sorted(festivals, key=lambda x: x.year or 0, reverse=True)
]