import uuid from datetime import datetime, timezone from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, Float, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.database import Base def new_uuid() -> str: return str(uuid.uuid4()) def now_utc() -> datetime: return datetime.now(timezone.utc) # ── User ────────────────────────────────────────────────────────────────────── class User(Base): __tablename__ = "users" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) full_name: Mapped[str] = mapped_column(String(128), default="") password_hash: Mapped[str] = mapped_column(String(255), nullable=False) is_verified: Mapped[bool] = mapped_column(Boolean, default=False) verify_token: Mapped[str|None] = mapped_column(String(64), nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) projects: Mapped[list["Project"]] = relationship("Project", back_populates="owner") memberships: Mapped[list["ProjectMember"]] = relationship("ProjectMember", back_populates="user") songs: Mapped[list["Song"]] = relationship("Song", back_populates="owner") alt_ratings: Mapped[list["DanceAltRating"]] = relationship("DanceAltRating", back_populates="user") playlist_shares: Mapped[list["PlaylistShare"]] = relationship("PlaylistShare", foreign_keys="PlaylistShare.shared_with_id", back_populates="shared_with") # ── Song ────────────────────────────────────────────────────────────────────── class Song(Base): __tablename__ = "songs" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) owner_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) title: Mapped[str] = mapped_column(String(255), nullable=False) artist: Mapped[str] = mapped_column(String(255), default="") album: Mapped[str] = mapped_column(String(255), default="") bpm: Mapped[int] = mapped_column(Integer, default=0) duration_sec: Mapped[int] = mapped_column(Integer, default=0) file_format: Mapped[str] = mapped_column(String(8), default="") mbid: Mapped[str|None] = mapped_column(String(36), nullable=True) acoustid: Mapped[str|None] = mapped_column(String(64), nullable=True) synced_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) owner: Mapped["User"] = relationship("User", back_populates="songs") project_songs: Mapped[list["ProjectSong"]] = relationship("ProjectSong", back_populates="song") # ── Dans-entitet ────────────────────────────────────────────────────────────── class DanceLevel(Base): __tablename__ = "dance_levels" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) sort_order: Mapped[int] = mapped_column(Integer, nullable=False) name: Mapped[str] = mapped_column(String(64), unique=True, nullable=False) description: Mapped[str] = mapped_column(String(255), default="") class Dance(Base): """Dans-entitet: navn + niveau er unik kombination.""" __tablename__ = "dances" __table_args__ = (UniqueConstraint("name", "level_id", name="uq_dance_name_level"),) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) name: Mapped[str] = mapped_column(String(128), nullable=False) level_id: Mapped[int|None] = mapped_column(Integer, ForeignKey("dance_levels.id"), nullable=True) choreographer: Mapped[str] = mapped_column(String(128), default="") video_url: Mapped[str] = mapped_column(String(512), default="") stepsheet_url: Mapped[str] = mapped_column(String(512), default="") notes: Mapped[str] = mapped_column(Text, default="") use_count: Mapped[int] = mapped_column(Integer, default=1) source: Mapped[str] = mapped_column(String(16), default="local") synced_at: Mapped[datetime|None] = mapped_column(DateTime, nullable=True) level: Mapped["DanceLevel|None"] = relationship("DanceLevel") # ── Project / Playlist ──────────────────────────────────────────────────────── class Project(Base): __tablename__ = "projects" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) owner_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) name: Mapped[str] = mapped_column(String(128), nullable=False) description: Mapped[str] = mapped_column(Text, default="") visibility: Mapped[str] = mapped_column(String(16), default="private") # private|shared|public updated_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc, onupdate=now_utc) created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) owner: Mapped["User"] = relationship("User", back_populates="projects") members: Mapped[list["ProjectMember"]] = relationship("ProjectMember", back_populates="project", cascade="all, delete-orphan") project_songs: Mapped[list["ProjectSong"]] = relationship("ProjectSong", back_populates="project", order_by="ProjectSong.position", cascade="all, delete-orphan") shares: Mapped[list["PlaylistShare"]] = relationship("PlaylistShare", back_populates="project", cascade="all, delete-orphan") class ProjectMember(Base): __tablename__ = "project_members" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False) user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) role: Mapped[str] = mapped_column(String(16), default="viewer") # owner|editor|viewer status: Mapped[str] = mapped_column(String(16), default="pending") # pending|accepted invited_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) project: Mapped["Project"] = relationship("Project", back_populates="members") user: Mapped["User"] = relationship("User", back_populates="memberships") class ProjectSong(Base): __tablename__ = "project_songs" id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False) song_id: Mapped[str] = mapped_column(String(36), ForeignKey("songs.id"), nullable=False) position: Mapped[int] = mapped_column(Integer, nullable=False) status: Mapped[str] = mapped_column(String(16), default="pending") is_workshop: Mapped[bool] = mapped_column(Boolean, default=False) dance_override: Mapped[str] = mapped_column(String(128), default="") project: Mapped["Project"] = relationship("Project", back_populates="project_songs") song: Mapped["Song"] = relationship("Song", back_populates="project_songs") class PlaylistShare(Base): """Deling af en playlist med specifikke brugere.""" __tablename__ = "playlist_shares" __table_args__ = (UniqueConstraint("project_id", "shared_with_id", name="uq_share"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) project_id: Mapped[str] = mapped_column(String(36), ForeignKey("projects.id"), nullable=False) shared_with_id: Mapped[str|None] = mapped_column(String(36), ForeignKey("users.id"), nullable=True) invited_email: Mapped[str] = mapped_column(String(255), default="") # til ikke-registrerede permission: Mapped[str] = mapped_column(String(16), default="view") # view|copy|edit accepted_at: Mapped[datetime|None] = mapped_column(DateTime, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) project: Mapped["Project"] = relationship("Project", back_populates="shares") shared_with: Mapped["User|None"] = relationship("User", foreign_keys=[shared_with_id], back_populates="playlist_shares") # ── Community dans-tags ─────────────────────────────────────────────────────── class CommunityDance(Base): """Fællesskabets dans-tags på sange.""" __tablename__ = "community_dances" __table_args__ = (UniqueConstraint("song_mbid", "song_title", "song_artist", "dance_id", name="uq_comm_dance"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) song_mbid: Mapped[str|None] = mapped_column(String(36), nullable=True) song_title: Mapped[str] = mapped_column(String(255), default="") song_artist: Mapped[str] = mapped_column(String(255), default="") dance_id: Mapped[int] = mapped_column(Integer, ForeignKey("dances.id"), nullable=False) submitted_by: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) dance: Mapped["Dance"] = relationship("Dance") class CommunityDanceAlt(Base): """Fællesskabets alternativ-danse til en sang med ratings.""" __tablename__ = "community_dance_alts" __table_args__ = (UniqueConstraint("song_mbid", "song_title", "song_artist", "alt_dance_id", name="uq_comm_alt"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) song_mbid: Mapped[str|None] = mapped_column(String(36), nullable=True) song_title: Mapped[str] = mapped_column(String(255), default="") song_artist: Mapped[str] = mapped_column(String(255), default="") alt_dance_id: Mapped[int] = mapped_column(Integer, ForeignKey("dances.id"), nullable=False) note: Mapped[str] = mapped_column(Text, default="") submitted_by: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) avg_rating: Mapped[float] = mapped_column(Float, default=0.0) rating_count: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) alt_dance: Mapped["Dance"] = relationship("Dance") ratings: Mapped[list["DanceAltRating"]] = relationship("DanceAltRating", back_populates="alternative", cascade="all, delete-orphan") class DanceAltRating(Base): """1-5 stjerne rating af en alternativ-dans.""" __tablename__ = "dance_alt_ratings" __table_args__ = (UniqueConstraint("alternative_id", "user_id", name="uq_rating"),) id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid) alternative_id: Mapped[str] = mapped_column(String(36), ForeignKey("community_dance_alts.id"), nullable=False) user_id: Mapped[str] = mapped_column(String(36), ForeignKey("users.id"), nullable=False) score: Mapped[int] = mapped_column(Integer, nullable=False) # 1-5 created_at: Mapped[datetime] = mapped_column(DateTime, default=now_utc) alternative: Mapped["CommunityDanceAlt"] = relationship("CommunityDanceAlt", back_populates="ratings") user: Mapped["User"] = relationship("User", back_populates="alt_ratings")