244 lines
7.3 KiB
Python
244 lines
7.3 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)
|
|
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,
|
|
"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
|