"""Sound model for storing sound file information.""" from datetime import datetime from enum import Enum from typing import Optional from zoneinfo import ZoneInfo from sqlalchemy import Boolean, DateTime, Integer, String from sqlalchemy.orm import Mapped, mapped_column from app.database import db 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, ) 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