fediversion/backend/seed_activity.py
fullsizemalt b4cddf41ea feat: Initialize Fediversion multi-band platform
- Fork elmeg-demo codebase for multi-band support
- Add data importer infrastructure with base class
- Create band-specific importers:
  - phish.py: Phish.net API v5
  - grateful_dead.py: Grateful Stats API
  - setlistfm.py: Dead & Company, Billy Strings (Setlist.fm)
- Add spec-kit configuration for Gemini
- Update README with supported bands and architecture
2025-12-28 12:39:28 -08:00

399 lines
20 KiB
Python

"""
El Goose Activity Seeder (Enhanced)
Populates the demo environment with 36 distinct user personas and realistic activity.
Generates attendance, ratings, reviews (Shows, Venues, Tours), and comments.
Includes "Wiki-Style" linking logic [[Entity:ID|Label]].
"""
import random
import re
from datetime import datetime, timedelta
from sqlmodel import Session, select
from database import engine
from models import User, UserPreferences, Show, Song, Venue, Performance, Review, Attendance, Comment, Tour, Profile
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
# --- 1. Define 36 Distinct Personas ---
PERSONAS = [
# Original 12
{"username": "TheArchivist", "email": "archivist@demo.com", "role": "user", "style": "factual", "bio": "Tracking every tease since 2016."},
{"username": "StatNerd420", "email": "statnerd@demo.com", "role": "user", "style": "analytical", "bio": "It's all about the gap times."},
{"username": "CriticalListener", "email": "reviewer@demo.com", "role": "user", "style": "critical", "bio": "Honest takes only. 3.5 stars is a good rating."},
{"username": "CasualFan", "email": "casual@demo.com", "role": "user", "style": "casual", "bio": "Just here for a good time."},
{"username": "NortheastHonkers", "email": "groupleader@demo.com", "role": "user", "style": "community", "bio": "Rep the Northeast flock!"},
{"username": "ModGoose", "email": "mod@demo.com", "role": "moderator", "style": "official", "bio": "Play nice in the comments."},
{"username": "AdminBird", "email": "admin@demo.com", "role": "admin", "style": "official", "bio": "System Admin."},
{"username": "NewToGoose", "email": "newbie@demo.com", "role": "user", "style": "excited_noob", "bio": "Just discovered them last week!"},
{"username": "TaperTom", "email": "taper@demo.com", "role": "user", "style": "technical", "bio": "MK4 > V3 > 24/96"},
{"username": "RoadWarrior", "email": "tourfollower@demo.com", "role": "user", "style": "traveler", "bio": "100+ shows and counting."},
{"username": "SilentHonker", "email": "lurker@demo.com", "role": "user", "style": "quiet", "bio": "..."},
{"username": "HypeGoose", "email": "hype@demo.com", "role": "user", "style": "hype", "bio": "BEST BAND EVER LFG!!!"},
# New 24
{"username": "VinylJunkie", "email": "vinyl@demo.com", "role": "user", "style": "collector", "bio": "Spinning wax only. ISO Shenanigans variants."},
{"username": "CouchTourCapt", "email": "couch@demo.com", "role": "user", "style": "streamer", "bio": "Streaming from the living room every night."},
{"username": "RailRider", "email": "rail@demo.com", "role": "user", "style": "intense", "bio": "If you're not on the rail, were you even there?"},
{"username": "PosterNutbag", "email": "poster@demo.com", "role": "user", "style": "collector", "bio": "Here for the foil prints."},
{"username": "SetlistPredictor", "email": "predict@demo.com", "role": "user", "style": "analytical", "bio": "Called the opener 3 nights in a row."},
{"username": "JamFlowMan", "email": "jam@demo.com", "role": "user", "style": "vibey", "bio": "Surrender to the flow."},
{"username": "TedHead", "email": "ted@demo.com", "role": "user", "style": "insider", "bio": "Ted Tapes or bust."},
{"username": "HonkIfUrHorny", "email": "honk@demo.com", "role": "user", "style": "meme", "bio": "HONK HONK"},
{"username": "PeterSide", "email": "peter@demo.com", "role": "user", "style": "fanboy", "bio": "Mustache appreciation society."},
{"username": "RicksPick", "email": "rick@demo.com", "role": "user", "style": "fanboy", "bio": "That tone though."},
{"username": "TrevorBassFace", "email": "trevor@demo.com", "role": "user", "style": "fanboy", "bio": "T-Bone holds it down."},
{"username": "SpudsMcKenzie", "email": "spuds@demo.com", "role": "user", "style": "fanboy", "bio": "Drums > Space"},
{"username": "JeffsPercussion", "email": "jeff@demo.com", "role": "user", "style": "fanboy", "bio": "More gong please."},
{"username": "CotterPin", "email": "cotter@demo.com", "role": "user", "style": "fanboy", "bio": "New era is here."},
{"username": "OreboloFan", "email": "acoustic@demo.com", "role": "user", "style": "acoustic", "bio": "Acoustic sets are superior."},
{"username": "IndieGroove", "email": "indie@demo.com", "role": "user", "style": "hipster", "bio": "I liked them when they played bars."},
{"username": "PhishConvert", "email": "phish@demo.com", "role": "user", "style": "comparative", "bio": "3.0 vet giving this a shot."},
{"username": "DeadheadConvert", "email": "dead@demo.com", "role": "user", "style": "old_school", "bio": "Searching for the sound."},
{"username": "DiscoGoose", "email": "disco@demo.com", "role": "user", "style": "dancer", "bio": "Here to dance."},
{"username": "ProgRockFan", "email": "prog@demo.com", "role": "user", "style": "technical", "bio": "Time signatures are my love language."},
{"username": "FunkyUncle", "email": "funky@demo.com", "role": "user", "style": "vibey", "bio": "Keep it funky."},
{"username": "WysteriaLocal", "email": "local@demo.com", "role": "user", "style": "local", "bio": "CT born and raised."},
{"username": "FactoryFiction", "email": "factory@demo.com", "role": "user", "style": "intense", "bio": "Chasing that Factory Fiction."},
{"username": "Shenanigans", "email": "squad@demo.com", "role": "user", "style": "party", "bio": "Squad up!"},
]
# --- 2. Content Generation Templates ---
SHOW_REVIEWS = {
"hype": [
"ABSOLUTELY INSANE SHOW! That [[Song:{song_id}|{song}]] jam changed my life.",
"Can we talk about [[Song:{song_id}|{song}]]?! [[Venue:{venue_id}|{venue}]] was shaking!",
"Best show of the tour hands down. The energy during [[Song:{song_id}|{song}]] was unreal.",
"LFG!!! [[Song:{song_id}|{song}]] > [[Song:{song2_id}|{song2}]] was the highlight for me.",
"I have no words. [[Song:{song_id}|{song}]] was a heater."
],
"critical": [
"Solid show, but the mix was a bit muddy during [[Song:{song_id}|{song}]].",
"First set was slow, but they picked it up with [[Song:{song_id}|{song}]] in the second.",
"Standard version of [[Song:{song_id}|{song}]], nothing special. [[Song:{song2_id}|{song2}]] was the improvisational peak.",
"Good energy at [[Venue:{venue_id}|{venue}]], but I've heard better versions of [[Song:{song_id}|{song}]].",
"3.5/5. [[Song:{song_id}|{song}]] went deep but didn't quite stick the landing."
],
"analytical": [
"First time [[Song:{song_id}|{song}]] has opened a second set since 2019.",
"The jam in [[Song:{song_id}|{song}]] clocked in at 22 minutes. Major key modulation at the 12-minute mark.",
"Interesting placement for [[Song:{song_id}|{song}]]. [[Venue:{venue_id}|{venue}]] always gets unique setlists.",
"Gap bust! Haven't played [[Song:{song_id}|{song}]] in 45 shows.",
"Statistically an above-average show. [[Song:{song_id}|{song}]] jam density was high."
],
"casual": [
"Had a blast at [[Venue:{venue_id}|{venue}]]! [[Song:{song_id}|{song}]] was my favorite.",
"Great vibes tonight. Loved hearing [[Song:{song_id}|{song}]].",
"So much fun dancing to [[Song:{song_id}|{song}]]. Can't wait for the next one.",
"Took my friend to their first show and they loved [[Song:{song_id}|{song}]].",
"Good times. [[Song:{song_id}|{song}]] was cool."
],
"technical": [
"Rick's tone on [[Song:{song_id}|{song}]] was dialed in perfectly.",
"Peter's clav work on [[Song:{song_id}|{song}]] was funky as hell.",
"The lighting rig looked amazing during [[Song:{song_id}|{song}]].",
"Trevor was locked in for [[Song:{song_id}|{song}]]. Great low end.",
"Spuds and Jeff were driving the bus on [[Song:{song_id}|{song}]]."
],
"comparative": [
"Getting major 97 Phish vibes from that [[Song:{song_id}|{song}]] jam.",
"That [[Song:{song_id}|{song}]] transition reminded me of a Dead segue.",
"This version of [[Song:{song_id}|{song}]] rivals the one from last month.",
"They are really finding their own sound on [[Song:{song_id}|{song}]].",
"Better than the studio version of [[Song:{song_id}|{song}]] for sure."
]
}
VENUE_REVIEWS = {
"hype": [
"[[Venue:{venue_id}|{venue}]] IS THE CHURCH! Best place to see them.",
"The energy in this room is unmatched. I love [[Venue:{venue_id}|{venue}]]!",
"Always a party at [[Venue:{venue_id}|{venue}]]. Can't wait to go back."
],
"critical": [
"Sightlines at [[Venue:{venue_id}|{venue}]] are terrible if you're not on the floor.",
"Security at [[Venue:{venue_id}|{venue}]] was a nightmare. Great show otherwise.",
"Sound was bouncy in the balcony. [[Venue:{venue_id}|{venue}]] needs acoustic treatment."
],
"casual": [
"Nice venue, easy parking. [[Venue:{venue_id}|{venue}]] is a solid spot.",
"Had a good time at [[Venue:{venue_id}|{venue}]]. Beers were expensive though.",
"Beautiful theatre. [[Venue:{venue_id}|{venue}]] has great architecture."
],
"traveler": [
"Drove 6 hours to get to [[Venue:{venue_id}|{venue}]]. Worth it.",
"Checked [[Venue:{venue_id}|{venue}]] off my bucket list. 10/10.",
"One of my favorite stops on tour. [[Venue:{venue_id}|{venue}]] never disappoints."
]
}
TOUR_REVIEWS = {
"hype": [
"[[Tour:{tour_id}|{tour}]] was their best tour yet! Every night was fire.",
"They leveled up on [[Tour:{tour_id}|{tour}]]. New songs are sounding great.",
"Take me back to [[Tour:{tour_id}|{tour}]]! What a run."
],
"analytical": [
"[[Tour:{tour_id}|{tour}]] featured the most 20+ minute jams of any era.",
"Setlist variety on [[Tour:{tour_id}|{tour}]] was at an all-time high.",
"Interesting evolution of the sound during [[Tour:{tour_id}|{tour}]]."
],
"critical": [
"[[Tour:{tour_id}|{tour}]] started strong but fizzled out at the end.",
"Too many repeats on [[Tour:{tour_id}|{tour}]]. Need to dig deeper.",
"Not my favorite era. [[Tour:{tour_id}|{tour}]] felt a bit disjointed."
]
}
# --- 3. Helper Functions ---
def get_random_show_review(style, song1, song2, venue):
"""Generate a show review with wiki links"""
templates = SHOW_REVIEWS.get(style, SHOW_REVIEWS["casual"])
# Map specific styles to generic categories
if style in ["excited_noob", "intense", "party", "fanboy"]: templates = SHOW_REVIEWS["hype"]
elif style in ["factual", "technical", "collector"]: templates = SHOW_REVIEWS["technical"]
elif style in ["old_school", "hipster"]: templates = SHOW_REVIEWS["comparative"]
elif style in ["quiet", "streamer"]: templates = SHOW_REVIEWS["casual"]
elif style in ["analytical", "insider"]: templates = SHOW_REVIEWS["analytical"]
template = random.choice(templates)
return template.format(
song=song1.title, song_id=song1.id,
song2=song2.title, song2_id=song2.id,
venue=venue.name, venue_id=venue.id
)
def get_random_venue_review(style, venue):
"""Generate a venue review with wiki links"""
templates = VENUE_REVIEWS.get(style, VENUE_REVIEWS["casual"])
if style in ["excited_noob", "intense", "party"]: templates = VENUE_REVIEWS["hype"]
elif style in ["critical", "technical"]: templates = VENUE_REVIEWS["critical"]
elif style in ["traveler", "collector"]: templates = VENUE_REVIEWS["traveler"]
template = random.choice(templates)
return template.format(venue=venue.name, venue_id=venue.id)
def get_random_tour_review(style, tour):
"""Generate a tour review with wiki links"""
templates = TOUR_REVIEWS.get(style, TOUR_REVIEWS["analytical"])
if style in ["excited_noob", "intense", "party"]: templates = TOUR_REVIEWS["hype"]
elif style in ["critical", "old_school"]: templates = TOUR_REVIEWS["critical"]
template = random.choice(templates)
return template.format(tour=tour.name, tour_id=tour.id)
def seed_users(session):
"""Create all 36 users if they don't exist"""
print("👥 Seeding Users...")
users = {}
for p in PERSONAS:
user = session.exec(select(User).where(User.email == p["email"])).first()
if not user:
user = User(
email=p["email"],
hashed_password=pwd_context.hash("demo123"),
role=p["role"],
bio=p["bio"],
is_active=True
)
session.add(user)
session.commit()
session.refresh(user)
# Create Profile
profile = Profile(
user_id=user.id,
username=p["username"],
display_name=p["username"]
)
session.add(profile)
# Create preferences
prefs = UserPreferences(
user_id=user.id,
wiki_mode=(p["style"] == "factual" or p["style"] == "quiet"),
show_ratings=True,
show_comments=True
)
session.add(prefs)
session.commit()
print(f" + Created {p['username']}")
else:
# Update bio if missing
if not user.bio:
user.bio = p["bio"]
session.add(user)
session.commit()
# Ensure profile exists
profile = session.exec(select(Profile).where(Profile.user_id == user.id)).first()
if not profile:
profile = Profile(
user_id=user.id,
username=p["username"],
display_name=p["username"]
)
session.add(profile)
session.commit()
users[p["username"]] = user
return users
def seed_activity(session, users):
"""Generate attendance, reviews, and ratings"""
print("\n🎲 Generating Activity...")
# Fetch data
shows = session.exec(select(Show).order_by(Show.date.desc())).all()
venues = session.exec(select(Venue)).all()
tours = session.exec(select(Tour)).all()
if not shows:
print("❌ No shows found! Run import_elgoose.py first.")
return
print(f" Found {len(shows)} shows, {len(venues)} venues, {len(tours)} tours.")
count_attendance = 0
count_reviews = 0
# 1. Show Activity (Attendance + Reviews)
target_shows = shows[:50] + random.sample(shows[50:], min(len(shows)-50, 50))
for show in target_shows:
venue = session.get(Venue, show.venue_id)
if not venue: continue
performances = session.exec(select(Performance).where(Performance.show_id == show.id)).all()
if not performances: continue
songs = [session.get(Song, p.song_id) for p in performances]
if not songs: continue
attendees = random.sample(list(users.values()), k=random.randint(3, 15))
for user in attendees:
# Attendance
exists = session.exec(select(Attendance).where(
Attendance.user_id == user.id,
Attendance.show_id == show.id
)).first()
if not exists:
att = Attendance(
user_id=user.id,
show_id=show.id,
created_at=show.date + timedelta(days=random.randint(1, 5))
)
session.add(att)
count_attendance += 1
# Show Review (30% chance)
if random.random() < 0.3:
persona = next((p for p in PERSONAS if p["email"] == user.email), None)
style = persona["style"] if persona else "casual"
song1 = random.choice(songs)
song2 = random.choice(songs)
review_text = get_random_show_review(style, song1, song2, venue)
rating = random.randint(3, 5)
if style == "critical": rating = random.randint(2, 4)
if style == "hype": rating = 5
rev_exists = session.exec(select(Review).where(
Review.user_id == user.id,
Review.show_id == show.id
)).first()
if not rev_exists:
blurb = review_text.split('.')[0] + "."
if len(blurb) > 100: blurb = blurb[:97] + "..."
review = Review(
user_id=user.id,
show_id=show.id,
score=rating,
content=review_text,
blurb=blurb,
created_at=show.date + timedelta(days=random.randint(1, 10))
)
session.add(review)
count_reviews += 1
# 2. Venue Reviews
target_venues = random.sample(venues, min(len(venues), 30))
for venue in target_venues:
reviewers = random.sample(list(users.values()), k=random.randint(1, 5))
for user in reviewers:
persona = next((p for p in PERSONAS if p["email"] == user.email), None)
style = persona["style"] if persona else "casual"
review_text = get_random_venue_review(style, venue)
rating = random.randint(3, 5)
rev_exists = session.exec(select(Review).where(
Review.user_id == user.id,
Review.venue_id == venue.id
)).first()
if not rev_exists:
blurb = review_text.split('.')[0] + "."
if len(blurb) > 100: blurb = blurb[:97] + "..."
review = Review(
user_id=user.id,
venue_id=venue.id,
score=rating,
content=review_text,
blurb=blurb,
created_at=datetime.utcnow() - timedelta(days=random.randint(1, 365))
)
session.add(review)
count_reviews += 1
# 3. Tour Reviews
target_tours = random.sample(tours, min(len(tours), 10))
for tour in target_tours:
reviewers = random.sample(list(users.values()), k=random.randint(1, 3))
for user in reviewers:
persona = next((p for p in PERSONAS if p["email"] == user.email), None)
style = persona["style"] if persona else "casual"
review_text = get_random_tour_review(style, tour)
rating = random.randint(3, 5)
rev_exists = session.exec(select(Review).where(
Review.user_id == user.id,
Review.tour_id == tour.id
)).first()
if not rev_exists:
blurb = review_text.split('.')[0] + "."
if len(blurb) > 100: blurb = blurb[:97] + "..."
review = Review(
user_id=user.id,
tour_id=tour.id,
score=rating,
content=review_text,
blurb=blurb,
created_at=datetime.utcnow() - timedelta(days=random.randint(1, 365))
)
session.add(review)
count_reviews += 1
session.commit()
print(f"\n✅ Activity Generation Complete!")
print(f" + {count_attendance} Attendance records")
print(f" + {count_reviews} Reviews generated (Shows, Venues, Tours)")
def main():
with Session(engine) as session:
users_map = seed_users(session)
seed_activity(session, users_map)
if __name__ == "__main__":
main()