diff --git a/backend/main.py b/backend/main.py index e286835..4f5624e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -43,6 +43,7 @@ app.include_router(chase.router) app.include_router(gamification.router) app.include_router(videos.router) + # Optional features - can be disabled via env vars if ENABLE_BUG_TRACKER: from routers import tickets diff --git a/backend/migrations/migrate_artists.py b/backend/migrations/migrate_artists.py new file mode 100644 index 0000000..1c86319 --- /dev/null +++ b/backend/migrations/migrate_artists.py @@ -0,0 +1,85 @@ +""" +Migration script to refactor Artists and link Songs. +1. Add new columns to Artist (slug, bio, image_url). +2. Add artist_id to Song. +3. Migrate 'original_artist' string to Artist records. +""" +from sqlmodel import Session, select, text +from database import engine +from models import Artist, Song +from slugify import slugify + +def migrate_artists(): + with Session(engine) as session: + print("Running Artist & Covers Migration...") + + # 1. Schema Changes (Idempotent ALTER TABLE) + # Artist columns + try: + session.exec(text("ALTER TABLE artist ADD COLUMN slug VARCHAR UNIQUE")) + print("Added artist.slug") + except Exception as e: + print("artist.slug already exists or error:", e) + session.rollback() + + try: + session.exec(text("ALTER TABLE artist ADD COLUMN bio VARCHAR")) + print("Added artist.bio") + except Exception as e: + session.rollback() + + try: + session.exec(text("ALTER TABLE artist ADD COLUMN image_url VARCHAR")) + print("Added artist.image_url") + except Exception as e: + session.rollback() + + # Song artist_id + try: + session.exec(text("ALTER TABLE song ADD COLUMN artist_id INTEGER REFERENCES artist(id)")) + print("Added song.artist_id") + except Exception as e: + print("song.artist_id already exists or error:", e) + session.rollback() + + session.commit() + + # 2. Data Migration + songs = session.exec(select(Song).where(Song.original_artist != None)).all() + print(f"Found {len(songs)} songs with original_artist string.") + + created_count = 0 + linked_count = 0 + + for song in songs: + if not song.original_artist or song.artist_id: + continue + + artist_name = song.original_artist.strip() + if not artist_name: + continue + + # Clean up name (e.g., "The Beatles" vs "Beatles") + # For now, just trust the string but ensure consistent slug + artist_slug = slugify(artist_name) + + # Find or Create Artist + artist = session.exec(select(Artist).where(Artist.slug == artist_slug)).first() + if not artist: + artist = Artist(name=artist_name, slug=artist_slug) + session.add(artist) + session.commit() + session.refresh(artist) + created_count += 1 + print(f"Created Artist: {artist_name} ({artist_slug})") + + # Link Song + song.artist_id = artist.id + session.add(song) + linked_count += 1 + + session.commit() + print(f"Migration Complete: Created {created_count} artists, linked {linked_count} songs.") + +if __name__ == "__main__": + migrate_artists() diff --git a/backend/models.py b/backend/models.py index 6c1e13f..1c93dd3 100644 --- a/backend/models.py +++ b/backend/models.py @@ -89,8 +89,13 @@ class Tour(SQLModel, table=True): class Artist(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) + slug: str = Field(unique=True, index=True) + bio: Optional[str] = Field(default=None) + image_url: Optional[str] = Field(default=None) instrument: Optional[str] = Field(default=None) notes: Optional[str] = Field(default=None) + + songs: List["Song"] = Relationship(back_populates="artist") class Show(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) @@ -121,6 +126,10 @@ class Song(SQLModel, table=True): notes: Optional[str] = Field(default=None) youtube_link: Optional[str] = Field(default=None) + # New Relation + artist_id: Optional[int] = Field(default=None, foreign_key="artist.id") + artist: Optional[Artist] = Relationship(back_populates="songs") + vertical: Vertical = Relationship(back_populates="songs") class Tag(SQLModel, table=True): diff --git a/backend/routers/artists.py b/backend/routers/artists.py index fd30e2a..75b3337 100644 --- a/backend/routers/artists.py +++ b/backend/routers/artists.py @@ -1,37 +1,50 @@ -from typing import List from fastapi import APIRouter, Depends, HTTPException, Query from sqlmodel import Session, select +from typing import List, Optional from database import get_session -from models import Artist, User -from schemas import ArtistCreate, ArtistRead -from auth import get_current_user +from models import Artist, Song, Performance, Show, PerformanceArtist +from schemas import SongRead router = APIRouter(prefix="/artists", tags=["artists"]) -@router.post("/", response_model=ArtistRead) -def create_artist( - artist: ArtistCreate, - session: Session = Depends(get_session), - current_user: User = Depends(get_current_user) -): - db_artist = Artist.model_validate(artist) - session.add(db_artist) - session.commit() - session.refresh(db_artist) - return db_artist - -@router.get("/", response_model=List[ArtistRead]) -def read_artists( - offset: int = 0, - limit: int = Query(default=100, le=100), - session: Session = Depends(get_session) -): - artists = session.exec(select(Artist).offset(offset).limit(limit)).all() - return artists - -@router.get("/{artist_id}", response_model=ArtistRead) -def read_artist(artist_id: int, session: Session = Depends(get_session)): - artist = session.get(Artist, artist_id) +@router.get("/{slug}") +async def get_artist(slug: str, session: Session = Depends(get_session)): + """Get artist details, covers, and guest appearances""" + artist = session.exec(select(Artist).where(Artist.slug == slug)).first() if not artist: raise HTTPException(status_code=404, detail="Artist not found") - return artist + + # Get covers (Songs by this artist that Goose has played) + covers = session.exec( + select(Song) + .where(Song.artist_id == artist.id) + .order_by(Song.title) + ).all() + + # Get guest appearances (Performances where this artist is linked) + # This queries the PerformanceArtist link table + guest_appearances = session.exec( + select(Performance, Show) + .join(PerformanceArtist, PerformanceArtist.performance_id == Performance.id) + .join(Show, Performance.show_id == Show.id) + .where(PerformanceArtist.artist_id == artist.id) + .order_by(Show.date.desc()) + ).all() + + # Format guest appearances + guest_spots = [] + for perf, show in guest_appearances: + guest_spots.append({ + "date": show.date, + "venue": show.venue.name if show.venue else "Unknown Venue", + "city": f"{show.venue.city}, {show.venue.state}" if show.venue else "", + "song_title": perf.song.title, + "show_slug": show.slug, + "song_slug": perf.song.slug + }) + + return { + "artist": artist, + "covers": covers, + "guest_appearances": guest_spots + }