from datetime import datetime from typing import List, Optional, TYPE_CHECKING from sqlalchemy import String, Integer, ForeignKey, DateTime, BigInteger, Enum as SQLEnum, func, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship import enum from ..database import Base if TYPE_CHECKING: from ..db_models import Opening class ThemeType(str, enum.Enum): """Type of anime theme (opening or ending).""" OP = "OP" ED = "ED" class DownloadStatus(str, enum.Enum): """Status of a download task.""" QUEUED = "queued" DOWNLOADING = "downloading" CONVERTING = "converting" UPLOADING = "uploading" DONE = "done" FAILED = "failed" class Anime(Base): """Anime entity from AnimeThemes.""" __tablename__ = "anime" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) animethemes_slug: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) title_russian: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) title_english: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) title_japanese: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) year: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) poster_url: Mapped[Optional[str]] = mapped_column(String(1024), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) # Relationships themes: Mapped[List["AnimeTheme"]] = relationship( "AnimeTheme", back_populates="anime", cascade="all, delete-orphan" ) def __repr__(self): return f"" class AnimeTheme(Base): """Anime opening/ending theme.""" __tablename__ = "anime_themes" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) anime_id: Mapped[int] = mapped_column( Integer, ForeignKey("anime.id", ondelete="CASCADE"), nullable=False ) theme_type: Mapped[ThemeType] = mapped_column( SQLEnum(ThemeType, native_enum=False), nullable=False ) sequence: Mapped[int] = mapped_column(Integer, nullable=False, default=1) # 1, 2, 3... song_title: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) artist: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # AnimeThemes video URL (WebM source) animethemes_video_url: Mapped[Optional[str]] = mapped_column(String(1024), nullable=True) # Downloaded file info audio_s3_key: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) file_size_bytes: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True) # Link to existing Opening entity (after download) opening_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("openings.id", ondelete="SET NULL"), nullable=True ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) # Unique constraint: one anime can have only one OP1, OP2, ED1, etc. __table_args__ = ( UniqueConstraint('anime_id', 'theme_type', 'sequence', name='uq_anime_theme_sequence'), ) # Relationships anime: Mapped["Anime"] = relationship("Anime", back_populates="themes") opening: Mapped[Optional["Opening"]] = relationship("Opening") @property def full_name(self) -> str: """Return full theme name like 'OP1' or 'ED2'.""" return f"{self.theme_type.value}{self.sequence}" @property def is_downloaded(self) -> bool: """Check if theme has been downloaded.""" return self.audio_s3_key is not None def __repr__(self): return f"" class DownloadTask(Base): """Download queue task.""" __tablename__ = "download_tasks" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) theme_id: Mapped[int] = mapped_column( Integer, ForeignKey("anime_themes.id", ondelete="CASCADE"), nullable=False, unique=True # One task per theme ) status: Mapped[DownloadStatus] = mapped_column( SQLEnum(DownloadStatus, native_enum=False), nullable=False, default=DownloadStatus.QUEUED ) # Progress tracking progress_percent: Mapped[int] = mapped_column(Integer, default=0) error_message: Mapped[Optional[str]] = mapped_column(String(1024), nullable=True) # Estimated size (6 MB default if unknown) estimated_size_bytes: Mapped[int] = mapped_column(BigInteger, default=6_291_456) # 6 MB created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now() ) started_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) # Relationships theme: Mapped["AnimeTheme"] = relationship("AnimeTheme") def __repr__(self): return f""