Files
mamonov.ep cc11f0b773 Remove Shikimori API, use AnimeThemes only, switch to WebM format
- Remove ShikimoriService, use AnimeThemes API for search
- Replace shikimori_id with animethemes_slug as primary identifier
- Remove FFmpeg MP3 conversion, download WebM directly
- Add .webm support in storage and upload endpoints
- Update frontend to use animethemes_slug

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 11:22:46 +03:00

157 lines
5.2 KiB
Python

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"<Anime {self.animethemes_slug}: {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}>"