402 lines
18 KiB
Python
Executable File
402 lines
18 KiB
Python
Executable File
"""All SQLAlchemy 2.0 ORM models for RehearsalHub."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from sqlalchemy import (
|
|
BigInteger,
|
|
Boolean,
|
|
DateTime,
|
|
ForeignKey,
|
|
Integer,
|
|
Numeric,
|
|
String,
|
|
Text,
|
|
UniqueConstraint,
|
|
func,
|
|
)
|
|
from sqlalchemy.dialects.postgresql import ARRAY, JSONB, UUID
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
# ── Members ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class Member(Base):
|
|
__tablename__ = "members"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
email: Mapped[str] = mapped_column(String(320), unique=True, nullable=False, index=True)
|
|
display_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
avatar_url: Mapped[Optional[str]] = mapped_column(Text)
|
|
nc_username: Mapped[Optional[str]] = mapped_column(String(255))
|
|
nc_url: Mapped[Optional[str]] = mapped_column(Text)
|
|
nc_password: Mapped[Optional[str]] = mapped_column(Text)
|
|
password_hash: Mapped[str] = mapped_column(Text, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
band_memberships: Mapped[list[BandMember]] = relationship(
|
|
"BandMember", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
authored_songs: Mapped[list[Song]] = relationship("Song", back_populates="creator")
|
|
uploaded_versions: Mapped[list[AudioVersion]] = relationship(
|
|
"AudioVersion", back_populates="uploader"
|
|
)
|
|
annotations: Mapped[list[Annotation]] = relationship(
|
|
"Annotation", back_populates="author", foreign_keys="Annotation.author_id"
|
|
)
|
|
reactions: Mapped[list[Reaction]] = relationship(
|
|
"Reaction", back_populates="member", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
# ── Bands ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class Band(Base):
|
|
__tablename__ = "bands"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
slug: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
|
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text)
|
|
nc_user: Mapped[Optional[str]] = mapped_column(String(255))
|
|
genre_tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
|
)
|
|
|
|
memberships: Mapped[list[BandMember]] = relationship(
|
|
"BandMember", back_populates="band", cascade="all, delete-orphan"
|
|
)
|
|
songs: Mapped[list[Song]] = relationship(
|
|
"Song", back_populates="band", cascade="all, delete-orphan"
|
|
)
|
|
sessions: Mapped[list[RehearsalSession]] = relationship(
|
|
"RehearsalSession", back_populates="band", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class BandMember(Base):
|
|
__tablename__ = "band_members"
|
|
__table_args__ = (UniqueConstraint("band_id", "member_id", name="uq_band_member"),)
|
|
|
|
band_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), primary_key=True
|
|
)
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), primary_key=True
|
|
)
|
|
role: Mapped[str] = mapped_column(String(20), nullable=False, default="member")
|
|
joined_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
instrument: Mapped[Optional[str]] = mapped_column(String(100))
|
|
|
|
band: Mapped[Band] = relationship("Band", back_populates="memberships")
|
|
member: Mapped[Member] = relationship("Member", back_populates="band_memberships")
|
|
|
|
|
|
class BandInvite(Base):
|
|
__tablename__ = "band_invites"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
band_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
token: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)
|
|
role: Mapped[str] = mapped_column(String(20), nullable=False, default="member")
|
|
created_by: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
|
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
used_by: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
|
|
)
|
|
|
|
band: Mapped[Band] = relationship("Band")
|
|
creator: Mapped[Member] = relationship("Member", foreign_keys=[created_by])
|
|
|
|
|
|
# ── Rehearsal Sessions ────────────────────────────────────────────────────────
|
|
|
|
|
|
class RehearsalSession(Base):
|
|
__tablename__ = "rehearsal_sessions"
|
|
__table_args__ = (UniqueConstraint("band_id", "date", name="uq_session_band_date"),)
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
band_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
date: Mapped[datetime] = mapped_column(DateTime(timezone=False), nullable=False)
|
|
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text)
|
|
label: Mapped[Optional[str]] = mapped_column(String(255))
|
|
notes: Mapped[Optional[str]] = mapped_column(Text)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
band: Mapped[Band] = relationship("Band", back_populates="sessions")
|
|
songs: Mapped[list[Song]] = relationship("Song", back_populates="session")
|
|
|
|
|
|
# ── Songs ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class Song(Base):
|
|
__tablename__ = "songs"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
band_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
session_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("rehearsal_sessions.id", ondelete="SET NULL"), index=True
|
|
)
|
|
title: Mapped[str] = mapped_column(String(500), nullable=False)
|
|
nc_folder_path: Mapped[Optional[str]] = mapped_column(Text)
|
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="jam")
|
|
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
|
|
global_key: Mapped[Optional[str]] = mapped_column(String(30))
|
|
global_bpm: Mapped[Optional[float]] = mapped_column(Numeric(6, 2))
|
|
notes: Mapped[Optional[str]] = mapped_column(Text)
|
|
created_by: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
|
)
|
|
|
|
band: Mapped[Band] = relationship("Band", back_populates="songs")
|
|
session: Mapped[Optional[RehearsalSession]] = relationship("RehearsalSession", back_populates="songs")
|
|
creator: Mapped[Optional[Member]] = relationship("Member", back_populates="authored_songs")
|
|
versions: Mapped[list[AudioVersion]] = relationship(
|
|
"AudioVersion", back_populates="song", cascade="all, delete-orphan"
|
|
)
|
|
comments: Mapped[list[SongComment]] = relationship(
|
|
"SongComment", back_populates="song", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
class SongComment(Base):
|
|
__tablename__ = "song_comments"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
song_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("songs.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
author_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
body: Mapped[str] = mapped_column(Text, nullable=False)
|
|
timestamp: Mapped[Optional[float]] = mapped_column(Numeric(10, 2), nullable=True)
|
|
tag: Mapped[Optional[str]] = mapped_column(String(32), nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
song: Mapped[Song] = relationship("Song", back_populates="comments")
|
|
author: Mapped[Member] = relationship("Member")
|
|
|
|
|
|
# ── Audio Versions ────────────────────────────────────────────────────────────
|
|
|
|
|
|
class AudioVersion(Base):
|
|
__tablename__ = "audio_versions"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
song_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("songs.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
version_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
label: Mapped[Optional[str]] = mapped_column(String(255))
|
|
nc_file_path: Mapped[str] = mapped_column(Text, nullable=False)
|
|
nc_file_etag: Mapped[Optional[str]] = mapped_column(String(255))
|
|
cdn_hls_base: Mapped[Optional[str]] = mapped_column(Text)
|
|
waveform_url: Mapped[Optional[str]] = mapped_column(Text)
|
|
duration_ms: Mapped[Optional[int]] = mapped_column(Integer)
|
|
format: Mapped[Optional[str]] = mapped_column(String(10))
|
|
file_size_bytes: Mapped[Optional[int]] = mapped_column(BigInteger)
|
|
analysis_status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
|
|
uploaded_by: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("members.id", ondelete="SET NULL")
|
|
)
|
|
uploaded_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
song: Mapped[Song] = relationship("Song", back_populates="versions")
|
|
uploader: Mapped[Optional[Member]] = relationship(
|
|
"Member", back_populates="uploaded_versions"
|
|
)
|
|
annotations: Mapped[list[Annotation]] = relationship(
|
|
"Annotation", back_populates="version", cascade="all, delete-orphan"
|
|
)
|
|
range_analyses: Mapped[list[RangeAnalysis]] = relationship(
|
|
"RangeAnalysis", back_populates="version", cascade="all, delete-orphan"
|
|
)
|
|
|
|
|
|
# ── Annotations ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
class Annotation(Base):
|
|
__tablename__ = "annotations"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
version_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("audio_versions.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
author_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
type: Mapped[str] = mapped_column(String(10), nullable=False) # 'point' | 'range'
|
|
timestamp_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
range_end_ms: Mapped[Optional[int]] = mapped_column(Integer)
|
|
body: Mapped[Optional[str]] = mapped_column(Text)
|
|
voice_note_url: Mapped[Optional[str]] = mapped_column(Text)
|
|
label: Mapped[Optional[str]] = mapped_column(String(255))
|
|
tags: Mapped[list[str]] = mapped_column(ARRAY(Text), default=list, nullable=False)
|
|
parent_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("annotations.id", ondelete="SET NULL")
|
|
)
|
|
resolved: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
|
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
|
)
|
|
|
|
version: Mapped[AudioVersion] = relationship("AudioVersion", back_populates="annotations")
|
|
author: Mapped[Member] = relationship(
|
|
"Member", back_populates="annotations", foreign_keys=[author_id]
|
|
)
|
|
replies: Mapped[list[Annotation]] = relationship(
|
|
"Annotation", foreign_keys=[parent_id], back_populates="parent"
|
|
)
|
|
parent: Mapped[Optional[Annotation]] = relationship(
|
|
"Annotation", foreign_keys=[parent_id], back_populates="replies", remote_side=[id]
|
|
)
|
|
reactions: Mapped[list[Reaction]] = relationship(
|
|
"Reaction", back_populates="annotation", cascade="all, delete-orphan"
|
|
)
|
|
range_analysis: Mapped[Optional[RangeAnalysis]] = relationship(
|
|
"RangeAnalysis", back_populates="annotation", uselist=False
|
|
)
|
|
|
|
|
|
# ── Range Analyses ────────────────────────────────────────────────────────────
|
|
|
|
|
|
class RangeAnalysis(Base):
|
|
__tablename__ = "range_analyses"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
annotation_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("annotations.id", ondelete="CASCADE"),
|
|
unique=True,
|
|
nullable=False,
|
|
)
|
|
version_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("audio_versions.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
start_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
end_ms: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
bpm: Mapped[Optional[float]] = mapped_column(Numeric(7, 2))
|
|
bpm_confidence: Mapped[Optional[float]] = mapped_column(Numeric(4, 3))
|
|
key: Mapped[Optional[str]] = mapped_column(String(30))
|
|
key_confidence: Mapped[Optional[float]] = mapped_column(Numeric(4, 3))
|
|
scale: Mapped[Optional[str]] = mapped_column(String(10))
|
|
avg_loudness_lufs: Mapped[Optional[float]] = mapped_column(Numeric(6, 2))
|
|
peak_loudness_dbfs: Mapped[Optional[float]] = mapped_column(Numeric(6, 2))
|
|
spectral_centroid: Mapped[Optional[float]] = mapped_column(Numeric(10, 2))
|
|
energy: Mapped[Optional[float]] = mapped_column(Numeric(5, 4))
|
|
danceability: Mapped[Optional[float]] = mapped_column(Numeric(5, 4))
|
|
chroma_vector: Mapped[Optional[list[float]]] = mapped_column(ARRAY(Numeric))
|
|
mfcc_mean: Mapped[Optional[list[float]]] = mapped_column(ARRAY(Numeric))
|
|
analysis_version: Mapped[Optional[str]] = mapped_column(String(20))
|
|
computed_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
annotation: Mapped[Annotation] = relationship(
|
|
"Annotation", back_populates="range_analysis"
|
|
)
|
|
version: Mapped[AudioVersion] = relationship(
|
|
"AudioVersion", back_populates="range_analyses"
|
|
)
|
|
|
|
|
|
# ── Reactions ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class Reaction(Base):
|
|
__tablename__ = "reactions"
|
|
__table_args__ = (
|
|
UniqueConstraint("annotation_id", "member_id", "emoji", name="uq_reaction"),
|
|
)
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
annotation_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True),
|
|
ForeignKey("annotations.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
index=True,
|
|
)
|
|
member_id: Mapped[uuid.UUID] = mapped_column(
|
|
UUID(as_uuid=True), ForeignKey("members.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
emoji: Mapped[str] = mapped_column(String(10), nullable=False)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
annotation: Mapped[Annotation] = relationship("Annotation", back_populates="reactions")
|
|
member: Mapped[Member] = relationship("Member", back_populates="reactions")
|
|
|
|
|
|
# ── Jobs ──────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class Job(Base):
|
|
__tablename__ = "jobs"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
type: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
|
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="queued", index=True)
|
|
attempt: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
error: Mapped[Optional[str]] = mapped_column(Text)
|
|
queued_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|
|
finished_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
|