- 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
134 lines
3.4 KiB
Python
134 lines
3.4 KiB
Python
"""
|
||
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}"
|