"""Sound model for storing sound file information.""" from datetime import datetime from enum import Enum from typing import TYPE_CHECKING, Optional from zoneinfo import ZoneInfo from sqlalchemy import Boolean, DateTime, Integer, String from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database import db if TYPE_CHECKING: from app.models.playlist_sound import PlaylistSound class SoundType(Enum): """Sound type enumeration.""" SDB = "SDB" # Soundboard sound SAY = "SAY" # Text-to-speech STR = "STR" # Stream sound class Sound(db.Model): """Sound model for storing sound file information.""" __tablename__ = "sounds" id: Mapped[int] = mapped_column(primary_key=True) # Sound type (SDB, SAY, or STR) type: Mapped[str] = mapped_column(String(3), nullable=False) # Basic sound information name: Mapped[str] = mapped_column(String(255), nullable=False) filename: Mapped[str] = mapped_column(String(500), nullable=False) duration: Mapped[int] = mapped_column(Integer, nullable=False) size: Mapped[int] = mapped_column(Integer, nullable=False) # Size in bytes hash: Mapped[str] = mapped_column(String(64), nullable=False) # SHA256 hash # Normalized sound information normalized_filename: Mapped[str | None] = mapped_column( String(500), nullable=True, ) normalized_duration: Mapped[int | None] = mapped_column( Integer, nullable=True, ) normalized_size: Mapped[int | None] = mapped_column( Integer, nullable=True, ) normalized_hash: Mapped[str | None] = mapped_column( String(64), nullable=True, ) # Sound properties is_normalized: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, ) is_music: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, ) is_deletable: Mapped[bool] = mapped_column( Boolean, nullable=False, default=True, ) # Usage tracking play_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) # Timestamps created_at: Mapped[datetime] = mapped_column( DateTime, default=lambda: datetime.now(tz=ZoneInfo("UTC")), nullable=False, ) updated_at: Mapped[datetime] = mapped_column( DateTime, default=lambda: datetime.now(tz=ZoneInfo("UTC")), onupdate=lambda: datetime.now(tz=ZoneInfo("UTC")), nullable=False, ) # Relationships playlist_sounds: Mapped[list["PlaylistSound"]] = relationship( "PlaylistSound", back_populates="sound", cascade="all, delete-orphan", ) def __repr__(self) -> str: """String representation of Sound.""" return f"" def to_dict(self) -> dict: """Convert sound to dictionary.""" return { "id": self.id, "type": self.type, "name": self.name, "filename": self.filename, "duration": self.duration, "size": self.size, "hash": self.hash, "normalized_filename": self.normalized_filename, "normalized_duration": self.normalized_duration, "normalized_size": self.normalized_size, "normalized_hash": self.normalized_hash, "is_normalized": self.is_normalized, "is_music": self.is_music, "is_deletable": self.is_deletable, "play_count": self.play_count, "created_at": self.created_at.isoformat(), "updated_at": self.updated_at.isoformat(), } def increment_play_count(self) -> None: """Increment the play count for this sound.""" self.play_count += 1 self.updated_at = datetime.now(tz=ZoneInfo("UTC")) db.session.commit() def set_normalized_info( self, normalized_filename: str, normalized_duration: float, normalized_size: int, normalized_hash: str, ) -> None: """Set normalized sound information.""" self.normalized_filename = normalized_filename self.normalized_duration = normalized_duration self.normalized_size = normalized_size self.normalized_hash = normalized_hash self.is_normalized = True self.updated_at = datetime.now(tz=ZoneInfo("UTC")) def clear_normalized_info(self) -> None: """Clear normalized sound information.""" self.normalized_filename = None self.normalized_duration = None self.normalized_hash = None self.normalized_size = None self.is_normalized = False self.updated_at = datetime.now(tz=ZoneInfo("UTC")) def update_file_info( self, filename: str, duration: float, size: int, hash_value: str, ) -> None: """Update file information for existing sound.""" self.filename = filename self.duration = duration self.size = size self.hash = hash_value self.updated_at = datetime.now(tz=ZoneInfo("UTC")) @classmethod def find_by_hash(cls, hash_value: str) -> Optional["Sound"]: """Find sound by hash.""" return cls.query.filter_by(hash=hash_value).first() @classmethod def find_by_name(cls, name: str) -> Optional["Sound"]: """Find sound by name.""" return cls.query.filter_by(name=name).first() @classmethod def find_by_filename(cls, filename: str) -> Optional["Sound"]: """Find sound by filename.""" return cls.query.filter_by(filename=filename).first() @classmethod def find_by_type(cls, sound_type: str) -> list["Sound"]: """Find all sounds by type.""" return cls.query.filter_by(type=sound_type).all() @classmethod def get_most_played(cls, limit: int = 10) -> list["Sound"]: """Get the most played sounds.""" return cls.query.order_by(cls.play_count.desc()).limit(limit).all() @classmethod def get_music_sounds(cls) -> list["Sound"]: """Get all music sounds.""" return cls.query.filter_by(is_music=True).all() @classmethod def get_deletable_sounds(cls) -> list["Sound"]: """Get all deletable sounds.""" return cls.query.filter_by(is_deletable=True).all() @classmethod def create_sound( cls, sound_type: str, name: str, filename: str, duration: float, size: int, hash_value: str, is_music: bool = False, is_deletable: bool = True, commit: bool = True, ) -> "Sound": """Create a new sound.""" # Validate sound type if sound_type not in [t.value for t in SoundType]: raise ValueError(f"Invalid sound type: {sound_type}") sound = cls( type=sound_type, name=name, filename=filename, duration=duration, size=size, hash=hash_value, is_music=is_music, is_deletable=is_deletable, ) db.session.add(sound) if commit: db.session.commit() return sound