Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
- Backend: Added video_links relationship to Performance model - Backend: Updated shows and songs routers to eager-load videos and populate youtube_link - Frontend: Added YouTube icon to performance list items if video exists
723 lines
33 KiB
Python
723 lines
33 KiB
Python
from typing import List, Optional
|
|
from sqlmodel import Field, Relationship, SQLModel
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
# --- Join Tables ---
|
|
class Performance(SQLModel, table=True):
|
|
"""Link table between Show and Song (Many-to-Many with extra data)"""
|
|
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")
|
|
song_id: int = Field(foreign_key="song.id")
|
|
position: int = Field(description="Order in the setlist")
|
|
set_name: Optional[str] = Field(default=None, description="e.g., Set 1, Encore")
|
|
segue: bool = Field(default=False, description="Transition to next song >")
|
|
notes: Optional[str] = Field(default=None)
|
|
track_url: Optional[str] = Field(default=None, description="Deep link to track audio")
|
|
youtube_link: Optional[str] = Field(default=None, description="YouTube video URL")
|
|
bandcamp_link: Optional[str] = Field(default=None, description="Bandcamp track URL")
|
|
nugs_link: Optional[str] = Field(default=None, description="Nugs.net track URL")
|
|
|
|
nicknames: List["PerformanceNickname"] = Relationship(back_populates="performance")
|
|
show: "Show" = Relationship(back_populates="performances")
|
|
song: "Song" = Relationship()
|
|
video_links: List["VideoPerformance"] = Relationship(back_populates="performance")
|
|
|
|
class ShowArtist(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
show_id: int = Field(foreign_key="show.id")
|
|
artist_id: int = Field(foreign_key="artist.id")
|
|
notes: Optional[str] = Field(default=None, description="Role e.g. Guest")
|
|
|
|
class PerformanceArtist(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
performance_id: int = Field(foreign_key="performance.id")
|
|
artist_id: int = Field(foreign_key="artist.id")
|
|
notes: Optional[str] = Field(default=None, description="Role e.g. Guest")
|
|
|
|
class PerformanceNickname(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
performance_id: int = Field(foreign_key="performance.id")
|
|
nickname: str = Field(index=True)
|
|
description: Optional[str] = Field(default=None)
|
|
status: str = Field(default="pending", index=True) # pending, approved, rejected
|
|
suggested_by: int = Field(foreign_key="user.id")
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
performance: "Performance" = Relationship(back_populates="nicknames")
|
|
user: "User" = Relationship()
|
|
|
|
class EntityTag(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
tag_id: int = Field(foreign_key="tag.id")
|
|
entity_type: str = Field(index=True) # "show", "song", "venue"
|
|
entity_id: int = Field(index=True)
|
|
|
|
# --- Core Entities ---
|
|
|
|
class VerticalScene(SQLModel, table=True):
|
|
"""Join table linking verticals to scenes (many-to-many)"""
|
|
vertical_id: int = Field(foreign_key="vertical.id", primary_key=True)
|
|
scene_id: int = Field(foreign_key="scene.id", primary_key=True)
|
|
|
|
|
|
class Scene(SQLModel, table=True):
|
|
"""Genre/scene categorization for bands (e.g., 'Jam', 'Bluegrass', 'Dead Family')"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(unique=True, index=True)
|
|
slug: str = Field(unique=True, index=True)
|
|
description: Optional[str] = Field(default=None)
|
|
|
|
# Relationships
|
|
verticals: List["Vertical"] = Relationship(back_populates="scenes", link_model=VerticalScene)
|
|
|
|
|
|
class Vertical(SQLModel, table=True):
|
|
"""Represents a Fandom Vertical (e.g., 'Phish', 'Goose', 'Star Wars')"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(index=True)
|
|
slug: str = Field(unique=True, index=True)
|
|
description: Optional[str] = Field(default=None)
|
|
|
|
# Link to primary artist/band for this vertical
|
|
primary_artist_id: Optional[int] = Field(default=None, foreign_key="artist.id")
|
|
|
|
# Setlist.fm integration for universal import
|
|
setlistfm_mbid: Optional[str] = Field(default=None, description="MusicBrainz ID for Setlist.fm")
|
|
|
|
# Admin/status fields
|
|
is_active: bool = Field(default=True, description="Show in band selector")
|
|
is_featured: bool = Field(default=False, description="Highlight in discovery")
|
|
|
|
# Branding
|
|
logo_url: Optional[str] = Field(default=None, description="Band logo URL for UI")
|
|
accent_color: Optional[str] = Field(default=None, description="Hex color for accents")
|
|
|
|
# Rich profile fields
|
|
formed_year: Optional[int] = Field(default=None, description="Year band was formed")
|
|
origin_city: Optional[str] = Field(default=None, description="City of origin")
|
|
origin_state: Optional[str] = Field(default=None, description="State/province")
|
|
origin_country: Optional[str] = Field(default=None, description="Country")
|
|
long_description: Optional[str] = Field(default=None, description="Full band biography")
|
|
|
|
# Social/external links
|
|
website_url: Optional[str] = Field(default=None)
|
|
wikipedia_url: Optional[str] = Field(default=None)
|
|
bandcamp_url: Optional[str] = Field(default=None)
|
|
nugs_url: Optional[str] = Field(default=None)
|
|
relisten_url: Optional[str] = Field(default=None)
|
|
spotify_url: Optional[str] = Field(default=None)
|
|
|
|
# Relationships
|
|
shows: List["Show"] = Relationship(back_populates="vertical")
|
|
songs: List["Song"] = Relationship(back_populates="vertical")
|
|
scenes: List["Scene"] = Relationship(back_populates="verticals", link_model=VerticalScene)
|
|
user_preferences: List["UserVerticalPreference"] = Relationship(back_populates="vertical")
|
|
|
|
class VenueCanon(SQLModel, table=True):
|
|
"""Canonical venue independent of band - enables cross-band venue linking (like SongCanon for songs)"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(index=True)
|
|
slug: str = Field(unique=True, index=True)
|
|
city: str
|
|
state: Optional[str] = Field(default=None)
|
|
country: str = Field(default="USA")
|
|
latitude: Optional[float] = Field(default=None)
|
|
longitude: Optional[float] = Field(default=None)
|
|
capacity: Optional[int] = Field(default=None)
|
|
website_url: Optional[str] = Field(default=None)
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
# All venue records that point to this canonical venue
|
|
venues: List["Venue"] = Relationship(back_populates="canon")
|
|
|
|
|
|
class Venue(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(index=True)
|
|
slug: Optional[str] = Field(default=None, unique=True, index=True)
|
|
city: str
|
|
state: Optional[str] = Field(default=None)
|
|
country: str
|
|
capacity: Optional[int] = Field(default=None)
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
# Link to canonical venue for cross-band deduplication
|
|
canon_id: Optional[int] = Field(default=None, foreign_key="venuecanon.id")
|
|
canon: Optional[VenueCanon] = Relationship(back_populates="venues")
|
|
|
|
shows: List["Show"] = Relationship(back_populates="venue")
|
|
|
|
|
|
class Tour(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(index=True)
|
|
slug: Optional[str] = Field(default=None, unique=True, index=True)
|
|
start_date: Optional[datetime] = None
|
|
end_date: Optional[datetime] = None
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
shows: List["Show"] = Relationship(back_populates="tour")
|
|
|
|
|
|
class Festival(SQLModel, table=True):
|
|
"""Multi-band festivals that span multiple shows/dates (Bonnaroo, Lockn, etc.)"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(index=True)
|
|
slug: str = Field(unique=True, index=True)
|
|
year: Optional[int] = Field(default=None, description="Festival year/edition")
|
|
start_date: Optional[datetime] = None
|
|
end_date: Optional[datetime] = None
|
|
venue_id: Optional[int] = Field(default=None, foreign_key="venue.id")
|
|
website_url: Optional[str] = Field(default=None)
|
|
description: Optional[str] = Field(default=None)
|
|
|
|
# Relationships
|
|
shows: List["ShowFestival"] = Relationship(back_populates="festival")
|
|
|
|
|
|
class ShowFestival(SQLModel, table=True):
|
|
"""Link table for shows at festivals (many-to-many)"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
show_id: int = Field(foreign_key="show.id")
|
|
festival_id: int = Field(foreign_key="festival.id")
|
|
stage: Optional[str] = Field(default=None, description="Which stage (Main, Second, etc.)")
|
|
set_time: Optional[str] = Field(default=None, description="Scheduled time slot")
|
|
|
|
festival: Festival = Relationship(back_populates="shows")
|
|
|
|
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 Musician(SQLModel, table=True):
|
|
"""Individual human musicians (for tracking sit-ins and band membership)"""
|
|
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)
|
|
primary_instrument: Optional[str] = Field(default=None)
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
# Rich profile fields
|
|
birth_year: Optional[int] = Field(default=None)
|
|
origin_city: Optional[str] = Field(default=None)
|
|
origin_state: Optional[str] = Field(default=None)
|
|
origin_country: Optional[str] = Field(default=None)
|
|
|
|
# Social links
|
|
website_url: Optional[str] = Field(default=None)
|
|
wikipedia_url: Optional[str] = Field(default=None)
|
|
instagram_url: Optional[str] = Field(default=None)
|
|
|
|
# Relationships
|
|
memberships: List["BandMembership"] = Relationship(back_populates="musician")
|
|
guest_appearances: List["PerformanceGuest"] = Relationship(back_populates="musician")
|
|
|
|
class BandMembership(SQLModel, table=True):
|
|
"""Link between Musician and Band/Artist with role and dates"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
musician_id: int = Field(foreign_key="musician.id")
|
|
artist_id: int = Field(foreign_key="artist.id", description="The band/group")
|
|
role: Optional[str] = Field(default=None, description="e.g., Keyboards, Rhythm Guitar")
|
|
start_date: Optional[datetime] = Field(default=None)
|
|
end_date: Optional[datetime] = Field(default=None, description="Null = current member")
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
musician: Musician = Relationship(back_populates="memberships")
|
|
artist: Artist = Relationship()
|
|
|
|
class PerformanceGuest(SQLModel, table=True):
|
|
"""Link between Performance and Musician for sit-ins/guest appearances"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
performance_id: int = Field(foreign_key="performance.id")
|
|
musician_id: int = Field(foreign_key="musician.id")
|
|
instrument: Optional[str] = Field(default=None, description="What they played on this track")
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
musician: Musician = Relationship(back_populates="guest_appearances")
|
|
|
|
|
|
class Show(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
date: datetime = Field(index=True)
|
|
slug: Optional[str] = Field(default=None, unique=True, index=True)
|
|
vertical_id: int = Field(foreign_key="vertical.id")
|
|
venue_id: Optional[int] = Field(default=None, foreign_key="venue.id")
|
|
tour_id: Optional[int] = Field(default=None, foreign_key="tour.id")
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
# External Links
|
|
bandcamp_link: Optional[str] = Field(default=None)
|
|
nugs_link: Optional[str] = Field(default=None)
|
|
youtube_link: Optional[str] = Field(default=None)
|
|
relisten_link: Optional[str] = Field(default=None, description="Link to Relisten.net or archive.org")
|
|
|
|
vertical: Vertical = Relationship(back_populates="shows")
|
|
venue: Optional[Venue] = Relationship(back_populates="shows")
|
|
tour: Optional[Tour] = Relationship(back_populates="shows")
|
|
attendances: List["Attendance"] = Relationship(back_populates="show")
|
|
performances: List["Performance"] = Relationship(back_populates="show")
|
|
|
|
class SongCanon(SQLModel, table=True):
|
|
"""Canonical 'master' song independent of band - enables cross-band song linking"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
title: str = Field(index=True)
|
|
slug: str = Field(unique=True, index=True)
|
|
original_artist: Optional[str] = Field(default=None, description="Original songwriter/band")
|
|
original_artist_id: Optional[int] = Field(default=None, foreign_key="artist.id")
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
# All vertical-specific versions of this song
|
|
versions: List["Song"] = Relationship(back_populates="canon")
|
|
|
|
class Song(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
title: str = Field(index=True)
|
|
slug: Optional[str] = Field(default=None, unique=True, index=True)
|
|
original_artist: Optional[str] = Field(default=None)
|
|
vertical_id: int = Field(foreign_key="vertical.id")
|
|
notes: Optional[str] = Field(default=None)
|
|
youtube_link: Optional[str] = Field(default=None)
|
|
|
|
# Link to canonical song for cross-band tracking
|
|
canon_id: Optional[int] = Field(default=None, foreign_key="songcanon.id")
|
|
canon: Optional[SongCanon] = Relationship(back_populates="versions")
|
|
|
|
# Artist who wrote/performs this version
|
|
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 Sequence(SQLModel, table=True):
|
|
"""Named groupings of consecutive songs, e.g. 'Autumn Crossing' = Travelers > Elmeg the Wise"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(index=True, description="Human-readable name like 'Autumn Crossing'")
|
|
slug: str = Field(unique=True, index=True)
|
|
description: Optional[str] = Field(default=None)
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
# Relationship to songs that make up this sequence
|
|
songs: List["SequenceSong"] = Relationship(back_populates="sequence")
|
|
|
|
class SequenceSong(SQLModel, table=True):
|
|
"""Join table linking songs to sequences with ordering"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
sequence_id: int = Field(foreign_key="sequence.id")
|
|
song_id: int = Field(foreign_key="song.id")
|
|
position: int = Field(description="Order in sequence, 1-indexed")
|
|
|
|
sequence: Sequence = Relationship(back_populates="songs")
|
|
|
|
class Tag(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(unique=True, index=True)
|
|
slug: str = Field(unique=True, index=True)
|
|
|
|
class PreferenceTier(str, Enum):
|
|
HEADLINER = "headliner"
|
|
MAIN_STAGE = "main_stage"
|
|
SUPPORTING = "supporting"
|
|
IGNORED = "ignored" # Exclude from feeds, keep attribution mentions
|
|
|
|
class UserVerticalPreference(SQLModel, table=True):
|
|
"""User preferences for which bands to display prominently vs. attribution-only"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id", index=True)
|
|
vertical_id: int = Field(foreign_key="vertical.id", index=True)
|
|
|
|
# Preferences
|
|
display_mode: str = Field(default="standard") # compact, standard, expanded
|
|
priority: int = Field(default=0) # 0-100 sorting
|
|
tier: PreferenceTier = Field(default=PreferenceTier.MAIN_STAGE)
|
|
notify_on_show: bool = Field(default=True)
|
|
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
user: "User" = Relationship(back_populates="vertical_preferences")
|
|
vertical: "Vertical" = Relationship(back_populates="user_preferences")
|
|
|
|
class Attendance(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
show_id: int = Field(foreign_key="show.id")
|
|
notes: Optional[str] = Field(default=None)
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
user: "User" = Relationship(back_populates="attendances")
|
|
show: "Show" = Relationship(back_populates="attendances")
|
|
|
|
class Comment(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
content: str
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
# Polymorphic-ish associations (nullable FKs)
|
|
show_id: Optional[int] = Field(default=None, foreign_key="show.id")
|
|
venue_id: Optional[int] = Field(default=None, foreign_key="venue.id")
|
|
song_id: Optional[int] = Field(default=None, foreign_key="song.id")
|
|
parent_id: Optional[int] = Field(default=None, foreign_key="comment.id")
|
|
|
|
user: "User" = Relationship(back_populates="comments")
|
|
|
|
class Rating(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
score: float = Field(ge=1.0, le=10.0, description="Rating from 1.0 to 10.0")
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
show_id: Optional[int] = Field(default=None, foreign_key="show.id")
|
|
song_id: Optional[int] = Field(default=None, foreign_key="song.id")
|
|
performance_id: Optional[int] = Field(default=None, foreign_key="performance.id")
|
|
venue_id: Optional[int] = Field(default=None, foreign_key="venue.id")
|
|
tour_id: Optional[int] = Field(default=None, foreign_key="tour.id")
|
|
|
|
user: "User" = Relationship(back_populates="ratings")
|
|
|
|
class NotificationType(str, Enum):
|
|
SHOW_ALERT = "SHOW_ALERT"
|
|
SIT_IN_ALERT = "SIT_IN_ALERT"
|
|
CHASE_SONG_ALERT = "CHASE_SONG_ALERT"
|
|
|
|
class Notification(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id", index=True)
|
|
type: NotificationType
|
|
title: str
|
|
message: str
|
|
link: Optional[str] = None
|
|
is_read: bool = Field(default=False)
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
user: "User" = Relationship(back_populates="notifications")
|
|
|
|
class User(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
email: str = Field(unique=True, index=True)
|
|
hashed_password: str
|
|
is_active: bool = Field(default=True)
|
|
is_superuser: bool = Field(default=False)
|
|
role: str = Field(default="user") # user, moderator, admin
|
|
bio: Optional[str] = Field(default=None)
|
|
avatar: Optional[str] = Field(default=None)
|
|
avatar_bg_color: Optional[str] = Field(default="#3B82F6", description="Hex color for avatar background")
|
|
avatar_text: Optional[str] = Field(default=None, description="1-3 character text overlay on avatar")
|
|
|
|
# Privacy settings
|
|
profile_public: bool = Field(default=True, description="Allow others to view profile")
|
|
show_attendance_public: bool = Field(default=True, description="Show attended shows on profile")
|
|
appear_in_leaderboards: bool = Field(default=True, description="Appear in community leaderboards")
|
|
|
|
# Gamification
|
|
xp: int = Field(default=0, description="Experience points")
|
|
level: int = Field(default=1, description="User level based on XP")
|
|
streak_days: int = Field(default=0, description="Consecutive days active")
|
|
last_activity: Optional[datetime] = Field(default=None)
|
|
|
|
# Social Identity
|
|
bluesky_handle: Optional[str] = Field(default=None)
|
|
mastodon_handle: Optional[str] = Field(default=None)
|
|
instagram_handle: Optional[str] = Field(default=None)
|
|
location: Optional[str] = Field(default=None, description="User's local scene/city")
|
|
|
|
# Custom Titles & Flair (tracker forum style)
|
|
custom_title: Optional[str] = Field(default=None, description="Custom title chosen by user")
|
|
title_color: Optional[str] = Field(default=None, description="Hex color for username display")
|
|
flair: Optional[str] = Field(default=None, description="Small text/emoji beside name")
|
|
is_early_adopter: bool = Field(default=False, description="First 100 users get special perks")
|
|
is_supporter: bool = Field(default=False, description="Donated/supported the platform")
|
|
joined_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
# Email verification
|
|
email_verified: bool = Field(default=False)
|
|
verification_token: Optional[str] = Field(default=None)
|
|
verification_token_expires: Optional[datetime] = Field(default=None)
|
|
|
|
# Password reset
|
|
reset_token: Optional[str] = Field(default=None)
|
|
reset_token_expires: Optional[datetime] = Field(default=None)
|
|
|
|
# Multi-identity support: A user can have multiple Profiles
|
|
profiles: List["Profile"] = Relationship(back_populates="user")
|
|
comments: List["Comment"] = Relationship(back_populates="user")
|
|
ratings: List["Rating"] = Relationship(back_populates="user")
|
|
reviews: List["Review"] = Relationship(back_populates="user")
|
|
attendances: List["Attendance"] = Relationship(back_populates="user")
|
|
badges: List["UserBadge"] = Relationship(back_populates="user")
|
|
preferences: Optional["UserPreferences"] = Relationship(back_populates="user", sa_relationship_kwargs={"uselist": False})
|
|
reports: List["Report"] = Relationship(back_populates="user")
|
|
notifications: List["Notification"] = Relationship(back_populates="user")
|
|
playlists: List["UserPlaylist"] = Relationship(back_populates="user")
|
|
vertical_preferences: List["UserVerticalPreference"] = Relationship(back_populates="user")
|
|
|
|
|
|
class UserPlaylist(SQLModel, table=True):
|
|
"""User-created curated collections of performances"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
name: str = Field(index=True)
|
|
slug: str = Field(index=True)
|
|
description: Optional[str] = Field(default=None)
|
|
is_public: bool = Field(default=True)
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
# Relationships
|
|
user: "User" = Relationship(back_populates="playlists")
|
|
performances: List["PlaylistPerformance"] = Relationship(back_populates="playlist")
|
|
|
|
|
|
class PlaylistPerformance(SQLModel, table=True):
|
|
"""Link table for performances in playlists with ordering"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
playlist_id: int = Field(foreign_key="userplaylist.id")
|
|
performance_id: int = Field(foreign_key="performance.id")
|
|
position: int = Field(description="Order in playlist, 1-indexed")
|
|
added_at: datetime = Field(default_factory=datetime.utcnow)
|
|
notes: Optional[str] = Field(default=None, description="User notes about this performance")
|
|
|
|
playlist: UserPlaylist = Relationship(back_populates="performances")
|
|
|
|
class Report(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
entity_type: str = Field(index=True) # comment, review, nickname
|
|
entity_id: int = Field(index=True)
|
|
reason: str
|
|
details: str = Field(default="")
|
|
status: str = Field(default="pending", index=True) # pending, resolved, dismissed
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
user: "User" = Relationship(back_populates="reports")
|
|
|
|
class Badge(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(unique=True, index=True)
|
|
description: str
|
|
icon: str = Field(description="Lucide icon name or image URL")
|
|
slug: str = Field(unique=True, index=True)
|
|
tier: str = Field(default="bronze", description="bronze, silver, gold, platinum, diamond")
|
|
category: str = Field(default="general", description="attendance, ratings, social, milestones")
|
|
xp_reward: int = Field(default=50, description="XP awarded when badge is earned")
|
|
|
|
class UserBadge(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
badge_id: int = Field(foreign_key="badge.id")
|
|
awarded_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
user: "User" = Relationship(back_populates="badges")
|
|
badge: "Badge" = Relationship()
|
|
|
|
class Review(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
blurb: str = Field(description="One-liner/pullquote")
|
|
content: str = Field(description="Full review text")
|
|
score: float = Field(ge=1.0, le=10.0)
|
|
show_id: Optional[int] = Field(default=None, foreign_key="show.id")
|
|
venue_id: Optional[int] = Field(default=None, foreign_key="venue.id")
|
|
song_id: Optional[int] = Field(default=None, foreign_key="song.id")
|
|
performance_id: Optional[int] = Field(default=None, foreign_key="performance.id")
|
|
tour_id: Optional[int] = Field(default=None, foreign_key="tour.id")
|
|
year: Optional[int] = Field(default=None, description="For reviewing a specific year")
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
user: "User" = Relationship(back_populates="reviews")
|
|
|
|
class UserPreferences(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id", unique=True)
|
|
wiki_mode: bool = Field(default=False, description="Disable social features")
|
|
show_ratings: bool = Field(default=True)
|
|
show_comments: bool = Field(default=True)
|
|
|
|
# Theme preference (synced from frontend)
|
|
theme: str = Field(default="system", description="light, dark, or system")
|
|
|
|
# Email notification preferences
|
|
email_on_reply: bool = Field(default=True, description="Email when someone replies to your review")
|
|
email_on_chase: bool = Field(default=True, description="Email when your chase song is played")
|
|
email_digest: bool = Field(default=False, description="Weekly digest email")
|
|
|
|
user: "User" = Relationship(back_populates="preferences")
|
|
|
|
|
|
|
|
class Profile(SQLModel, table=True):
|
|
"""A user's identity within a specific context or global"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
username: str = Field(index=True)
|
|
display_name: Optional[str] = Field(default=None)
|
|
|
|
user: User = Relationship(back_populates="profiles")
|
|
|
|
# --- Groups ---
|
|
class Group(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: str = Field(index=True, unique=True)
|
|
description: Optional[str] = None
|
|
privacy: str = Field(default="public") # public, private, invite_only
|
|
created_by: int = Field(foreign_key="user.id")
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
# Vertical scoping (optional - null means cross-band group)
|
|
vertical_id: Optional[int] = Field(default=None, foreign_key="vertical.id", index=True)
|
|
image_url: Optional[str] = Field(default=None, description="Group logo/image URL")
|
|
|
|
members: List["GroupMember"] = Relationship(back_populates="group")
|
|
posts: List["GroupPost"] = Relationship(back_populates="group")
|
|
|
|
class GroupMember(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
group_id: int = Field(foreign_key="group.id")
|
|
user_id: int = Field(foreign_key="user.id")
|
|
role: str = Field(default="member") # member, admin
|
|
joined_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
group: Group = Relationship(back_populates="members")
|
|
user: User = Relationship()
|
|
|
|
class GroupPost(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
group_id: int = Field(foreign_key="group.id")
|
|
user_id: int = Field(foreign_key="user.id")
|
|
content: str
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
group: Group = Relationship(back_populates="posts")
|
|
user: User = Relationship()
|
|
|
|
|
|
class Reaction(SQLModel, table=True):
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id")
|
|
entity_type: str = Field(index=True) # "review", "comment"
|
|
entity_id: int = Field(index=True)
|
|
emoji: str # "❤️", "🔥", etc.
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
user: User = Relationship()
|
|
|
|
class ChaseSong(SQLModel, table=True):
|
|
"""Songs a user wants to see live (hasn't seen performed yet or wants to see again)"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
user_id: int = Field(foreign_key="user.id", index=True)
|
|
song_id: int = Field(foreign_key="song.id", index=True)
|
|
priority: int = Field(default=1, description="1=high, 2=medium, 3=low")
|
|
notes: Optional[str] = Field(default=None)
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
caught_at: Optional[datetime] = Field(default=None, description="When they finally saw it")
|
|
caught_show_id: Optional[int] = Field(default=None, foreign_key="show.id")
|
|
|
|
user: User = Relationship()
|
|
song: "Song" = Relationship()
|
|
|
|
|
|
# --- Video System ---
|
|
|
|
class VideoType(str, Enum):
|
|
FULL_SHOW = "full_show" # Complete show recording
|
|
SINGLE_SONG = "single_song" # Individual song performance
|
|
SEQUENCE = "sequence" # Multi-song sequence
|
|
INTERVIEW = "interview" # Artist interview
|
|
DOCUMENTARY = "documentary" # Documentary/behind the scenes
|
|
LIVE_STREAM = "live_stream" # Live stream recording
|
|
OTHER = "other"
|
|
|
|
|
|
class VideoPlatform(str, Enum):
|
|
YOUTUBE = "youtube"
|
|
VIMEO = "vimeo"
|
|
NUGS = "nugs"
|
|
BANDCAMP = "bandcamp"
|
|
ARCHIVE = "archive" # archive.org
|
|
OTHER = "other"
|
|
|
|
|
|
class Video(SQLModel, table=True):
|
|
"""Modular video entity that can be linked to multiple entities"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
url: str = Field(index=True, description="Full video URL")
|
|
title: Optional[str] = Field(default=None, description="Video title")
|
|
description: Optional[str] = Field(default=None)
|
|
platform: VideoPlatform = Field(default=VideoPlatform.YOUTUBE)
|
|
video_type: VideoType = Field(default=VideoType.SINGLE_SONG)
|
|
|
|
# Metadata
|
|
duration_seconds: Optional[int] = Field(default=None)
|
|
thumbnail_url: Optional[str] = Field(default=None)
|
|
external_id: Optional[str] = Field(default=None, description="Platform-specific ID (e.g., YouTube video ID)")
|
|
|
|
# Timestamps
|
|
recorded_date: Optional[datetime] = Field(default=None, description="When the video was recorded")
|
|
published_date: Optional[datetime] = Field(default=None, description="When published to platform")
|
|
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
|
|
# Optional vertical scoping
|
|
vertical_id: Optional[int] = Field(default=None, foreign_key="vertical.id", index=True)
|
|
|
|
# Relationships
|
|
shows: List["VideoShow"] = Relationship(back_populates="video")
|
|
performances: List["VideoPerformance"] = Relationship(back_populates="video")
|
|
songs: List["VideoSong"] = Relationship(back_populates="video")
|
|
musicians: List["VideoMusician"] = Relationship(back_populates="video")
|
|
|
|
|
|
class VideoShow(SQLModel, table=True):
|
|
"""Junction table linking videos to shows"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
video_id: int = Field(foreign_key="video.id", index=True)
|
|
show_id: int = Field(foreign_key="show.id", index=True)
|
|
notes: Optional[str] = Field(default=None, description="Context for this link")
|
|
|
|
video: Video = Relationship(back_populates="shows")
|
|
show: "Show" = Relationship()
|
|
|
|
|
|
class VideoPerformance(SQLModel, table=True):
|
|
"""Junction table linking videos to specific performances"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
video_id: int = Field(foreign_key="video.id", index=True)
|
|
performance_id: int = Field(foreign_key="performance.id", index=True)
|
|
timestamp_start: Optional[int] = Field(default=None, description="Start time in seconds for this performance in the video")
|
|
timestamp_end: Optional[int] = Field(default=None, description="End time in seconds")
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
video: Video = Relationship(back_populates="performances")
|
|
performance: "Performance" = Relationship(back_populates="video_links")
|
|
|
|
|
|
class VideoSong(SQLModel, table=True):
|
|
"""Junction table linking videos to songs (general, not performance-specific)"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
video_id: int = Field(foreign_key="video.id", index=True)
|
|
song_id: int = Field(foreign_key="song.id", index=True)
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
video: Video = Relationship(back_populates="songs")
|
|
song: "Song" = Relationship()
|
|
|
|
|
|
class VideoMusician(SQLModel, table=True):
|
|
"""Junction table linking videos to musicians (for interviews, documentaries, etc.)"""
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
video_id: int = Field(foreign_key="video.id", index=True)
|
|
musician_id: int = Field(foreign_key="musician.id", index=True)
|
|
role: Optional[str] = Field(default=None, description="Role in video: 'featured', 'interview', 'performance'")
|
|
notes: Optional[str] = Field(default=None)
|
|
|
|
video: Video = Relationship(back_populates="musicians")
|
|
musician: "Musician" = Relationship()
|