add download service
This commit is contained in:
157
backend/app/openings_downloader/db_models.py
Normal file
157
backend/app/openings_downloader/db_models.py
Normal file
@@ -0,0 +1,157 @@
|
||||
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 Shikimori."""
|
||||
__tablename__ = "anime"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
shikimori_id: Mapped[int] = mapped_column(Integer, unique=True, index=True, nullable=False)
|
||||
animethemes_slug: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, index=True)
|
||||
|
||||
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"<Anime {self.shikimori_id}: {self.title_russian or self.title_english}>"
|
||||
|
||||
|
||||
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"<AnimeTheme {self.full_name}: {self.song_title}>"
|
||||
|
||||
|
||||
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"<DownloadTask {self.id}: {self.status.value}>"
|
||||
Reference in New Issue
Block a user