Files
sdb-back/app/models/sound.py

228 lines
6.9 KiB
Python

"""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"<Sound {self.name} ({self.type}) - {self.play_count} plays>"
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