From 56f52de7fca8d903346e901b6204f65723213758 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Sun, 28 Dec 2025 14:51:22 -0800 Subject: [PATCH] feat: Add cross-band entity relationship models Schema additions for fediversion multi-band architecture: - SongCanon: Canonical master songs for cross-band linking (e.g., 'Dark Star' links GD, D&C, Billy Strings versions) - UserVerticalPreference: User band display preferences - display_mode: primary/secondary/attribution_only/hidden - priority: Sort order for UI - notify_on_show: Per-band notification settings - Vertical additions: - primary_artist_id: Link to main Artist entity - color: Hex branding color - emoji: Display emoji - Song additions: - canon_id: Link to SongCanon for cross-band tracking --- backend/models.py | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/backend/models.py b/backend/models.py index 105177b..12421df 100644 --- a/backend/models.py +++ b/backend/models.py @@ -61,6 +61,13 @@ class Vertical(SQLModel, table=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") + + # Theming + color: Optional[str] = Field(default=None, description="Hex color for branding") + emoji: Optional[str] = Field(default=None, description="Display emoji") + shows: List["Show"] = Relationship(back_populates="vertical") songs: List["Song"] = Relationship(back_populates="vertical") @@ -155,6 +162,18 @@ class Show(SQLModel, table=True): 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) @@ -164,11 +183,15 @@ 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") + # 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") - vertical: Vertical = Relationship(back_populates="songs") + # 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""" @@ -346,7 +369,20 @@ class UserPreferences(SQLModel, table=True): 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") + user: "User" = Relationship(back_populates="preferences") + +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) + display_mode: str = Field(default="primary", description="primary, secondary, attribution_only, hidden") + priority: int = Field(default=0, description="Sort order - lower = higher priority") + notify_on_show: bool = Field(default=True, description="Notify when this band plays a show") + created_at: datetime = Field(default_factory=datetime.utcnow) + + user: "User" = Relationship() + vertical: "Vertical" = Relationship() class Profile(SQLModel, table=True): """A user's identity within a specific context or global"""