From 8fa04e96905d06adad5b88a71e1b3c540d6d06ed Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sat, 20 Dec 2025 21:16:25 -0800 Subject: [PATCH] feat: Add container max-width, revamp homepage with real data, add setlist import scripts --- backend/fast_import_setlists.py | 128 +++++++++++++++++ backend/import_by_date.py | 103 ++++++++++++++ backend/import_per_show.py | 131 ++++++++++++++++++ frontend/app/globals.css | 24 ++++ frontend/app/page.tsx | 236 +++++++++++++++++++++++++++----- 5 files changed, 587 insertions(+), 35 deletions(-) create mode 100644 backend/fast_import_setlists.py create mode 100644 backend/import_by_date.py create mode 100644 backend/import_per_show.py diff --git a/backend/fast_import_setlists.py b/backend/fast_import_setlists.py new file mode 100644 index 0000000..466aebf --- /dev/null +++ b/backend/fast_import_setlists.py @@ -0,0 +1,128 @@ +""" +Fast Setlist Importer - Skips validation checks for speed +Run after main import if it times out +""" +import requests +from datetime import datetime +from sqlmodel import Session, select +from database import engine +from models import Show, Song, Performance + +BASE_URL = "https://elgoose.net/api/v2" + +def fetch_json(endpoint, params=None): + url = f"{BASE_URL}/{endpoint}.json" + try: + response = requests.get(url, params=params, timeout=30) + response.raise_for_status() + data = response.json() + if data.get('error') == 1: + return None + return data.get('data', []) + except Exception as e: + print(f"Error: {e}") + return None + +def main(): + print("FAST SETLIST IMPORTER") + print("=" * 40) + + with Session(engine) as session: + # Build lookup maps + print("Building show map...") + shows = session.exec(select(Show)).all() + # Map API show_id to our show_id + # We need the API show_id. The 'notes' field might have showtitle but not API id. + # Problem: We don't store API show_id in our DB! + # We need to re-fetch shows and match by date+venue_id + + show_map = {} # api_show_id -> our_show_id + song_map = {} # api_song_id -> our_song_id + + # Fetch shows from API to build map + print("Fetching API shows to build map...") + page = 1 + while True: + data = fetch_json("shows", {"artist": 1, "page": page}) + if not data: + break + for s in data: + api_show_id = s['show_id'] + show_date = datetime.strptime(s['showdate'], '%Y-%m-%d') + # Find matching show in our DB + our_show = session.exec( + select(Show).where(Show.date == show_date) + ).first() + if our_show: + show_map[api_show_id] = our_show.id + page += 1 + if page > 50: + break + print(f"Mapped {len(show_map)} shows") + + # Build song map + print("Building song map...") + songs_data = fetch_json("songs") + if songs_data: + for s in songs_data: + api_song_id = s['id'] + our_song = session.exec( + select(Song).where(Song.title == s['name']) + ).first() + if our_song: + song_map[api_song_id] = our_song.id + print(f"Mapped {len(song_map)} songs") + + # Get existing performances to skip + print("Loading existing performances...") + existing = set() + perfs = session.exec(select(Performance.show_id, Performance.song_id, Performance.position)).all() + for p in perfs: + existing.add((p[0], p[1], p[2])) + print(f"Found {len(existing)} existing performances") + + # Import setlists page by page + print("\\nImporting setlists...") + page = 1 + total_added = 0 + + while True: + data = fetch_json("setlists", {"page": page}) + if not data: + print(f"No data on page {page}, done.") + break + + added_this_page = 0 + for perf_data in data: + our_show_id = show_map.get(perf_data.get('show_id')) + our_song_id = song_map.get(perf_data.get('song_id')) + position = perf_data.get('position', 0) + + if not our_show_id or not our_song_id: + continue + + key = (our_show_id, our_song_id, position) + if key in existing: + continue + + perf = Performance( + show_id=our_show_id, + song_id=our_song_id, + position=position, + set_name=perf_data.get('set'), + segue=bool(perf_data.get('segue', 0)), + notes=perf_data.get('notes') + ) + session.add(perf) + existing.add(key) + added_this_page += 1 + total_added += 1 + + session.commit() + print(f"Page {page}: +{added_this_page} ({total_added} total)") + page += 1 + + print(f"\\n✓ Added {total_added} performances") + +if __name__ == "__main__": + main() diff --git a/backend/import_by_date.py b/backend/import_by_date.py new file mode 100644 index 0000000..9810aaf --- /dev/null +++ b/backend/import_by_date.py @@ -0,0 +1,103 @@ +""" +Direct Date-Based Setlist Importer +Matches setlists to shows by date +""" +import requests +from datetime import datetime +from sqlmodel import Session, select +from database import engine +from models import Show, Song, Performance + +BASE_URL = "https://elgoose.net/api/v2" + +def main(): + print("DATE-BASED SETLIST IMPORTER") + print("=" * 40) + + with Session(engine) as session: + # Build show map by date + shows = session.exec(select(Show)).all() + show_by_date = {} + for s in shows: + date_str = s.date.strftime('%Y-%m-%d') + show_by_date[date_str] = s.id + print(f"Mapped {len(show_by_date)} shows by date") + + # Build song map by title (lowercase) + songs = session.exec(select(Song)).all() + song_map = {s.title.lower().strip(): s.id for s in songs} + print(f"Mapped {len(song_map)} songs") + + # Get existing performances for dedup + existing = set() + perfs = session.exec( + select(Performance.show_id, Performance.song_id, Performance.position) + ).all() + for p in perfs: + existing.add((p[0], p[1], p[2])) + print(f"Found {len(existing)} existing performances") + + # Fetch setlists page by page + print("\\nImporting setlists...") + page = 1 + total_added = 0 + + while page <= 200: # Safety limit + print(f" Page {page}...", end="", flush=True) + + url = f"{BASE_URL}/setlists.json" + try: + resp = requests.get(url, params={"page": page}, timeout=60) + data = resp.json().get('data', []) + except Exception as e: + print(f" Error: {e}") + break + + if not data: + print(" Done (no data)") + break + + added_this_page = 0 + for item in data: + # Get showdate and match to our shows + showdate = item.get('showdate') + if not showdate: + continue + + our_show_id = show_by_date.get(showdate) + if not our_show_id: + continue + + # Match song + song_name = (item.get('songname') or '').lower().strip() + song_id = song_map.get(song_name) + if not song_id: + continue + + position = item.get('position', 0) + key = (our_show_id, song_id, position) + + if key in existing: + continue + + perf = Performance( + show_id=our_show_id, + song_id=song_id, + position=position, + set_name=item.get('setnumber'), + segue=(item.get('transition', ', ') != ', '), + notes=item.get('footnote') + ) + session.add(perf) + existing.add(key) + added_this_page += 1 + total_added += 1 + + session.commit() + print(f" +{added_this_page} ({total_added} total)") + page += 1 + + print(f"\\n✓ Added {total_added} performances") + +if __name__ == "__main__": + main() diff --git a/backend/import_per_show.py b/backend/import_per_show.py new file mode 100644 index 0000000..297f506 --- /dev/null +++ b/backend/import_per_show.py @@ -0,0 +1,131 @@ +""" +Per-Show Setlist Importer +Fetches setlist for each show individually +""" +import requests +from sqlmodel import Session, select +from database import engine +from models import Show, Song, Performance + +BASE_URL = "https://elgoose.net/api/v2" + +def fetch_show_setlist(api_show_id): + """Fetch setlist for a specific show""" + url = f"{BASE_URL}/setlists/showid/{api_show_id}.json" + try: + response = requests.get(url, timeout=30) + response.raise_for_status() + data = response.json() + if data.get('error') == 1: + return None + return data.get('data', []) + except Exception as e: + return None + +def main(): + print("PER-SHOW SETLIST IMPORTER") + print("=" * 40) + + with Session(engine) as session: + # Get all shows + shows = session.exec(select(Show)).all() + print(f"Found {len(shows)} shows in database") + + # Build song map by title + songs = session.exec(select(Song)).all() + song_map = {s.title.lower(): s.id for s in songs} + print(f"Mapped {len(song_map)} songs") + + # Get existing performances + existing = set() + perfs = session.exec( + select(Performance.show_id, Performance.song_id, Performance.position) + ).all() + for p in perfs: + existing.add((p[0], p[1], p[2])) + print(f"Found {len(existing)} existing performances") + + # We need API show IDs. The ElGoose API shows endpoint returns show_id. + # Let's fetch and correlate by date + print("Fetching API shows to get API IDs...") + api_shows = {} # date_str -> api_show_id + + page = 1 + while True: + url = f"{BASE_URL}/shows.json" + try: + resp = requests.get(url, params={"artist": 1, "page": page}, timeout=30) + data = resp.json().get('data', []) + if not data: + break + for s in data: + date_str = s['showdate'] + api_shows[date_str] = s['show_id'] + page += 1 + if page > 50: + break + except: + break + + print(f"Got {len(api_shows)} API show IDs") + + # Now import setlists for each show + total_added = 0 + processed = 0 + + for show in shows: + date_str = show.date.strftime('%Y-%m-%d') + api_show_id = api_shows.get(date_str) + + if not api_show_id: + continue + + # Check if we already have performances for this show + existing_for_show = session.exec( + select(Performance).where(Performance.show_id == show.id) + ).first() + + if existing_for_show: + continue # Skip shows that already have performances + + # Fetch setlist + setlist = fetch_show_setlist(api_show_id) + if not setlist: + continue + + added = 0 + for item in setlist: + song_title = item.get('songname', '').lower() + song_id = song_map.get(song_title) + + if not song_id: + continue + + position = item.get('position', 0) + key = (show.id, song_id, position) + + if key in existing: + continue + + perf = Performance( + show_id=show.id, + song_id=song_id, + position=position, + set_name=item.get('set'), + segue=bool(item.get('segue', 0)), + notes=item.get('footnote') + ) + session.add(perf) + existing.add(key) + added += 1 + total_added += 1 + + if added > 0: + session.commit() + processed += 1 + print(f"Show {date_str}: +{added} songs ({total_added} total)") + + print(f"\\n✓ Added {total_added} performances from {processed} shows") + +if __name__ == "__main__": + main() diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 97afb5e..3c79151 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -120,3 +120,27 @@ @apply bg-background text-foreground; } } + +/* Container constraints for large screens */ +.container { + width: 100%; + max-width: 1280px; + margin-left: auto; + margin-right: auto; + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +@media (min-width: 640px) { + .container { + padding-left: 2rem; + padding-right: 2rem; + } +} + +@media (min-width: 1024px) { + .container { + padding-left: 4rem; + padding-right: 4rem; + } +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 22790d4..c1c3758 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,9 +1,81 @@ import { ActivityFeed } from "@/components/feed/activity-feed" import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import Link from "next/link" -import { Trophy, Music, MapPin, Users } from "lucide-react" +import { Trophy, Music, MapPin, Calendar, ChevronRight, Star } from "lucide-react" +import { getApiUrl } from "@/lib/api-config" + +interface Show { + id: number + date: string + venue?: { + id: number + name: string + city?: string + state?: string + } + tour?: { + id: number + name: string + } +} + +interface Song { + id: number + title: string + times_played?: number + avg_rating?: number +} + +async function getRecentShows(): Promise { + try { + const res = await fetch(`${getApiUrl()}/shows?limit=8&sort=date&order=desc`, { + cache: 'no-store', + next: { revalidate: 60 } + }) + if (!res.ok) return [] + return res.json() + } catch (e) { + console.error('Failed to fetch recent shows:', e) + return [] + } +} + +async function getTopSongs(): Promise { + try { + const res = await fetch(`${getApiUrl()}/songs?limit=5&sort=times_played&order=desc`, { + cache: 'no-store', + next: { revalidate: 300 } + }) + if (!res.ok) return [] + return res.json() + } catch (e) { + console.error('Failed to fetch top songs:', e) + return [] + } +} + +async function getStats() { + try { + const [showsRes, songsRes, venuesRes] = await Promise.all([ + fetch(`${getApiUrl()}/shows?limit=1`, { cache: 'no-store' }), + fetch(`${getApiUrl()}/songs?limit=1`, { cache: 'no-store' }), + fetch(`${getApiUrl()}/venues?limit=1`, { cache: 'no-store' }) + ]) + // These endpoints return arrays, we need to get counts differently + // For now we'll just show the data we have + return { shows: 0, songs: 0, venues: 0 } + } catch (e) { + return { shows: 0, songs: 0, venues: 0 } + } +} + +export default async function Home() { + const [recentShows, topSongs] = await Promise.all([ + getRecentShows(), + getTopSongs() + ]) -export default function Home() { return (
{/* Hero Section */} @@ -31,45 +103,139 @@ export default function Home() {
-
- {/* Activity Feed */} -
+ {/* Recent Shows */} +
+
+

+ + Recent Shows +

+ + View all shows + +
+ {recentShows.length > 0 ? ( +
+ {recentShows.map((show) => ( + + + +
+ {new Date(show.date).toLocaleDateString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric' + })} +
+ {show.venue && ( +
+ {show.venue.name} +
+ )} + {show.venue?.city && ( +
+ {show.venue.city}{show.venue.state ? `, ${show.venue.state}` : ''} +
+ )} + {show.tour && ( +
+ {show.tour.name} +
+ )} +
+
+ + ))} +
+ ) : ( + +

No shows yet. Check back soon!

+
+ )} +
+ +
+ {/* Top Songs */} +
-

Recent Activity

- - View all activity +

+ + Top Songs +

+ + All songs + +
+ + + {topSongs.length > 0 ? ( +
    + {topSongs.map((song, idx) => ( +
  • + + + {idx + 1} + +
    +
    {song.title}
    + {song.times_played && ( +
    + {song.times_played} performances +
    + )} +
    + +
  • + ))} +
+ ) : ( +
+ No songs yet +
+ )} +
+
+
+ + {/* Activity Feed */} +
+
+

Recent Activity

+ + View all
- - {/* Quick Stats / Leaderboard Preview */} -
-

Explore

-
- - -

Shows

-

Browse the archive

- - - -

Venues

-

Find your favorite spots

- - - -

Community

-

Join the conversation

- - - -

Leaderboards

-

Top rated everything

- -
-
+ + {/* Quick Links */} +
+ + +

Shows

+

Browse the complete archive

+ + + +

Venues

+

Find your favorite spots

+ + + +

Songs

+

Explore the catalog

+ + + +

Leaderboards

+

Top rated everything

+ +
) }