from __future__ import annotations from datetime import date, datetime from sqlalchemy import Boolean, Date, DateTime, Float, ForeignKey, Integer, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base, TimestampMixin class User(Base, TimestampMixin): __tablename__ = "users" id: Mapped[int] = mapped_column(primary_key=True) telegram_chat_id: Mapped[str] = mapped_column(String(64), unique=True, index=True) telegram_username: Mapped[str | None] = mapped_column(String(128), nullable=True) spotify_user_id: Mapped[str | None] = mapped_column(String(128), unique=True, nullable=True, index=True) spotify_access_token: Mapped[str | None] = mapped_column(Text, nullable=True) spotify_refresh_token: Mapped[str | None] = mapped_column(Text, nullable=True) spotify_token_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) spotify_scopes: Mapped[str | None] = mapped_column(Text, nullable=True) timezone: Mapped[str] = mapped_column(String(64), default="UTC") is_active: Mapped[bool] = mapped_column(Boolean, default=True) playlist_size: Mapped[int] = mapped_column(Integer, default=30) min_new_ratio: Mapped[float] = mapped_column(Float, default=0.8) last_generated_date: Mapped[date | None] = mapped_column(Date, nullable=True) latest_playlist_id: Mapped[str | None] = mapped_column(String(128), nullable=True) latest_playlist_url: Mapped[str | None] = mapped_column(Text, nullable=True) saved_tracks: Mapped[list["SavedTrack"]] = relationship(back_populates="user", cascade="all, delete-orphan") playlist_runs: Mapped[list["PlaylistRun"]] = relationship(back_populates="user", cascade="all, delete-orphan") rec_history: Mapped[list["RecommendationHistory"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) class AuthState(Base): __tablename__ = "auth_states" id: Mapped[int] = mapped_column(primary_key=True) state: Mapped[str] = mapped_column(String(128), unique=True, index=True) telegram_chat_id: Mapped[str] = mapped_column(String(64), index=True) expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) class SavedTrack(Base): __tablename__ = "saved_tracks" __table_args__ = (UniqueConstraint("user_id", "spotify_track_id", name="uq_saved_track_user_track"),) id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) spotify_track_id: Mapped[str] = mapped_column(String(128), index=True) name: Mapped[str] = mapped_column(Text) artist_names: Mapped[str] = mapped_column(Text) artist_ids_csv: Mapped[str] = mapped_column(Text) album_name: Mapped[str | None] = mapped_column(Text, nullable=True) added_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) popularity: Mapped[int | None] = mapped_column(Integer, nullable=True) user: Mapped[User] = relationship(back_populates="saved_tracks") class RecommendationHistory(Base): __tablename__ = "recommendation_history" __table_args__ = (UniqueConstraint("user_id", "spotify_track_id", name="uq_rec_history_user_track"),) id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) spotify_track_id: Mapped[str] = mapped_column(String(128), index=True) first_recommended_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) last_recommended_at: Mapped[datetime] = mapped_column(DateTime(timezone=True)) times_recommended: Mapped[int] = mapped_column(Integer, default=1) user: Mapped[User] = relationship(back_populates="rec_history") class PlaylistRun(Base, TimestampMixin): __tablename__ = "playlist_runs" id: Mapped[int] = mapped_column(primary_key=True) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True) run_date: Mapped[date] = mapped_column(Date) status: Mapped[str] = mapped_column(String(32), default="pending") playlist_id: Mapped[str | None] = mapped_column(String(128), nullable=True) playlist_name: Mapped[str | None] = mapped_column(Text, nullable=True) playlist_url: Mapped[str | None] = mapped_column(Text, nullable=True) total_tracks: Mapped[int] = mapped_column(Integer, default=0) new_tracks: Mapped[int] = mapped_column(Integer, default=0) reused_tracks: Mapped[int] = mapped_column(Integer, default=0) notes: Mapped[str | None] = mapped_column(Text, nullable=True) user: Mapped[User] = relationship(back_populates="playlist_runs") tracks: Mapped[list["PlaylistRunTrack"]] = relationship(back_populates="run", cascade="all, delete-orphan") class PlaylistRunTrack(Base): __tablename__ = "playlist_run_tracks" __table_args__ = ( UniqueConstraint("run_id", "spotify_track_id", name="uq_run_track"), UniqueConstraint("run_id", "position", name="uq_run_position"), ) id: Mapped[int] = mapped_column(primary_key=True) run_id: Mapped[int] = mapped_column(ForeignKey("playlist_runs.id", ondelete="CASCADE"), index=True) spotify_track_id: Mapped[str] = mapped_column(String(128), index=True) name: Mapped[str] = mapped_column(Text) artist_names: Mapped[str] = mapped_column(Text) source: Mapped[str] = mapped_column(String(64)) position: Mapped[int] = mapped_column(Integer) is_new_to_bot: Mapped[bool] = mapped_column(Boolean, default=True) run: Mapped[PlaylistRun] = relationship(back_populates="tracks")