feat: Add slug support for all entities
- Add slug fields to Song, Venue, Show, Tour, Performance models - Update routers to support lookup by slug or ID - Create slugify.py utility for generating URL-safe slugs - Add migration script to generate slugs for existing data - Performance slugs use songslug-YYYY-MM-DD format
This commit is contained in:
parent
2e4e0b811d
commit
3edbcdeb64
6 changed files with 398 additions and 9 deletions
224
backend/migrations/add_slugs.py
Normal file
224
backend/migrations/add_slugs.py
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
"""
|
||||||
|
Migration script to add slug columns and generate slugs for existing data
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from sqlmodel import create_engine, Session, select, text
|
||||||
|
from slugify import generate_slug, generate_show_slug, generate_performance_slug
|
||||||
|
|
||||||
|
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://elmeg:elmeg@localhost/elmeg")
|
||||||
|
engine = create_engine(DATABASE_URL)
|
||||||
|
|
||||||
|
def add_slug_columns():
|
||||||
|
"""Add slug columns to tables if they don't exist"""
|
||||||
|
with engine.connect() as conn:
|
||||||
|
# Add slug to song
|
||||||
|
conn.execute(text("ALTER TABLE song ADD COLUMN IF NOT EXISTS slug VARCHAR UNIQUE"))
|
||||||
|
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_song_slug ON song(slug)"))
|
||||||
|
|
||||||
|
# Add slug to venue
|
||||||
|
conn.execute(text("ALTER TABLE venue ADD COLUMN IF NOT EXISTS slug VARCHAR UNIQUE"))
|
||||||
|
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_venue_slug ON venue(slug)"))
|
||||||
|
|
||||||
|
# Add slug to show
|
||||||
|
conn.execute(text("ALTER TABLE show ADD COLUMN IF NOT EXISTS slug VARCHAR UNIQUE"))
|
||||||
|
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_show_slug ON show(slug)"))
|
||||||
|
|
||||||
|
# Add slug to tour
|
||||||
|
conn.execute(text("ALTER TABLE tour ADD COLUMN IF NOT EXISTS slug VARCHAR UNIQUE"))
|
||||||
|
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_tour_slug ON tour(slug)"))
|
||||||
|
|
||||||
|
# Add slug to performance
|
||||||
|
conn.execute(text("ALTER TABLE performance ADD COLUMN IF NOT EXISTS slug VARCHAR UNIQUE"))
|
||||||
|
conn.execute(text("CREATE INDEX IF NOT EXISTS ix_performance_slug ON performance(slug)"))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("✓ Slug columns added")
|
||||||
|
|
||||||
|
def generate_song_slugs():
|
||||||
|
"""Generate slugs for all songs"""
|
||||||
|
with Session(engine) as session:
|
||||||
|
# Get all songs without slugs
|
||||||
|
result = session.exec(text("SELECT id, title FROM song WHERE slug IS NULL"))
|
||||||
|
songs = result.fetchall()
|
||||||
|
|
||||||
|
existing_slugs = set()
|
||||||
|
# Get existing slugs
|
||||||
|
existing = session.exec(text("SELECT slug FROM song WHERE slug IS NOT NULL"))
|
||||||
|
for row in existing.fetchall():
|
||||||
|
existing_slugs.add(row[0])
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for song_id, title in songs:
|
||||||
|
base_slug = generate_slug(title, 50)
|
||||||
|
slug = base_slug
|
||||||
|
counter = 2
|
||||||
|
|
||||||
|
while slug in existing_slugs:
|
||||||
|
slug = f"{base_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
existing_slugs.add(slug)
|
||||||
|
session.execute(
|
||||||
|
text("UPDATE song SET slug = :slug WHERE id = :id"),
|
||||||
|
{"slug": slug, "id": song_id}
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f"✓ Generated slugs for {count} songs")
|
||||||
|
|
||||||
|
def generate_venue_slugs():
|
||||||
|
"""Generate slugs for all venues"""
|
||||||
|
with Session(engine) as session:
|
||||||
|
result = session.exec(text("SELECT id, name, city FROM venue WHERE slug IS NULL"))
|
||||||
|
venues = result.fetchall()
|
||||||
|
|
||||||
|
existing_slugs = set()
|
||||||
|
existing = session.exec(text("SELECT slug FROM venue WHERE slug IS NOT NULL"))
|
||||||
|
for row in existing.fetchall():
|
||||||
|
existing_slugs.add(row[0])
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for venue_id, name, city in venues:
|
||||||
|
# Include city to help disambiguate
|
||||||
|
base_slug = generate_slug(f"{name} {city}", 60)
|
||||||
|
slug = base_slug
|
||||||
|
counter = 2
|
||||||
|
|
||||||
|
while slug in existing_slugs:
|
||||||
|
slug = f"{base_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
existing_slugs.add(slug)
|
||||||
|
session.execute(
|
||||||
|
text("UPDATE venue SET slug = :slug WHERE id = :id"),
|
||||||
|
{"slug": slug, "id": venue_id}
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f"✓ Generated slugs for {count} venues")
|
||||||
|
|
||||||
|
def generate_show_slugs():
|
||||||
|
"""Generate slugs for all shows"""
|
||||||
|
with Session(engine) as session:
|
||||||
|
result = session.exec(text("""
|
||||||
|
SELECT s.id, s.date, v.name
|
||||||
|
FROM show s
|
||||||
|
LEFT JOIN venue v ON s.venue_id = v.id
|
||||||
|
WHERE s.slug IS NULL
|
||||||
|
"""))
|
||||||
|
shows = result.fetchall()
|
||||||
|
|
||||||
|
existing_slugs = set()
|
||||||
|
existing = session.exec(text("SELECT slug FROM show WHERE slug IS NOT NULL"))
|
||||||
|
for row in existing.fetchall():
|
||||||
|
existing_slugs.add(row[0])
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for show_id, date, venue_name in shows:
|
||||||
|
date_str = date.strftime("%Y-%m-%d") if date else "unknown"
|
||||||
|
venue_slug = generate_slug(venue_name or "unknown", 25)
|
||||||
|
base_slug = f"{date_str}-{venue_slug}"
|
||||||
|
slug = base_slug
|
||||||
|
counter = 2
|
||||||
|
|
||||||
|
while slug in existing_slugs:
|
||||||
|
slug = f"{base_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
existing_slugs.add(slug)
|
||||||
|
session.execute(
|
||||||
|
text("UPDATE show SET slug = :slug WHERE id = :id"),
|
||||||
|
{"slug": slug, "id": show_id}
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f"✓ Generated slugs for {count} shows")
|
||||||
|
|
||||||
|
def generate_tour_slugs():
|
||||||
|
"""Generate slugs for all tours"""
|
||||||
|
with Session(engine) as session:
|
||||||
|
result = session.exec(text("SELECT id, name FROM tour WHERE slug IS NULL"))
|
||||||
|
tours = result.fetchall()
|
||||||
|
|
||||||
|
existing_slugs = set()
|
||||||
|
existing = session.exec(text("SELECT slug FROM tour WHERE slug IS NOT NULL"))
|
||||||
|
for row in existing.fetchall():
|
||||||
|
existing_slugs.add(row[0])
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for tour_id, name in tours:
|
||||||
|
base_slug = generate_slug(name, 50)
|
||||||
|
slug = base_slug
|
||||||
|
counter = 2
|
||||||
|
|
||||||
|
while slug in existing_slugs:
|
||||||
|
slug = f"{base_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
existing_slugs.add(slug)
|
||||||
|
session.execute(
|
||||||
|
text("UPDATE tour SET slug = :slug WHERE id = :id"),
|
||||||
|
{"slug": slug, "id": tour_id}
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f"✓ Generated slugs for {count} tours")
|
||||||
|
|
||||||
|
def generate_performance_slugs():
|
||||||
|
"""Generate slugs for all performances (songslug-date format)"""
|
||||||
|
with Session(engine) as session:
|
||||||
|
result = session.exec(text("""
|
||||||
|
SELECT p.id, s.slug as song_slug, sh.date
|
||||||
|
FROM performance p
|
||||||
|
JOIN song s ON p.song_id = s.id
|
||||||
|
JOIN show sh ON p.show_id = sh.id
|
||||||
|
WHERE p.slug IS NULL
|
||||||
|
"""))
|
||||||
|
performances = result.fetchall()
|
||||||
|
|
||||||
|
existing_slugs = set()
|
||||||
|
existing = session.exec(text("SELECT slug FROM performance WHERE slug IS NOT NULL"))
|
||||||
|
for row in existing.fetchall():
|
||||||
|
existing_slugs.add(row[0])
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for perf_id, song_slug, date in performances:
|
||||||
|
date_str = date.strftime("%Y-%m-%d") if date else "unknown"
|
||||||
|
base_slug = f"{song_slug}-{date_str}"
|
||||||
|
slug = base_slug
|
||||||
|
counter = 2
|
||||||
|
|
||||||
|
# Handle multiple performances of same song in same show
|
||||||
|
while slug in existing_slugs:
|
||||||
|
slug = f"{base_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
existing_slugs.add(slug)
|
||||||
|
session.execute(
|
||||||
|
text("UPDATE performance SET slug = :slug WHERE id = :id"),
|
||||||
|
{"slug": slug, "id": perf_id}
|
||||||
|
)
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f"✓ Generated slugs for {count} performances")
|
||||||
|
|
||||||
|
def run_migration():
|
||||||
|
print("=== Running Slug Migration ===")
|
||||||
|
add_slug_columns()
|
||||||
|
generate_song_slugs()
|
||||||
|
generate_venue_slugs()
|
||||||
|
generate_tour_slugs()
|
||||||
|
generate_show_slugs()
|
||||||
|
generate_performance_slugs() # Must run after song slugs
|
||||||
|
print("=== Migration Complete ===")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_migration()
|
||||||
|
|
@ -6,6 +6,7 @@ from datetime import datetime
|
||||||
class Performance(SQLModel, table=True):
|
class Performance(SQLModel, table=True):
|
||||||
"""Link table between Show and Song (Many-to-Many with extra data)"""
|
"""Link table between Show and Song (Many-to-Many with extra data)"""
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
|
slug: Optional[str] = Field(default=None, unique=True, index=True, description="songslug-YYYY-MM-DD")
|
||||||
show_id: int = Field(foreign_key="show.id")
|
show_id: int = Field(foreign_key="show.id")
|
||||||
song_id: int = Field(foreign_key="song.id")
|
song_id: int = Field(foreign_key="song.id")
|
||||||
position: int = Field(description="Order in the setlist")
|
position: int = Field(description="Order in the setlist")
|
||||||
|
|
@ -64,6 +65,7 @@ class Vertical(SQLModel, table=True):
|
||||||
class Venue(SQLModel, table=True):
|
class Venue(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: Optional[str] = Field(default=None, unique=True, index=True)
|
||||||
city: str
|
city: str
|
||||||
state: Optional[str] = Field(default=None)
|
state: Optional[str] = Field(default=None)
|
||||||
country: str
|
country: str
|
||||||
|
|
@ -75,6 +77,7 @@ class Venue(SQLModel, table=True):
|
||||||
class Tour(SQLModel, table=True):
|
class Tour(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: Optional[str] = Field(default=None, unique=True, index=True)
|
||||||
start_date: Optional[datetime] = None
|
start_date: Optional[datetime] = None
|
||||||
end_date: Optional[datetime] = None
|
end_date: Optional[datetime] = None
|
||||||
notes: Optional[str] = Field(default=None)
|
notes: Optional[str] = Field(default=None)
|
||||||
|
|
@ -90,6 +93,7 @@ class Artist(SQLModel, table=True):
|
||||||
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)
|
||||||
|
slug: Optional[str] = Field(default=None, unique=True, index=True)
|
||||||
vertical_id: int = Field(foreign_key="vertical.id")
|
vertical_id: int = Field(foreign_key="vertical.id")
|
||||||
venue_id: Optional[int] = Field(default=None, foreign_key="venue.id")
|
venue_id: Optional[int] = Field(default=None, foreign_key="venue.id")
|
||||||
tour_id: Optional[int] = Field(default=None, foreign_key="tour.id")
|
tour_id: Optional[int] = Field(default=None, foreign_key="tour.id")
|
||||||
|
|
@ -109,6 +113,7 @@ class Show(SQLModel, table=True):
|
||||||
class Song(SQLModel, table=True):
|
class Song(SQLModel, table=True):
|
||||||
id: Optional[int] = Field(default=None, primary_key=True)
|
id: Optional[int] = Field(default=None, primary_key=True)
|
||||||
title: str = Field(index=True)
|
title: str = Field(index=True)
|
||||||
|
slug: Optional[str] = Field(default=None, unique=True, index=True)
|
||||||
original_artist: Optional[str] = Field(default=None)
|
original_artist: Optional[str] = Field(default=None)
|
||||||
vertical_id: int = Field(foreign_key="vertical.id")
|
vertical_id: int = Field(foreign_key="vertical.id")
|
||||||
notes: Optional[str] = Field(default=None)
|
notes: Optional[str] = Field(default=None)
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,23 @@ from auth import get_current_user
|
||||||
|
|
||||||
router = APIRouter(prefix="/performances", tags=["performances"])
|
router = APIRouter(prefix="/performances", tags=["performances"])
|
||||||
|
|
||||||
@router.get("/{performance_id}", response_model=PerformanceDetailRead)
|
@router.get("/{performance_id_or_slug}", response_model=PerformanceDetailRead)
|
||||||
def read_performance(performance_id: int, session: Session = Depends(get_session)):
|
def read_performance(performance_id_or_slug: str, session: Session = Depends(get_session)):
|
||||||
performance = session.get(Performance, performance_id)
|
performance = None
|
||||||
|
if performance_id_or_slug.isdigit():
|
||||||
|
performance = session.get(Performance, int(performance_id_or_slug))
|
||||||
|
|
||||||
|
if not performance:
|
||||||
|
# Try slug lookup
|
||||||
|
performance = session.exec(
|
||||||
|
select(Performance).where(Performance.slug == performance_id_or_slug)
|
||||||
|
).first()
|
||||||
|
|
||||||
if not performance:
|
if not performance:
|
||||||
raise HTTPException(status_code=404, detail="Performance not found")
|
raise HTTPException(status_code=404, detail="Performance not found")
|
||||||
|
|
||||||
|
performance_id = performance.id # Use actual ID for lookups
|
||||||
|
|
||||||
# --- Calculate Stats & Navigation ---
|
# --- Calculate Stats & Navigation ---
|
||||||
from sqlmodel import select, func, desc
|
from sqlmodel import select, func, desc
|
||||||
from models import Show
|
from models import Show
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,21 @@ def read_songs(offset: int = 0, limit: int = Query(default=100, le=100), session
|
||||||
|
|
||||||
from services.stats import get_song_stats
|
from services.stats import get_song_stats
|
||||||
|
|
||||||
@router.get("/{song_id}", response_model=SongReadWithStats)
|
@router.get("/{song_id_or_slug}", response_model=SongReadWithStats)
|
||||||
def read_song(song_id: int, session: Session = Depends(get_session)):
|
def read_song(song_id_or_slug: str, session: Session = Depends(get_session)):
|
||||||
song = session.get(Song, song_id)
|
# Try to parse as int (ID), otherwise treat as slug
|
||||||
|
song = None
|
||||||
|
if song_id_or_slug.isdigit():
|
||||||
|
song = session.get(Song, int(song_id_or_slug))
|
||||||
|
|
||||||
|
if not song:
|
||||||
|
# Try slug lookup
|
||||||
|
song = session.exec(select(Song).where(Song.slug == song_id_or_slug)).first()
|
||||||
|
|
||||||
if not song:
|
if not song:
|
||||||
raise HTTPException(status_code=404, detail="Song not found")
|
raise HTTPException(status_code=404, detail="Song not found")
|
||||||
|
|
||||||
|
song_id = song.id # Use actual ID for lookups
|
||||||
stats = get_song_stats(session, song_id)
|
stats = get_song_stats(session, song_id)
|
||||||
|
|
||||||
tags = session.exec(
|
tags = session.exec(
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,15 @@ def read_venues(offset: int = 0, limit: int = Query(default=100, le=100), sessio
|
||||||
venues = session.exec(select(Venue).offset(offset).limit(limit)).all()
|
venues = session.exec(select(Venue).offset(offset).limit(limit)).all()
|
||||||
return venues
|
return venues
|
||||||
|
|
||||||
@router.get("/{venue_id}", response_model=VenueRead)
|
@router.get("/{venue_id_or_slug}", response_model=VenueRead)
|
||||||
def read_venue(venue_id: int, session: Session = Depends(get_session)):
|
def read_venue(venue_id_or_slug: str, session: Session = Depends(get_session)):
|
||||||
venue = session.get(Venue, venue_id)
|
venue = None
|
||||||
|
if venue_id_or_slug.isdigit():
|
||||||
|
venue = session.get(Venue, int(venue_id_or_slug))
|
||||||
|
|
||||||
|
if not venue:
|
||||||
|
venue = session.exec(select(Venue).where(Venue.slug == venue_id_or_slug)).first()
|
||||||
|
|
||||||
if not venue:
|
if not venue:
|
||||||
raise HTTPException(status_code=404, detail="Venue not found")
|
raise HTTPException(status_code=404, detail="Venue not found")
|
||||||
return venue
|
return venue
|
||||||
|
|
|
||||||
134
backend/slugify.py
Normal file
134
backend/slugify.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
"""
|
||||||
|
Slug generation utilities
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
def generate_slug(text: str, max_length: int = 50) -> str:
|
||||||
|
"""
|
||||||
|
Generate a URL-safe slug from text.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
"Tweezer Reprise" -> "tweezer-reprise"
|
||||||
|
"You Enjoy Myself" -> "you-enjoy-myself"
|
||||||
|
"The Gorge Amphitheatre" -> "the-gorge-amphitheatre"
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Convert to lowercase
|
||||||
|
slug = text.lower()
|
||||||
|
|
||||||
|
# Replace common special characters
|
||||||
|
replacements = {
|
||||||
|
"'": "",
|
||||||
|
"'": "",
|
||||||
|
'"': "",
|
||||||
|
"&": "and",
|
||||||
|
"+": "and",
|
||||||
|
"@": "at",
|
||||||
|
"#": "",
|
||||||
|
"$": "",
|
||||||
|
"%": "",
|
||||||
|
"!": "",
|
||||||
|
"?": "",
|
||||||
|
".": "",
|
||||||
|
",": "",
|
||||||
|
":": "",
|
||||||
|
";": "",
|
||||||
|
"/": "-",
|
||||||
|
"\\": "-",
|
||||||
|
"(": "",
|
||||||
|
")": "",
|
||||||
|
"[": "",
|
||||||
|
"]": "",
|
||||||
|
"{": "",
|
||||||
|
"}": "",
|
||||||
|
"<": "",
|
||||||
|
">": "",
|
||||||
|
"—": "-",
|
||||||
|
"–": "-",
|
||||||
|
"...": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
for old, new in replacements.items():
|
||||||
|
slug = slug.replace(old, new)
|
||||||
|
|
||||||
|
# Replace any non-alphanumeric characters with dashes
|
||||||
|
slug = re.sub(r'[^a-z0-9]+', '-', slug)
|
||||||
|
|
||||||
|
# Remove leading/trailing dashes and collapse multiple dashes
|
||||||
|
slug = re.sub(r'-+', '-', slug).strip('-')
|
||||||
|
|
||||||
|
# Truncate to max length (at word boundary if possible)
|
||||||
|
if len(slug) > max_length:
|
||||||
|
slug = slug[:max_length]
|
||||||
|
# Try to cut at last dash to avoid partial words
|
||||||
|
last_dash = slug.rfind('-')
|
||||||
|
if last_dash > max_length // 2:
|
||||||
|
slug = slug[:last_dash]
|
||||||
|
|
||||||
|
return slug
|
||||||
|
|
||||||
|
|
||||||
|
def generate_unique_slug(
|
||||||
|
base_text: str,
|
||||||
|
existing_slugs: list[str],
|
||||||
|
max_length: int = 50
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Generate a unique slug, appending numbers if necessary.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
"Tweezer" with existing ["tweezer"] -> "tweezer-2"
|
||||||
|
"Tweezer" with existing ["tweezer", "tweezer-2"] -> "tweezer-3"
|
||||||
|
"""
|
||||||
|
base_slug = generate_slug(base_text, max_length - 4) # Leave room for "-999"
|
||||||
|
|
||||||
|
if base_slug not in existing_slugs:
|
||||||
|
return base_slug
|
||||||
|
|
||||||
|
# Find next available number
|
||||||
|
counter = 2
|
||||||
|
while f"{base_slug}-{counter}" in existing_slugs:
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
return f"{base_slug}-{counter}"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_show_slug(date_str: str, venue_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate a slug for a show based on date and venue.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
"2024-12-31", "Madison Square Garden" -> "2024-12-31-msg"
|
||||||
|
"2024-07-04", "The Gorge Amphitheatre" -> "2024-07-04-the-gorge"
|
||||||
|
"""
|
||||||
|
# Common venue abbreviations
|
||||||
|
abbreviations = {
|
||||||
|
"madison square garden": "msg",
|
||||||
|
"red rocks amphitheatre": "red-rocks",
|
||||||
|
"the gorge amphitheatre": "the-gorge",
|
||||||
|
"alpine valley music theatre": "alpine",
|
||||||
|
"dicks sporting goods park": "dicks",
|
||||||
|
"mgm grand garden arena": "mgm",
|
||||||
|
"saratoga performing arts center": "spac",
|
||||||
|
}
|
||||||
|
|
||||||
|
venue_slug = abbreviations.get(venue_name.lower())
|
||||||
|
if not venue_slug:
|
||||||
|
# Take first 2-3 words of venue name
|
||||||
|
venue_slug = generate_slug(venue_name, 25)
|
||||||
|
|
||||||
|
return f"{date_str}-{venue_slug}"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_performance_slug(song_title: str, show_date: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate a slug for a specific performance.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
"Tweezer", "2024-12-31" -> "tweezer-2024-12-31"
|
||||||
|
"""
|
||||||
|
song_slug = generate_slug(song_title, 30)
|
||||||
|
return f"{song_slug}-{show_date}"
|
||||||
Loading…
Add table
Reference in a new issue