feat: Add container max-width, revamp homepage with real data, add setlist import scripts

This commit is contained in:
fullsizemalt 2025-12-20 21:16:25 -08:00
parent f9cdd626f4
commit 8fa04e9690
5 changed files with 587 additions and 35 deletions

View file

@ -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()

103
backend/import_by_date.py Normal file
View file

@ -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()

131
backend/import_per_show.py Normal file
View file

@ -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()

View file

@ -120,3 +120,27 @@
@apply bg-background text-foreground; @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;
}
}

View file

@ -1,9 +1,81 @@
import { ActivityFeed } from "@/components/feed/activity-feed" import { ActivityFeed } from "@/components/feed/activity-feed"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import Link from "next/link" 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<Show[]> {
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<Song[]> {
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 ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
{/* Hero Section */} {/* Hero Section */}
@ -31,45 +103,139 @@ export default function Home() {
</div> </div>
</section> </section>
<div className="grid gap-8 md:grid-cols-2"> {/* Recent Shows */}
{/* Activity Feed */} <section className="space-y-4">
<section className="space-y-4"> <div className="flex items-center justify-between">
<h2 className="text-2xl font-bold flex items-center gap-2">
<Calendar className="h-6 w-6 text-blue-500" />
Recent Shows
</h2>
<Link href="/shows" className="text-sm text-muted-foreground hover:underline flex items-center gap-1">
View all shows <ChevronRight className="h-4 w-4" />
</Link>
</div>
{recentShows.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{recentShows.map((show) => (
<Link key={show.id} href={`/shows/${show.id}`}>
<Card className="h-full hover:bg-accent/50 transition-colors cursor-pointer">
<CardContent className="p-4">
<div className="font-semibold">
{new Date(show.date).toLocaleDateString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
})}
</div>
{show.venue && (
<div className="text-sm text-muted-foreground mt-1">
{show.venue.name}
</div>
)}
{show.venue?.city && (
<div className="text-xs text-muted-foreground">
{show.venue.city}{show.venue.state ? `, ${show.venue.state}` : ''}
</div>
)}
{show.tour && (
<div className="text-xs text-primary mt-2">
{show.tour.name}
</div>
)}
</CardContent>
</Card>
</Link>
))}
</div>
) : (
<Card className="p-8 text-center text-muted-foreground">
<p>No shows yet. Check back soon!</p>
</Card>
)}
</section>
<div className="grid gap-8 lg:grid-cols-3">
{/* Top Songs */}
<section className="space-y-4 lg:col-span-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h2 className="text-2xl font-bold">Recent Activity</h2> <h2 className="text-xl font-bold flex items-center gap-2">
<Link href="/leaderboards" className="text-sm text-muted-foreground hover:underline"> <Star className="h-5 w-5 text-yellow-500" />
View all activity Top Songs
</h2>
<Link href="/songs" className="text-sm text-muted-foreground hover:underline flex items-center gap-1">
All songs <ChevronRight className="h-4 w-4" />
</Link>
</div>
<Card>
<CardContent className="p-0">
{topSongs.length > 0 ? (
<ul className="divide-y">
{topSongs.map((song, idx) => (
<li key={song.id}>
<Link
href={`/songs/${song.id}`}
className="flex items-center gap-3 p-3 hover:bg-accent/50 transition-colors"
>
<span className="text-lg font-bold text-muted-foreground w-6 text-center">
{idx + 1}
</span>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{song.title}</div>
{song.times_played && (
<div className="text-xs text-muted-foreground">
{song.times_played} performances
</div>
)}
</div>
</Link>
</li>
))}
</ul>
) : (
<div className="p-4 text-center text-muted-foreground text-sm">
No songs yet
</div>
)}
</CardContent>
</Card>
</section>
{/* Activity Feed */}
<section className="space-y-4 lg:col-span-2">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold">Recent Activity</h2>
<Link href="/leaderboards" className="text-sm text-muted-foreground hover:underline flex items-center gap-1">
View all <ChevronRight className="h-4 w-4" />
</Link> </Link>
</div> </div>
<ActivityFeed /> <ActivityFeed />
</section> </section>
{/* Quick Stats / Leaderboard Preview */}
<section className="space-y-4">
<h2 className="text-2xl font-bold">Explore</h2>
<div className="grid gap-4 sm:grid-cols-2">
<Link href="/shows" className="block p-6 border rounded-xl hover:bg-accent transition-colors">
<Music className="h-8 w-8 mb-2 text-blue-500" />
<h3 className="font-bold">Shows</h3>
<p className="text-sm text-muted-foreground">Browse the archive</p>
</Link>
<Link href="/venues" className="block p-6 border rounded-xl hover:bg-accent transition-colors">
<MapPin className="h-8 w-8 mb-2 text-green-500" />
<h3 className="font-bold">Venues</h3>
<p className="text-sm text-muted-foreground">Find your favorite spots</p>
</Link>
<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">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" />
<h3 className="font-bold">Leaderboards</h3>
<p className="text-sm text-muted-foreground">Top rated everything</p>
</Link>
</div>
</section>
</div> </div>
{/* Quick Links */}
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Link href="/shows" className="block p-6 border rounded-xl hover:bg-accent transition-colors group">
<Music className="h-8 w-8 mb-2 text-blue-500 group-hover:scale-110 transition-transform" />
<h3 className="font-bold">Shows</h3>
<p className="text-sm text-muted-foreground">Browse the complete archive</p>
</Link>
<Link href="/venues" className="block p-6 border rounded-xl hover:bg-accent transition-colors group">
<MapPin className="h-8 w-8 mb-2 text-green-500 group-hover:scale-110 transition-transform" />
<h3 className="font-bold">Venues</h3>
<p className="text-sm text-muted-foreground">Find your favorite spots</p>
</Link>
<Link href="/songs" className="block p-6 border rounded-xl hover:bg-accent transition-colors group">
<Music className="h-8 w-8 mb-2 text-purple-500 group-hover:scale-110 transition-transform" />
<h3 className="font-bold">Songs</h3>
<p className="text-sm text-muted-foreground">Explore the catalog</p>
</Link>
<Link href="/leaderboards" className="block p-6 border rounded-xl hover:bg-accent transition-colors group">
<Trophy className="h-8 w-8 mb-2 text-yellow-500 group-hover:scale-110 transition-transform" />
<h3 className="font-bold">Leaderboards</h3>
<p className="text-sm text-muted-foreground">Top rated everything</p>
</Link>
</section>
</div> </div>
) )
} }