Compare commits
2 commits
19c5e97e7f
...
159cbc853c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
159cbc853c | ||
|
|
5b236608f8 |
5 changed files with 455 additions and 2 deletions
159
backend/link_canon_songs.py
Normal file
159
backend/link_canon_songs.py
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
"""
|
||||||
|
Auto-linker script to find and link shared songs across bands.
|
||||||
|
|
||||||
|
This script identifies songs with matching titles across different verticals
|
||||||
|
and creates SongCanon entries to link them together.
|
||||||
|
|
||||||
|
Common shared songs in the jam scene:
|
||||||
|
- Grateful Dead covers (Friend of the Devil, Dark Star, Scarlet Begonias)
|
||||||
|
- Traditional songs (Amazing Grace, etc.)
|
||||||
|
- Songs that multiple bands cover
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from database import engine
|
||||||
|
from models import Song, SongCanon, Vertical
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_title(title: str) -> str:
|
||||||
|
"""Normalize song title for matching"""
|
||||||
|
# Lowercase
|
||||||
|
t = title.lower()
|
||||||
|
# Remove common suffixes
|
||||||
|
t = re.sub(r'\s*\(.*\)$', '', t) # Remove parenthetical notes
|
||||||
|
t = re.sub(r'\s*->.*$', '', t) # Remove segue indicators
|
||||||
|
t = re.sub(r'\s*>.*$', '', t) # Remove segue indicators
|
||||||
|
# Remove special characters
|
||||||
|
t = re.sub(r'[^\w\s]', '', t)
|
||||||
|
# Normalize whitespace
|
||||||
|
t = ' '.join(t.split())
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def generate_slug(title: str) -> str:
|
||||||
|
"""Generate URL-safe slug from title"""
|
||||||
|
slug = title.lower()
|
||||||
|
slug = re.sub(r'[^\w\s-]', '', slug)
|
||||||
|
slug = re.sub(r'[\s_]+', '-', slug)
|
||||||
|
slug = re.sub(r'-+', '-', slug)
|
||||||
|
return slug.strip('-')
|
||||||
|
|
||||||
|
|
||||||
|
def find_shared_songs():
|
||||||
|
"""Find songs that appear in multiple verticals"""
|
||||||
|
print("Finding shared songs across bands...\n")
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
# Get all songs grouped by normalized title
|
||||||
|
all_songs = session.exec(select(Song)).all()
|
||||||
|
|
||||||
|
# Group by normalized title
|
||||||
|
title_groups = {}
|
||||||
|
for song in all_songs:
|
||||||
|
norm = normalize_title(song.title)
|
||||||
|
if norm not in title_groups:
|
||||||
|
title_groups[norm] = []
|
||||||
|
title_groups[norm].append(song)
|
||||||
|
|
||||||
|
# Find songs that appear in multiple verticals
|
||||||
|
shared = {}
|
||||||
|
for norm_title, songs in title_groups.items():
|
||||||
|
vertical_ids = set(s.vertical_id for s in songs)
|
||||||
|
if len(vertical_ids) > 1:
|
||||||
|
shared[norm_title] = songs
|
||||||
|
|
||||||
|
print(f"Found {len(shared)} songs shared across bands:\n")
|
||||||
|
|
||||||
|
for norm_title, songs in sorted(shared.items()):
|
||||||
|
# Get band names
|
||||||
|
bands = []
|
||||||
|
for song in songs:
|
||||||
|
vertical = session.get(Vertical, song.vertical_id)
|
||||||
|
if vertical:
|
||||||
|
bands.append(f"{vertical.name} ({song.title})")
|
||||||
|
|
||||||
|
print(f" {norm_title}")
|
||||||
|
for band in bands:
|
||||||
|
print(f" - {band}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
return shared
|
||||||
|
|
||||||
|
|
||||||
|
def create_canon_links(dry_run: bool = True):
|
||||||
|
"""Create SongCanon entries and link songs to them"""
|
||||||
|
print(f"{'[DRY RUN] ' if dry_run else ''}Creating SongCanon links...\n")
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
shared = find_shared_songs()
|
||||||
|
|
||||||
|
created = 0
|
||||||
|
linked = 0
|
||||||
|
|
||||||
|
for norm_title, songs in shared.items():
|
||||||
|
# Use the most common title as the canonical title
|
||||||
|
title_counts = {}
|
||||||
|
for song in songs:
|
||||||
|
t = song.title
|
||||||
|
title_counts[t] = title_counts.get(t, 0) + 1
|
||||||
|
|
||||||
|
canonical_title = max(title_counts, key=title_counts.get)
|
||||||
|
slug = generate_slug(canonical_title)
|
||||||
|
|
||||||
|
# Check if canon already exists
|
||||||
|
existing = session.exec(
|
||||||
|
select(SongCanon).where(SongCanon.slug == slug)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
canon = existing
|
||||||
|
print(f" Found existing: {canonical_title}")
|
||||||
|
else:
|
||||||
|
# Determine original artist
|
||||||
|
original_artist = None
|
||||||
|
for song in songs:
|
||||||
|
if song.original_artist:
|
||||||
|
original_artist = song.original_artist
|
||||||
|
break
|
||||||
|
|
||||||
|
canon = SongCanon(
|
||||||
|
title=canonical_title,
|
||||||
|
slug=slug,
|
||||||
|
original_artist=original_artist
|
||||||
|
)
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
session.add(canon)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(canon)
|
||||||
|
|
||||||
|
created += 1
|
||||||
|
print(f" Created canon: {canonical_title}")
|
||||||
|
|
||||||
|
# Link songs to canon
|
||||||
|
for song in songs:
|
||||||
|
if song.canon_id != (canon.id if canon.id else None):
|
||||||
|
if not dry_run:
|
||||||
|
song.canon_id = canon.id
|
||||||
|
session.add(song)
|
||||||
|
linked += 1
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
print(f"\n{'Would create' if dry_run else 'Created'}: {created} canonical songs")
|
||||||
|
print(f"{'Would link' if dry_run else 'Linked'}: {linked} songs")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print("\nRun with dry_run=False to apply changes.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) > 1 and sys.argv[1] == "--apply":
|
||||||
|
create_canon_links(dry_run=False)
|
||||||
|
else:
|
||||||
|
create_canon_links(dry_run=True)
|
||||||
|
print("\nTo apply changes, run: python link_canon_songs.py --apply")
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
import os
|
import os
|
||||||
from routers import auth, shows, venues, songs, social, tours, artists, preferences, reviews, badges, nicknames, moderation, attendance, groups, users, search, performances, notifications, feed, leaderboards, stats, admin, chase, gamification, videos, musicians, sequences, verticals
|
from routers import auth, shows, venues, songs, social, tours, artists, preferences, reviews, badges, nicknames, moderation, attendance, groups, users, search, performances, notifications, feed, leaderboards, stats, admin, chase, gamification, videos, musicians, sequences, verticals, canon
|
||||||
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
|
@ -45,6 +45,7 @@ app.include_router(videos.router)
|
||||||
app.include_router(musicians.router)
|
app.include_router(musicians.router)
|
||||||
app.include_router(sequences.router)
|
app.include_router(sequences.router)
|
||||||
app.include_router(verticals.router)
|
app.include_router(verticals.router)
|
||||||
|
app.include_router(canon.router)
|
||||||
|
|
||||||
|
|
||||||
# Optional features - can be disabled via env vars
|
# Optional features - can be disabled via env vars
|
||||||
|
|
|
||||||
142
backend/routers/canon.py
Normal file
142
backend/routers/canon.py
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from typing import List
|
||||||
|
from database import get_session
|
||||||
|
from models import SongCanon, Song, Vertical
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/canon", tags=["canon"])
|
||||||
|
|
||||||
|
|
||||||
|
class SongVersionRead(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
slug: str | None
|
||||||
|
vertical_id: int
|
||||||
|
vertical_name: str
|
||||||
|
vertical_slug: str
|
||||||
|
|
||||||
|
|
||||||
|
class SongCanonRead(BaseModel):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
slug: str
|
||||||
|
original_artist: str | None
|
||||||
|
notes: str | None
|
||||||
|
versions: List[SongVersionRead]
|
||||||
|
|
||||||
|
|
||||||
|
class SongCanonCreate(BaseModel):
|
||||||
|
title: str
|
||||||
|
slug: str
|
||||||
|
original_artist: str | None = None
|
||||||
|
notes: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[SongCanonRead])
|
||||||
|
def list_canon_songs(
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0,
|
||||||
|
session: Session = Depends(get_session)
|
||||||
|
):
|
||||||
|
"""List all canonical songs with their cross-band versions"""
|
||||||
|
canons = session.exec(
|
||||||
|
select(SongCanon).offset(offset).limit(limit)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for canon in canons:
|
||||||
|
versions = []
|
||||||
|
songs = session.exec(
|
||||||
|
select(Song).where(Song.canon_id == canon.id)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for song in songs:
|
||||||
|
vertical = session.get(Vertical, song.vertical_id)
|
||||||
|
versions.append({
|
||||||
|
"id": song.id,
|
||||||
|
"title": song.title,
|
||||||
|
"slug": song.slug,
|
||||||
|
"vertical_id": song.vertical_id,
|
||||||
|
"vertical_name": vertical.name if vertical else "Unknown",
|
||||||
|
"vertical_slug": vertical.slug if vertical else "unknown"
|
||||||
|
})
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"id": canon.id,
|
||||||
|
"title": canon.title,
|
||||||
|
"slug": canon.slug,
|
||||||
|
"original_artist": canon.original_artist,
|
||||||
|
"notes": canon.notes,
|
||||||
|
"versions": versions
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{slug}", response_model=SongCanonRead)
|
||||||
|
def get_canon_song(slug: str, session: Session = Depends(get_session)):
|
||||||
|
"""Get a canonical song with all its band-specific versions"""
|
||||||
|
canon = session.exec(
|
||||||
|
select(SongCanon).where(SongCanon.slug == slug)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not canon:
|
||||||
|
raise HTTPException(status_code=404, detail="Canonical song not found")
|
||||||
|
|
||||||
|
versions = []
|
||||||
|
songs = session.exec(
|
||||||
|
select(Song).where(Song.canon_id == canon.id)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for song in songs:
|
||||||
|
vertical = session.get(Vertical, song.vertical_id)
|
||||||
|
versions.append({
|
||||||
|
"id": song.id,
|
||||||
|
"title": song.title,
|
||||||
|
"slug": song.slug,
|
||||||
|
"vertical_id": song.vertical_id,
|
||||||
|
"vertical_name": vertical.name if vertical else "Unknown",
|
||||||
|
"vertical_slug": vertical.slug if vertical else "unknown"
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": canon.id,
|
||||||
|
"title": canon.title,
|
||||||
|
"slug": canon.slug,
|
||||||
|
"original_artist": canon.original_artist,
|
||||||
|
"notes": canon.notes,
|
||||||
|
"versions": versions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/song/{song_id}/related", response_model=List[SongVersionRead])
|
||||||
|
def get_related_versions(song_id: int, session: Session = Depends(get_session)):
|
||||||
|
"""Get all versions of the same song across bands"""
|
||||||
|
song = session.get(Song, song_id)
|
||||||
|
if not song:
|
||||||
|
raise HTTPException(status_code=404, detail="Song not found")
|
||||||
|
|
||||||
|
if not song.canon_id:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Get all songs with same canon_id (excluding this one)
|
||||||
|
related = session.exec(
|
||||||
|
select(Song)
|
||||||
|
.where(Song.canon_id == song.canon_id)
|
||||||
|
.where(Song.id != song_id)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for s in related:
|
||||||
|
vertical = session.get(Vertical, s.vertical_id)
|
||||||
|
result.append({
|
||||||
|
"id": s.id,
|
||||||
|
"title": s.title,
|
||||||
|
"slug": s.slug,
|
||||||
|
"vertical_id": s.vertical_id,
|
||||||
|
"vertical_name": vertical.name if vertical else "Unknown",
|
||||||
|
"vertical_slug": vertical.slug if vertical else "unknown"
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
@ -60,6 +60,8 @@ def list_musicians(
|
||||||
@router.get("/{slug}")
|
@router.get("/{slug}")
|
||||||
def get_musician(slug: str, session: Session = Depends(get_session)):
|
def get_musician(slug: str, session: Session = Depends(get_session)):
|
||||||
"""Get musician details with band memberships and guest appearances"""
|
"""Get musician details with band memberships and guest appearances"""
|
||||||
|
from models import Show, Song, Vertical
|
||||||
|
|
||||||
musician = session.exec(select(Musician).where(Musician.slug == slug)).first()
|
musician = session.exec(select(Musician).where(Musician.slug == slug)).first()
|
||||||
if not musician:
|
if not musician:
|
||||||
raise HTTPException(status_code=404, detail="Musician not found")
|
raise HTTPException(status_code=404, detail="Musician not found")
|
||||||
|
|
@ -70,8 +72,10 @@ def get_musician(slug: str, session: Session = Depends(get_session)):
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
bands = []
|
bands = []
|
||||||
|
primary_band_ids = set()
|
||||||
for m in memberships:
|
for m in memberships:
|
||||||
artist = session.get(Artist, m.artist_id)
|
artist = session.get(Artist, m.artist_id)
|
||||||
|
primary_band_ids.add(m.artist_id)
|
||||||
bands.append({
|
bands.append({
|
||||||
"id": m.id,
|
"id": m.id,
|
||||||
"artist_id": m.artist_id,
|
"artist_id": m.artist_id,
|
||||||
|
|
@ -80,20 +84,44 @@ def get_musician(slug: str, session: Session = Depends(get_session)):
|
||||||
"role": m.role,
|
"role": m.role,
|
||||||
"start_date": str(m.start_date) if m.start_date else None,
|
"start_date": str(m.start_date) if m.start_date else None,
|
||||||
"end_date": str(m.end_date) if m.end_date else None,
|
"end_date": str(m.end_date) if m.end_date else None,
|
||||||
|
"is_current": m.end_date is None,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Get guest appearances
|
# Get guest appearances with full context
|
||||||
appearances = session.exec(
|
appearances = session.exec(
|
||||||
select(PerformanceGuest).where(PerformanceGuest.musician_id == musician.id)
|
select(PerformanceGuest).where(PerformanceGuest.musician_id == musician.id)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
guests = []
|
guests = []
|
||||||
|
sit_in_verticals = {} # Track which bands they've sat in with
|
||||||
|
|
||||||
for g in appearances:
|
for g in appearances:
|
||||||
perf = session.get(Performance, g.performance_id)
|
perf = session.get(Performance, g.performance_id)
|
||||||
|
if not perf:
|
||||||
|
continue
|
||||||
|
|
||||||
|
show = session.get(Show, perf.show_id) if perf else None
|
||||||
|
song = session.get(Song, perf.song_id) if perf else None
|
||||||
|
vertical = session.get(Vertical, show.vertical_id) if show else None
|
||||||
|
|
||||||
|
if vertical:
|
||||||
|
if vertical.id not in sit_in_verticals:
|
||||||
|
sit_in_verticals[vertical.id] = {
|
||||||
|
"vertical_id": vertical.id,
|
||||||
|
"vertical_name": vertical.name,
|
||||||
|
"vertical_slug": vertical.slug,
|
||||||
|
"count": 0
|
||||||
|
}
|
||||||
|
sit_in_verticals[vertical.id]["count"] += 1
|
||||||
|
|
||||||
guests.append({
|
guests.append({
|
||||||
"id": g.id,
|
"id": g.id,
|
||||||
"performance_id": g.performance_id,
|
"performance_id": g.performance_id,
|
||||||
"performance_slug": perf.slug if perf else None,
|
"performance_slug": perf.slug if perf else None,
|
||||||
|
"song_title": song.title if song else None,
|
||||||
|
"show_date": str(show.date.date()) if show and show.date else None,
|
||||||
|
"vertical_name": vertical.name if vertical else None,
|
||||||
|
"vertical_slug": vertical.slug if vertical else None,
|
||||||
"instrument": g.instrument,
|
"instrument": g.instrument,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -108,6 +136,13 @@ def get_musician(slug: str, session: Session = Depends(get_session)):
|
||||||
},
|
},
|
||||||
"bands": bands,
|
"bands": bands,
|
||||||
"guest_appearances": guests,
|
"guest_appearances": guests,
|
||||||
|
"sit_in_summary": list(sit_in_verticals.values()),
|
||||||
|
"stats": {
|
||||||
|
"total_bands": len(bands),
|
||||||
|
"current_bands": len([b for b in bands if b.get("is_current")]),
|
||||||
|
"total_sit_ins": len(guests),
|
||||||
|
"bands_sat_in_with": len(sit_in_verticals),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Admin Endpoints (for now, no auth check - can be added later) ---
|
# --- Admin Endpoints (for now, no auth check - can be added later) ---
|
||||||
|
|
|
||||||
116
backend/seed_musicians.py
Normal file
116
backend/seed_musicians.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
"""
|
||||||
|
Seed script to create common cross-band musicians.
|
||||||
|
|
||||||
|
These are musicians known for sitting in with multiple bands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from database import engine
|
||||||
|
from models import Musician, Artist, BandMembership
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def generate_slug(name: str) -> str:
|
||||||
|
"""Generate URL-safe slug"""
|
||||||
|
slug = name.lower()
|
||||||
|
slug = re.sub(r'[^\w\s-]', '', slug)
|
||||||
|
slug = re.sub(r'[\s_]+', '-', slug)
|
||||||
|
return slug.strip('-')
|
||||||
|
|
||||||
|
|
||||||
|
# Notable jam scene musicians who appear across bands
|
||||||
|
CROSS_BAND_MUSICIANS = [
|
||||||
|
# Grateful Dead / Dead Family
|
||||||
|
{"name": "Bob Weir", "primary_instrument": "Guitar", "bands": ["grateful-dead", "dead-and-company"]},
|
||||||
|
{"name": "Mickey Hart", "primary_instrument": "Drums", "bands": ["grateful-dead", "dead-and-company"]},
|
||||||
|
{"name": "Bill Kreutzmann", "primary_instrument": "Drums", "bands": ["grateful-dead", "dead-and-company"]},
|
||||||
|
{"name": "John Mayer", "primary_instrument": "Guitar", "bands": ["dead-and-company"]},
|
||||||
|
{"name": "Oteil Burbridge", "primary_instrument": "Bass", "bands": ["dead-and-company"]},
|
||||||
|
{"name": "Jeff Chimenti", "primary_instrument": "Keyboards", "bands": ["dead-and-company"]},
|
||||||
|
|
||||||
|
# Goose
|
||||||
|
{"name": "Rick Mitarotonda", "primary_instrument": "Guitar, Vocals", "bands": ["goose"]},
|
||||||
|
{"name": "Peter Anspach", "primary_instrument": "Keyboards, Guitar", "bands": ["goose"]},
|
||||||
|
{"name": "Trevor Weekz", "primary_instrument": "Bass", "bands": ["goose"]},
|
||||||
|
{"name": "Ben Atkind", "primary_instrument": "Drums", "bands": ["goose"]},
|
||||||
|
{"name": "Jeff Arevalo", "primary_instrument": "Percussion", "bands": ["goose"]},
|
||||||
|
|
||||||
|
# Phish
|
||||||
|
{"name": "Trey Anastasio", "primary_instrument": "Guitar", "bands": ["phish"]},
|
||||||
|
{"name": "Page McConnell", "primary_instrument": "Keyboards", "bands": ["phish"]},
|
||||||
|
{"name": "Mike Gordon", "primary_instrument": "Bass", "bands": ["phish"]},
|
||||||
|
{"name": "Jon Fishman", "primary_instrument": "Drums", "bands": ["phish"]},
|
||||||
|
|
||||||
|
# Billy Strings
|
||||||
|
{"name": "Billy Strings", "primary_instrument": "Guitar, Vocals", "bands": ["billy-strings"]},
|
||||||
|
{"name": "Billy Failing", "primary_instrument": "Banjo", "bands": ["billy-strings"]},
|
||||||
|
{"name": "Royal Masat", "primary_instrument": "Bass", "bands": ["billy-strings"]},
|
||||||
|
{"name": "Jarrod Walker", "primary_instrument": "Mandolin", "bands": ["billy-strings"]},
|
||||||
|
|
||||||
|
# Cross-band sit-in regulars
|
||||||
|
{"name": "Marcus King", "primary_instrument": "Guitar, Vocals", "bands": []},
|
||||||
|
{"name": "Pigeons Playing Ping Pong", "primary_instrument": "Funk", "bands": []},
|
||||||
|
{"name": "Karina Rykman", "primary_instrument": "Bass, Vocals", "bands": []},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def seed_musicians():
|
||||||
|
"""Create musicians if they don't exist"""
|
||||||
|
print("Seeding musicians...\n")
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
created = 0
|
||||||
|
|
||||||
|
for m in CROSS_BAND_MUSICIANS:
|
||||||
|
slug = generate_slug(m["name"])
|
||||||
|
|
||||||
|
existing = session.exec(
|
||||||
|
select(Musician).where(Musician.slug == slug)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
print(f" Exists: {m['name']}")
|
||||||
|
musician = existing
|
||||||
|
else:
|
||||||
|
musician = Musician(
|
||||||
|
name=m["name"],
|
||||||
|
slug=slug,
|
||||||
|
primary_instrument=m["primary_instrument"]
|
||||||
|
)
|
||||||
|
session.add(musician)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(musician)
|
||||||
|
created += 1
|
||||||
|
print(f" Created: {m['name']}")
|
||||||
|
|
||||||
|
# Create band memberships
|
||||||
|
for band_slug in m.get("bands", []):
|
||||||
|
# Find vertical by slug to get artist
|
||||||
|
from models import Vertical
|
||||||
|
vertical = session.exec(
|
||||||
|
select(Vertical).where(Vertical.slug == band_slug)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if vertical and vertical.primary_artist_id:
|
||||||
|
# Check if membership exists
|
||||||
|
existing_membership = session.exec(
|
||||||
|
select(BandMembership)
|
||||||
|
.where(BandMembership.musician_id == musician.id)
|
||||||
|
.where(BandMembership.artist_id == vertical.primary_artist_id)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not existing_membership:
|
||||||
|
membership = BandMembership(
|
||||||
|
musician_id=musician.id,
|
||||||
|
artist_id=vertical.primary_artist_id,
|
||||||
|
role=m["primary_instrument"]
|
||||||
|
)
|
||||||
|
session.add(membership)
|
||||||
|
print(f" -> Added to {band_slug}")
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f"\nCreated {created} new musicians")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
seed_musicians()
|
||||||
Loading…
Add table
Reference in a new issue