From 65bb05d9b02ef267c719fb3f41a7867ce2425350 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sat, 20 Dec 2025 02:05:45 -0800 Subject: [PATCH] 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 --- backend/import_elgoose.py | 113 +++++++++++++++++++++++-------- backend/models.py | 1 + backend/routers/social.py | 9 ++- backend/routers/songs.py | 55 +++++++++++++++ backend/schemas.py | 20 ++++-- frontend/app/page.tsx | 8 +-- frontend/app/songs/[id]/page.tsx | 39 +++++++++-- 7 files changed, 202 insertions(+), 43 deletions(-) diff --git a/backend/import_elgoose.py b/backend/import_elgoose.py index 9b284d7..cc89a0d 100644 --- a/backend/import_elgoose.py +++ b/backend/import_elgoose.py @@ -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() diff --git a/backend/models.py b/backend/models.py index 522f7cb..ebe8f0d 100644 --- a/backend/models.py +++ b/backend/models.py @@ -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") diff --git a/backend/routers/social.py b/backend/routers/social.py index ccb1d4a..c0a0027 100644 --- a/backend/routers/social.py +++ b/backend/routers/social.py @@ -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 diff --git a/backend/routers/songs.py b/backend/routers/songs.py index 233db68..052d0c5 100644 --- a/backend/routers/songs.py +++ b/backend/routers/songs.py @@ -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) diff --git a/backend/schemas.py b/backend/schemas.py index 09c9e3c..a2af5ab 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -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 diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 3dfe830..74ef0a6 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -9,12 +9,12 @@ export default function Home() { {/* Hero Section */}

- Elmeg Demo + Elmeg Archive

- Explore the "vibey" data we just generated. + The ultimate community archive for Goose history.
- 36 Personas. Wiki-linked Reviews. Interconnected Data. + Discover shows, rate performances, and connect with fans.

@@ -60,7 +60,7 @@ export default function Home() {

Community

-

Meet the 36 personas

+

Join the conversation

diff --git a/frontend/app/songs/[id]/page.tsx b/frontend/app/songs/[id]/page.tsx index 079ab43..1911abb 100644 --- a/frontend/app/songs/[id]/page.tsx +++ b/frontend/app/songs/[id]/page.tsx @@ -94,11 +94,40 @@ export default async function SongDetailPage({ params }: { params: Promise<{ id: Performance History -

Performance history coming soon...

- {/* - We need to fetch performances list here. - For now, we leave a placeholder. - */} + {song.performances && song.performances.length > 0 ? ( +
+ {song.performances.map((perf: any) => ( +
+
+ + {new Date(perf.show_date).toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + })} + +
+ + {perf.venue_name}, {perf.venue_city} {perf.venue_state} + +
+ {perf.notes && ( +

"{perf.notes}"

+ )} +
+
+ {/* Placeholder for Rating UI */} + + {perf.avg_rating > 0 ? perf.avg_rating.toFixed(1) : "Unrated"} + +
+
+ ))} +
+ ) : ( +

No performances recorded.

+ )}