feat: implement Artist model usage, router, and migration script
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run

This commit is contained in:
fullsizemalt 2025-12-24 12:19:28 -08:00
parent 49e025d3bf
commit 61715a119c
4 changed files with 137 additions and 29 deletions

View file

@ -43,6 +43,7 @@ app.include_router(chase.router)
app.include_router(gamification.router) app.include_router(gamification.router)
app.include_router(videos.router) app.include_router(videos.router)
# Optional features - can be disabled via env vars # Optional features - can be disabled via env vars
if ENABLE_BUG_TRACKER: if ENABLE_BUG_TRACKER:
from routers import tickets from routers import tickets

View file

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

View file

@ -89,9 +89,14 @@ class Tour(SQLModel, table=True):
class Artist(SQLModel, table=True): class Artist(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
name: str = Field(index=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) instrument: Optional[str] = Field(default=None)
notes: Optional[str] = Field(default=None) notes: Optional[str] = Field(default=None)
songs: List["Song"] = Relationship(back_populates="artist")
class Show(SQLModel, table=True): class Show(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
date: datetime = Field(index=True) date: datetime = Field(index=True)
@ -121,6 +126,10 @@ class Song(SQLModel, table=True):
notes: Optional[str] = Field(default=None) notes: Optional[str] = Field(default=None)
youtube_link: 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") vertical: Vertical = Relationship(back_populates="songs")
class Tag(SQLModel, table=True): class Tag(SQLModel, table=True):

View file

@ -1,37 +1,50 @@
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session, select from sqlmodel import Session, select
from typing import List, Optional
from database import get_session from database import get_session
from models import Artist, User from models import Artist, Song, Performance, Show, PerformanceArtist
from schemas import ArtistCreate, ArtistRead from schemas import SongRead
from auth import get_current_user
router = APIRouter(prefix="/artists", tags=["artists"]) router = APIRouter(prefix="/artists", tags=["artists"])
@router.post("/", response_model=ArtistRead) @router.get("/{slug}")
def create_artist( async def get_artist(slug: str, session: Session = Depends(get_session)):
artist: ArtistCreate, """Get artist details, covers, and guest appearances"""
session: Session = Depends(get_session), artist = session.exec(select(Artist).where(Artist.slug == slug)).first()
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)
if not artist: if not artist:
raise HTTPException(status_code=404, detail="Artist not found") 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
}