feat: Restore performances, enable HeadyVersion ratings, update branding
- Updated import script to fix pagination and add idempotency - Added performances list to Song Detail API and schemas - Activated backend rating logic for performances - Updated Landing Page branding - Implemented frontend performance list display
This commit is contained in:
parent
b837d7654f
commit
65bb05d9b0
7 changed files with 202 additions and 43 deletions
|
|
@ -34,7 +34,7 @@ DEMO_USERS = [
|
|||
]
|
||||
|
||||
def fetch_json(endpoint, params=None):
|
||||
"""Fetch JSON from El Goose API with error handling"""
|
||||
"""Fetch JSON from El Goose API (Single Page)"""
|
||||
url = f"{BASE_URL}/{endpoint}.json"
|
||||
try:
|
||||
response = requests.get(url, params=params)
|
||||
|
|
@ -50,11 +50,45 @@ def fetch_json(endpoint, params=None):
|
|||
print(f"❌ Failed to fetch {endpoint}: {e}")
|
||||
return None
|
||||
|
||||
def fetch_all_json(endpoint, params=None):
|
||||
"""Fetch ALL data from El Goose API using pagination"""
|
||||
all_data = []
|
||||
page = 1
|
||||
params = params.copy() if params else {}
|
||||
|
||||
print(f" Fetching {endpoint} pages...", end="", flush=True)
|
||||
while True:
|
||||
params['page'] = page
|
||||
data = fetch_json(endpoint, params)
|
||||
if not data:
|
||||
break
|
||||
all_data.extend(data)
|
||||
print(f" {page}", end="", flush=True)
|
||||
# If less than 50 results (typical page size might be larger, but if small, likely last page)
|
||||
# Actually API returns empty list [] if page out of range usually.
|
||||
# So 'if not data' handles it.
|
||||
# Safety break if too many pages
|
||||
if page > 100:
|
||||
print(" (Safety limit reached)", end="")
|
||||
break
|
||||
page += 1
|
||||
print(" Done.")
|
||||
return all_data
|
||||
|
||||
def create_users(session):
|
||||
"""Create demo user personas"""
|
||||
print("\n📝 Creating user personas...")
|
||||
users = []
|
||||
for user_data in DEMO_USERS:
|
||||
# Check existing
|
||||
existing = session.exec(
|
||||
select(User).where(User.email == user_data["email"])
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
users.append(existing)
|
||||
continue
|
||||
|
||||
user = User(
|
||||
email=user_data["email"],
|
||||
hashed_password=pwd_context.hash("demo123"),
|
||||
|
|
@ -76,13 +110,13 @@ def create_users(session):
|
|||
users.append(user)
|
||||
|
||||
session.commit()
|
||||
print(f"✓ Created {len(users)} users")
|
||||
print(f"✓ Created/Found {len(users)} users")
|
||||
return users
|
||||
|
||||
def import_venues(session):
|
||||
"""Import all venues"""
|
||||
print("\n🏛️ Importing venues...")
|
||||
venues_data = fetch_json("venues")
|
||||
venues_data = fetch_all_json("venues")
|
||||
if not venues_data:
|
||||
return {}
|
||||
|
||||
|
|
@ -113,7 +147,7 @@ def import_venues(session):
|
|||
def import_songs(session, vertical_id):
|
||||
"""Import all songs"""
|
||||
print("\n🎵 Importing songs...")
|
||||
songs_data = fetch_json("songs")
|
||||
songs_data = fetch_all_json("songs")
|
||||
if not songs_data:
|
||||
return {}
|
||||
|
||||
|
|
@ -148,12 +182,13 @@ def import_shows(session, vertical_id, venue_map):
|
|||
"""Import all Goose shows"""
|
||||
print("\n🎤 Importing shows...")
|
||||
params = {"artist": ARTIST_ID}
|
||||
shows_data = fetch_json("shows", params)
|
||||
shows_data = fetch_all_json("shows", params)
|
||||
|
||||
if not shows_data:
|
||||
# Fallback: fetch all shows and filter
|
||||
# Fallback: fetch all shows and filter
|
||||
print(" Fetching all shows and filtering for Goose...")
|
||||
shows_data = fetch_json("shows")
|
||||
shows_data = fetch_all_json("shows")
|
||||
shows_data = [s for s in (shows_data or []) if s.get('artist_id') == ARTIST_ID]
|
||||
|
||||
if not shows_data:
|
||||
|
|
@ -186,17 +221,29 @@ def import_shows(session, vertical_id, venue_map):
|
|||
|
||||
# Create show
|
||||
show_date = datetime.strptime(s['showdate'], '%Y-%m-%d')
|
||||
show = Show(
|
||||
date=show_date,
|
||||
vertical_id=vertical_id,
|
||||
venue_id=venue_map.get(s['venue_id']),
|
||||
tour_id=tour_id,
|
||||
notes=s.get('showtitle')
|
||||
)
|
||||
session.add(show)
|
||||
session.commit()
|
||||
session.refresh(show)
|
||||
show_map[s['show_id']] = show.id
|
||||
|
||||
# Check existing show
|
||||
existing_show = session.exec(
|
||||
select(Show).where(
|
||||
Show.date == show_date,
|
||||
Show.venue_id == venue_map.get(s['venue_id'])
|
||||
)
|
||||
).first()
|
||||
|
||||
if existing_show:
|
||||
show_map[s['show_id']] = existing_show.id
|
||||
else:
|
||||
show = Show(
|
||||
date=show_date,
|
||||
vertical_id=vertical_id,
|
||||
venue_id=venue_map.get(s['venue_id']),
|
||||
tour_id=tour_id,
|
||||
notes=s.get('showtitle')
|
||||
)
|
||||
session.add(show)
|
||||
session.commit()
|
||||
session.refresh(show)
|
||||
show_map[s['show_id']] = show.id
|
||||
|
||||
if len(show_map) % 50 == 0:
|
||||
print(f" Progress: {len(show_map)} shows...")
|
||||
|
|
@ -209,7 +256,7 @@ def import_setlists(session, show_map, song_map):
|
|||
print("\n📋 Importing setlists...")
|
||||
|
||||
# Fetch all setlists (this gets all performances across all shows)
|
||||
setlists_data = fetch_json("setlists")
|
||||
setlists_data = fetch_all_json("setlists")
|
||||
if not setlists_data:
|
||||
print("❌ No setlist data found")
|
||||
return
|
||||
|
|
@ -229,16 +276,26 @@ def import_setlists(session, show_map, song_map):
|
|||
if not our_show_id or not our_song_id:
|
||||
continue
|
||||
|
||||
perf = Performance(
|
||||
show_id=our_show_id,
|
||||
song_id=our_song_id,
|
||||
position=perf_data.get('position', 0),
|
||||
set_name=perf_data.get('set'),
|
||||
segue=bool(perf_data.get('segue', 0)),
|
||||
notes=perf_data.get('notes')
|
||||
)
|
||||
session.add(perf)
|
||||
performance_count += 1
|
||||
# Check existing performance
|
||||
existing_perf = session.exec(
|
||||
select(Performance).where(
|
||||
Performance.show_id == our_show_id,
|
||||
Performance.song_id == our_song_id,
|
||||
Performance.position == perf_data.get('position', 0)
|
||||
)
|
||||
).first()
|
||||
|
||||
if not existing_perf:
|
||||
perf = Performance(
|
||||
show_id=our_show_id,
|
||||
song_id=our_song_id,
|
||||
position=perf_data.get('position', 0),
|
||||
set_name=perf_data.get('set'),
|
||||
segue=bool(perf_data.get('segue', 0)),
|
||||
notes=perf_data.get('notes')
|
||||
)
|
||||
session.add(perf)
|
||||
performance_count += 1
|
||||
|
||||
if performance_count % 100 == 0:
|
||||
session.commit()
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ class Rating(SQLModel, table=True):
|
|||
|
||||
show_id: Optional[int] = Field(default=None, foreign_key="show.id")
|
||||
song_id: Optional[int] = Field(default=None, foreign_key="song.id")
|
||||
performance_id: Optional[int] = Field(default=None, foreign_key="performance.id")
|
||||
|
||||
user: "User" = Relationship(back_populates="ratings")
|
||||
|
||||
|
|
|
|||
|
|
@ -62,8 +62,10 @@ def create_rating(
|
|||
query = query.where(Rating.show_id == rating.show_id)
|
||||
elif rating.song_id:
|
||||
query = query.where(Rating.song_id == rating.song_id)
|
||||
elif rating.performance_id:
|
||||
query = query.where(Rating.performance_id == rating.performance_id)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Must rate a show or song")
|
||||
raise HTTPException(status_code=400, detail="Must rate a show, song, or performance")
|
||||
|
||||
existing_rating = session.exec(query).first()
|
||||
if existing_rating:
|
||||
|
|
@ -85,6 +87,7 @@ def create_rating(
|
|||
def get_average_rating(
|
||||
show_id: Optional[int] = None,
|
||||
song_id: Optional[int] = None,
|
||||
performance_id: Optional[int] = None,
|
||||
session: Session = Depends(get_session)
|
||||
):
|
||||
query = select(func.avg(Rating.score))
|
||||
|
|
@ -92,8 +95,10 @@ def get_average_rating(
|
|||
query = query.where(Rating.show_id == show_id)
|
||||
elif song_id:
|
||||
query = query.where(Rating.song_id == song_id)
|
||||
elif performance_id:
|
||||
query = query.where(Rating.performance_id == performance_id)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Must specify show_id or song_id")
|
||||
raise HTTPException(status_code=400, detail="Must specify show_id, song_id, or performance_id")
|
||||
|
||||
avg = session.exec(query).first()
|
||||
return float(avg) if avg else 0.0
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import List
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlmodel import Session, select
|
||||
from database import get_session
|
||||
|
|
@ -38,12 +39,66 @@ def read_song(song_id: int, session: Session = Depends(get_session)):
|
|||
.where(EntityTag.entity_id == song_id)
|
||||
).all()
|
||||
|
||||
# Fetch performances
|
||||
# We join Show to ensure we can order by date
|
||||
from models import Show, Performance, Rating
|
||||
from sqlmodel import func
|
||||
# We need PerformanceReadWithShow from schemas
|
||||
from schemas import PerformanceReadWithShow
|
||||
|
||||
perfs = session.exec(
|
||||
select(Performance)
|
||||
.join(Show)
|
||||
.where(Performance.song_id == song_id)
|
||||
.order_by(Show.date.desc())
|
||||
).all()
|
||||
|
||||
# Calculate ratings
|
||||
perf_ids = [p.id for p in perfs]
|
||||
rating_stats = {}
|
||||
if perf_ids:
|
||||
results = session.exec(
|
||||
select(Rating.performance_id, func.avg(Rating.score), func.count(Rating.id))
|
||||
.where(Rating.performance_id.in_(perf_ids))
|
||||
.group_by(Rating.performance_id)
|
||||
).all()
|
||||
for r in results:
|
||||
rating_stats[r[0]] = {"avg": float(r[1]) if r[1] else 0.0, "count": r[2]}
|
||||
|
||||
perf_dtos = []
|
||||
for p in perfs:
|
||||
# Lazy load show/venue (could be optimized)
|
||||
venue_name = "Unknown"
|
||||
venue_city = ""
|
||||
venue_state = ""
|
||||
show_date = datetime.now()
|
||||
|
||||
if p.show:
|
||||
show_date = p.show.date
|
||||
if p.show.venue:
|
||||
venue_name = p.show.venue.name
|
||||
venue_city = p.show.venue.city
|
||||
venue_state = p.show.venue.state
|
||||
|
||||
stats = rating_stats.get(p.id, {"avg": 0.0, "count": 0})
|
||||
|
||||
perf_dtos.append(PerformanceReadWithShow(
|
||||
**p.model_dump(),
|
||||
show_date=show_date,
|
||||
venue_name=venue_name,
|
||||
venue_city=venue_city,
|
||||
venue_state=venue_state,
|
||||
avg_rating=stats["avg"],
|
||||
total_reviews=stats["count"]
|
||||
))
|
||||
|
||||
# Merge song data with stats
|
||||
song_with_stats = SongReadWithStats(
|
||||
**song.model_dump(),
|
||||
**stats
|
||||
)
|
||||
song_with_stats.tags = tags
|
||||
song_with_stats.performances = perf_dtos
|
||||
return song_with_stats
|
||||
|
||||
@router.patch("/{song_id}", response_model=SongRead)
|
||||
|
|
|
|||
|
|
@ -57,10 +57,7 @@ class SongRead(SongBase):
|
|||
id: int
|
||||
tags: List["TagRead"] = []
|
||||
|
||||
class SongReadWithStats(SongRead):
|
||||
times_played: int
|
||||
gap: int
|
||||
last_played: Optional[datetime] = None
|
||||
|
||||
|
||||
class SongUpdate(SQLModel):
|
||||
title: Optional[str] = None
|
||||
|
|
@ -92,6 +89,20 @@ class PerformanceRead(PerformanceBase):
|
|||
song: Optional["SongRead"] = None
|
||||
nicknames: List["PerformanceNicknameRead"] = []
|
||||
|
||||
class PerformanceReadWithShow(PerformanceRead):
|
||||
show_date: datetime
|
||||
venue_name: str
|
||||
venue_city: str
|
||||
venue_state: Optional[str] = None
|
||||
avg_rating: Optional[float] = 0.0
|
||||
total_reviews: Optional[int] = 0
|
||||
|
||||
class SongReadWithStats(SongRead):
|
||||
times_played: int
|
||||
gap: int
|
||||
last_played: Optional[datetime] = None
|
||||
performances: List[PerformanceReadWithShow] = []
|
||||
|
||||
class PerformanceDetailRead(PerformanceRead):
|
||||
show: Optional["ShowRead"] = None
|
||||
previous_performance_id: Optional[int] = None
|
||||
|
|
@ -210,6 +221,7 @@ class RatingBase(SQLModel):
|
|||
score: int
|
||||
show_id: Optional[int] = None
|
||||
song_id: Optional[int] = None
|
||||
performance_id: Optional[int] = None
|
||||
|
||||
class RatingCreate(RatingBase):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ export default function Home() {
|
|||
{/* Hero Section */}
|
||||
<section className="flex flex-col items-center gap-4 py-12 text-center md:py-20 bg-gradient-to-b from-background to-accent/20 rounded-3xl border">
|
||||
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
||||
Elmeg Demo
|
||||
Elmeg Archive
|
||||
</h1>
|
||||
<p className="max-w-[600px] text-lg text-muted-foreground">
|
||||
Explore the "vibey" data we just generated.
|
||||
The ultimate community archive for Goose history.
|
||||
<br />
|
||||
36 Personas. Wiki-linked Reviews. Interconnected Data.
|
||||
Discover shows, rate performances, and connect with fans.
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
<Link href="/leaderboards">
|
||||
|
|
@ -60,7 +60,7 @@ export default function Home() {
|
|||
<Link href="/leaderboards" className="block p-6 border rounded-xl hover:bg-accent transition-colors">
|
||||
<Users className="h-8 w-8 mb-2 text-purple-500" />
|
||||
<h3 className="font-bold">Community</h3>
|
||||
<p className="text-sm text-muted-foreground">Meet the 36 personas</p>
|
||||
<p className="text-sm text-muted-foreground">Join the conversation</p>
|
||||
</Link>
|
||||
<Link href="/leaderboards" className="block p-6 border rounded-xl hover:bg-accent transition-colors">
|
||||
<Trophy className="h-8 w-8 mb-2 text-yellow-500" />
|
||||
|
|
|
|||
|
|
@ -94,11 +94,40 @@ export default async function SongDetailPage({ params }: { params: Promise<{ id:
|
|||
<CardTitle>Performance History</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground text-sm">Performance history coming soon...</p>
|
||||
{/*
|
||||
We need to fetch performances list here.
|
||||
For now, we leave a placeholder.
|
||||
*/}
|
||||
{song.performances && song.performances.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{song.performances.map((perf: any) => (
|
||||
<div key={perf.id} className="flex items-center justify-between border-b pb-4 last:border-0 last:pb-0">
|
||||
<div className="space-y-1">
|
||||
<Link href={`/shows/${perf.show_id}`} className="font-medium hover:underline text-primary">
|
||||
{new Date(perf.show_date).toLocaleDateString(undefined, {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</Link>
|
||||
<div className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<span>
|
||||
{perf.venue_name}, {perf.venue_city} {perf.venue_state}
|
||||
</span>
|
||||
</div>
|
||||
{perf.notes && (
|
||||
<p className="text-sm italic text-muted-foreground/80">"{perf.notes}"</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{/* Placeholder for Rating UI */}
|
||||
<span className="text-xs font-semibold bg-secondary px-2 py-1 rounded-full text-secondary-foreground">
|
||||
{perf.avg_rating > 0 ? perf.avg_rating.toFixed(1) : "Unrated"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">No performances recorded.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue