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

235 lines
7.0 KiB
Python

"""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
from app.models.stream import Stream
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__ = "sound"
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)
thumbnail: Mapped[str | None] = mapped_column(
String(500), nullable=True
) # Thumbnail filename
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",
)
streams: Mapped[list["Stream"]] = relationship(
"Stream",
back_populates="sound",
cascade="all, delete-orphan",
)
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,
"thumbnail": self.thumbnail,
"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 create_sound(
cls,
sound_type: str,
name: str,
filename: str,
duration: float,
size: int,
hash_value: str,
thumbnail: Optional[str] = None,
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,
thumbnail=thumbnail,
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