Compare commits

..

2 Commits

Author SHA1 Message Date
JSC
c13285ca4e refactor: Update response types to use common schemas across API endpoints
Some checks failed
Backend CI / test (push) Failing after 3m51s
2025-07-31 10:40:03 +02:00
JSC
dc372b961e refactor: Organize and implement player and playlist schemas 2025-07-31 10:23:46 +02:00
7 changed files with 288 additions and 158 deletions

View File

@@ -3,6 +3,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.core.logging import get_logger from app.core.logging import get_logger
from app.schemas.common import HealthResponse
router = APIRouter() router = APIRouter()
@@ -10,7 +11,7 @@ logger = get_logger(__name__)
@router.get("/") @router.get("/")
def health() -> dict[str, str]: def health() -> HealthResponse:
"""Health check endpoint.""" """Health check endpoint."""
logger.info("Health check endpoint accessed") logger.info("Health check endpoint accessed")
return {"status": "healthy"} return HealthResponse(status="healthy")

View File

@@ -3,45 +3,33 @@
from typing import Annotated, Any from typing import Annotated, Any
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field
from app.core.dependencies import get_current_active_user_flexible from app.core.dependencies import get_current_active_user_flexible
from app.core.logging import get_logger from app.core.logging import get_logger
from app.models.user import User from app.models.user import User
from app.services.player import PlayerMode, get_player_service from app.schemas.common import MessageResponse
from app.schemas.player import (
PlayerModeRequest,
PlayerSeekRequest,
PlayerStateResponse,
PlayerVolumeRequest,
)
from app.services.player import get_player_service
logger = get_logger(__name__) logger = get_logger(__name__)
router = APIRouter(prefix="/player", tags=["player"]) router = APIRouter(prefix="/player", tags=["player"])
class SeekRequest(BaseModel):
"""Request model for seek operation."""
position_ms: int = Field(ge=0, description="Position in milliseconds")
class VolumeRequest(BaseModel):
"""Request model for volume control."""
volume: int = Field(ge=0, le=100, description="Volume level (0-100)")
class ModeRequest(BaseModel):
"""Request model for mode change."""
mode: PlayerMode = Field(description="Playback mode")
@router.post("/play") @router.post("/play")
async def play( async def play(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Play current sound.""" """Play current sound."""
try: try:
player = get_player_service() player = get_player_service()
await player.play() await player.play()
return {"message": "Playback started"} return MessageResponse(message="Playback started")
except Exception as e: except Exception as e:
logger.exception("Error starting playback") logger.exception("Error starting playback")
raise HTTPException( raise HTTPException(
@@ -54,12 +42,12 @@ async def play(
async def play_at_index( async def play_at_index(
index: int, index: int,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Play sound at specific index.""" """Play sound at specific index."""
try: try:
player = get_player_service() player = get_player_service()
await player.play(index) await player.play(index)
return {"message": f"Playing sound at index {index}"} return MessageResponse(message=f"Playing sound at index {index}")
except ValueError as e: except ValueError as e:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, status_code=status.HTTP_400_BAD_REQUEST,
@@ -76,12 +64,12 @@ async def play_at_index(
@router.post("/pause") @router.post("/pause")
async def pause( async def pause(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Pause playback.""" """Pause playback."""
try: try:
player = get_player_service() player = get_player_service()
await player.pause() await player.pause()
return {"message": "Playback paused"} return MessageResponse(message="Playback paused")
except Exception as e: except Exception as e:
logger.exception("Error pausing playback") logger.exception("Error pausing playback")
raise HTTPException( raise HTTPException(
@@ -93,12 +81,12 @@ async def pause(
@router.post("/stop") @router.post("/stop")
async def stop( async def stop(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Stop playback.""" """Stop playback."""
try: try:
player = get_player_service() player = get_player_service()
await player.stop_playback() await player.stop_playback()
return {"message": "Playback stopped"} return MessageResponse(message="Playback stopped")
except Exception as e: except Exception as e:
logger.exception("Error stopping playback") logger.exception("Error stopping playback")
raise HTTPException( raise HTTPException(
@@ -110,12 +98,12 @@ async def stop(
@router.post("/next") @router.post("/next")
async def next_track( async def next_track(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Skip to next track.""" """Skip to next track."""
try: try:
player = get_player_service() player = get_player_service()
await player.next() await player.next()
return {"message": "Skipped to next track"} return MessageResponse(message="Skipped to next track")
except Exception as e: except Exception as e:
logger.exception("Error skipping to next track") logger.exception("Error skipping to next track")
raise HTTPException( raise HTTPException(
@@ -127,12 +115,12 @@ async def next_track(
@router.post("/previous") @router.post("/previous")
async def previous_track( async def previous_track(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Go to previous track.""" """Go to previous track."""
try: try:
player = get_player_service() player = get_player_service()
await player.previous() await player.previous()
return {"message": "Went to previous track"} return MessageResponse(message="Went to previous track")
except Exception as e: except Exception as e:
logger.exception("Error going to previous track") logger.exception("Error going to previous track")
raise HTTPException( raise HTTPException(
@@ -143,14 +131,14 @@ async def previous_track(
@router.post("/seek") @router.post("/seek")
async def seek( async def seek(
request: SeekRequest, request: PlayerSeekRequest,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Seek to specific position in current track.""" """Seek to specific position in current track."""
try: try:
player = get_player_service() player = get_player_service()
await player.seek(request.position_ms) await player.seek(request.position_ms)
return {"message": f"Seeked to position {request.position_ms}ms"} return MessageResponse(message=f"Seeked to position {request.position_ms}ms")
except Exception as e: except Exception as e:
logger.exception("Error seeking to position %s", request.position_ms) logger.exception("Error seeking to position %s", request.position_ms)
raise HTTPException( raise HTTPException(
@@ -161,14 +149,14 @@ async def seek(
@router.post("/volume") @router.post("/volume")
async def set_volume( async def set_volume(
request: VolumeRequest, request: PlayerVolumeRequest,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Set playback volume.""" """Set playback volume."""
try: try:
player = get_player_service() player = get_player_service()
await player.set_volume(request.volume) await player.set_volume(request.volume)
return {"message": f"Volume set to {request.volume}"} return MessageResponse(message=f"Volume set to {request.volume}")
except Exception as e: except Exception as e:
logger.exception("Error setting volume to %s", request.volume) logger.exception("Error setting volume to %s", request.volume)
raise HTTPException( raise HTTPException(
@@ -179,14 +167,14 @@ async def set_volume(
@router.post("/mode") @router.post("/mode")
async def set_mode( async def set_mode(
request: ModeRequest, request: PlayerModeRequest,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Set playback mode.""" """Set playback mode."""
try: try:
player = get_player_service() player = get_player_service()
await player.set_mode(request.mode) await player.set_mode(request.mode)
return {"message": f"Mode set to {request.mode.value}"} return MessageResponse(message=f"Mode set to {request.mode.value}")
except Exception as e: except Exception as e:
logger.exception("Error setting mode to %s", request.mode) logger.exception("Error setting mode to %s", request.mode)
raise HTTPException( raise HTTPException(
@@ -198,12 +186,12 @@ async def set_mode(
@router.post("/reload-playlist") @router.post("/reload-playlist")
async def reload_playlist( async def reload_playlist(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, str]: ) -> MessageResponse:
"""Reload current playlist.""" """Reload current playlist."""
try: try:
player = get_player_service() player = get_player_service()
await player.reload_playlist() await player.reload_playlist()
return {"message": "Playlist reloaded"} return MessageResponse(message="Playlist reloaded")
except Exception as e: except Exception as e:
logger.exception("Error reloading playlist") logger.exception("Error reloading playlist")
raise HTTPException( raise HTTPException(
@@ -215,11 +203,12 @@ async def reload_playlist(
@router.get("/state") @router.get("/state")
async def get_state( async def get_state(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
) -> dict[str, Any]: ) -> PlayerStateResponse:
"""Get current player state.""" """Get current player state."""
try: try:
player = get_player_service() player = get_player_service()
return player.get_state() state = player.get_state()
return PlayerStateResponse(**state)
except Exception as e: except Exception as e:
logger.exception("Error getting player state") logger.exception("Error getting player state")
raise HTTPException( raise HTTPException(

View File

@@ -3,113 +3,26 @@
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.database import get_db from app.core.database import get_db
from app.core.dependencies import get_current_active_user_flexible from app.core.dependencies import get_current_active_user_flexible
from app.models.playlist import Playlist
from app.models.sound import Sound
from app.models.user import User from app.models.user import User
from app.schemas.common import MessageResponse
from app.schemas.playlist import (
PlaylistAddSoundRequest,
PlaylistCreateRequest,
PlaylistReorderRequest,
PlaylistResponse,
PlaylistSoundResponse,
PlaylistStatsResponse,
PlaylistUpdateRequest,
)
from app.services.playlist import PlaylistService from app.services.playlist import PlaylistService
router = APIRouter(prefix="/playlists", tags=["playlists"]) router = APIRouter(prefix="/playlists", tags=["playlists"])
class PlaylistCreateRequest(BaseModel):
"""Request model for creating a playlist."""
name: str
description: str | None = None
genre: str | None = None
class PlaylistUpdateRequest(BaseModel):
"""Request model for updating a playlist."""
name: str | None = None
description: str | None = None
genre: str | None = None
is_current: bool | None = None
class PlaylistResponse(BaseModel):
"""Response model for playlist data."""
id: int
name: str
description: str | None
genre: str | None
is_main: bool
is_current: bool
is_deletable: bool
created_at: str
updated_at: str | None
@classmethod
def from_playlist(cls, playlist: Playlist) -> "PlaylistResponse":
"""Create response from playlist model."""
return cls(
id=playlist.id,
name=playlist.name,
description=playlist.description,
genre=playlist.genre,
is_main=playlist.is_main,
is_current=playlist.is_current,
is_deletable=playlist.is_deletable,
created_at=playlist.created_at.isoformat(),
updated_at=playlist.updated_at.isoformat() if playlist.updated_at else None,
)
class SoundResponse(BaseModel):
"""Response model for sound data in playlists."""
id: int
name: str
filename: str
type: str
duration: int | None
size: int | None
play_count: int
created_at: str
@classmethod
def from_sound(cls, sound: Sound) -> "SoundResponse":
"""Create response from sound model."""
return cls(
id=sound.id,
name=sound.name,
filename=sound.filename,
type=sound.type,
duration=sound.duration,
size=sound.size,
play_count=sound.play_count,
created_at=sound.created_at.isoformat(),
)
class AddSoundRequest(BaseModel):
"""Request model for adding a sound to a playlist."""
sound_id: int
position: int | None = None
class ReorderRequest(BaseModel):
"""Request model for reordering sounds in a playlist."""
sound_positions: list[tuple[int, int]]
class PlaylistStatsResponse(BaseModel):
"""Response model for playlist statistics."""
sound_count: int
total_duration_ms: int
total_play_count: int
async def get_playlist_service( async def get_playlist_service(
session: Annotated[AsyncSession, Depends(get_db)], session: Annotated[AsyncSession, Depends(get_db)],
) -> PlaylistService: ) -> PlaylistService:
@@ -208,10 +121,10 @@ async def delete_playlist(
playlist_id: int, playlist_id: int,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], current_user: Annotated[User, Depends(get_current_active_user_flexible)],
playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)], playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)],
) -> dict[str, str]: ) -> MessageResponse:
"""Delete a playlist.""" """Delete a playlist."""
await playlist_service.delete_playlist(playlist_id, current_user.id) await playlist_service.delete_playlist(playlist_id, current_user.id)
return {"message": "Playlist deleted successfully"} return MessageResponse(message="Playlist deleted successfully")
@router.get("/search/{query}") @router.get("/search/{query}")
@@ -241,19 +154,19 @@ async def get_playlist_sounds(
playlist_id: int, playlist_id: int,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], current_user: Annotated[User, Depends(get_current_active_user_flexible)],
playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)], playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)],
) -> list[SoundResponse]: ) -> list[PlaylistSoundResponse]:
"""Get all sounds in a playlist.""" """Get all sounds in a playlist."""
sounds = await playlist_service.get_playlist_sounds(playlist_id) sounds = await playlist_service.get_playlist_sounds(playlist_id)
return [SoundResponse.from_sound(sound) for sound in sounds] return [PlaylistSoundResponse.from_sound(sound) for sound in sounds]
@router.post("/{playlist_id}/sounds") @router.post("/{playlist_id}/sounds")
async def add_sound_to_playlist( async def add_sound_to_playlist(
playlist_id: int, playlist_id: int,
request: AddSoundRequest, request: PlaylistAddSoundRequest,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], current_user: Annotated[User, Depends(get_current_active_user_flexible)],
playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)], playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)],
) -> dict[str, str]: ) -> MessageResponse:
"""Add a sound to a playlist.""" """Add a sound to a playlist."""
await playlist_service.add_sound_to_playlist( await playlist_service.add_sound_to_playlist(
playlist_id=playlist_id, playlist_id=playlist_id,
@@ -261,7 +174,7 @@ async def add_sound_to_playlist(
user_id=current_user.id, user_id=current_user.id,
position=request.position, position=request.position,
) )
return {"message": "Sound added to playlist successfully"} return MessageResponse(message="Sound added to playlist successfully")
@router.delete("/{playlist_id}/sounds/{sound_id}") @router.delete("/{playlist_id}/sounds/{sound_id}")
@@ -270,30 +183,30 @@ async def remove_sound_from_playlist(
sound_id: int, sound_id: int,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], current_user: Annotated[User, Depends(get_current_active_user_flexible)],
playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)], playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)],
) -> dict[str, str]: ) -> MessageResponse:
"""Remove a sound from a playlist.""" """Remove a sound from a playlist."""
await playlist_service.remove_sound_from_playlist( await playlist_service.remove_sound_from_playlist(
playlist_id=playlist_id, playlist_id=playlist_id,
sound_id=sound_id, sound_id=sound_id,
user_id=current_user.id, user_id=current_user.id,
) )
return {"message": "Sound removed from playlist successfully"} return MessageResponse(message="Sound removed from playlist successfully")
@router.put("/{playlist_id}/sounds/reorder") @router.put("/{playlist_id}/sounds/reorder")
async def reorder_playlist_sounds( async def reorder_playlist_sounds(
playlist_id: int, playlist_id: int,
request: ReorderRequest, request: PlaylistReorderRequest,
current_user: Annotated[User, Depends(get_current_active_user_flexible)], current_user: Annotated[User, Depends(get_current_active_user_flexible)],
playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)], playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)],
) -> dict[str, str]: ) -> MessageResponse:
"""Reorder sounds in a playlist.""" """Reorder sounds in a playlist."""
await playlist_service.reorder_playlist_sounds( await playlist_service.reorder_playlist_sounds(
playlist_id=playlist_id, playlist_id=playlist_id,
user_id=current_user.id, user_id=current_user.id,
sound_positions=request.sound_positions, sound_positions=request.sound_positions,
) )
return {"message": "Playlist sounds reordered successfully"} return MessageResponse(message="Playlist sounds reordered successfully")
@router.put("/{playlist_id}/set-current") @router.put("/{playlist_id}/set-current")
@@ -311,10 +224,10 @@ async def set_current_playlist(
async def unset_current_playlist( async def unset_current_playlist(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], current_user: Annotated[User, Depends(get_current_active_user_flexible)],
playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)], playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)],
) -> dict[str, str]: ) -> MessageResponse:
"""Unset the current playlist.""" """Unset the current playlist."""
await playlist_service.unset_current_playlist(current_user.id) await playlist_service.unset_current_playlist(current_user.id)
return {"message": "Current playlist unset successfully"} return MessageResponse(message="Current playlist unset successfully")
@router.get("/{playlist_id}/stats") @router.get("/{playlist_id}/stats")

View File

@@ -1 +1,57 @@
"""Schemas package.""" """Schemas package."""
from .auth import (
ApiTokenRequest,
ApiTokenResponse,
ApiTokenStatusResponse,
AuthResponse,
TokenResponse,
UserLoginRequest,
UserRegisterRequest,
UserResponse,
)
from .common import HealthResponse, MessageResponse, StatusResponse
from .player import (
PlayerModeRequest,
PlayerSeekRequest,
PlayerStateResponse,
PlayerVolumeRequest,
)
from .playlist import (
PlaylistAddSoundRequest,
PlaylistCreateRequest,
PlaylistReorderRequest,
PlaylistResponse,
PlaylistSoundResponse,
PlaylistStatsResponse,
PlaylistUpdateRequest,
)
__all__ = [
# Auth schemas
"ApiTokenRequest",
"ApiTokenResponse",
"ApiTokenStatusResponse",
"AuthResponse",
"TokenResponse",
"UserLoginRequest",
"UserRegisterRequest",
"UserResponse",
# Common schemas
"HealthResponse",
"MessageResponse",
"StatusResponse",
# Player schemas
"PlayerModeRequest",
"PlayerSeekRequest",
"PlayerStateResponse",
"PlayerVolumeRequest",
# Playlist schemas
"PlaylistAddSoundRequest",
"PlaylistCreateRequest",
"PlaylistReorderRequest",
"PlaylistResponse",
"PlaylistSoundResponse",
"PlaylistStatsResponse",
"PlaylistUpdateRequest",
]

21
app/schemas/common.py Normal file
View File

@@ -0,0 +1,21 @@
"""Common response schemas."""
from pydantic import BaseModel, Field
class MessageResponse(BaseModel):
"""Generic message response."""
message: str = Field(description="Response message")
class StatusResponse(BaseModel):
"""Generic status response."""
status: str = Field(description="Status message")
class HealthResponse(BaseModel):
"""Health check response."""
status: str = Field(description="Health status")

46
app/schemas/player.py Normal file
View File

@@ -0,0 +1,46 @@
"""Player schemas."""
from typing import Any
from pydantic import BaseModel, Field
from app.services.player import PlayerMode
class PlayerSeekRequest(BaseModel):
"""Request model for seek operation."""
position_ms: int = Field(ge=0, description="Position in milliseconds")
class PlayerVolumeRequest(BaseModel):
"""Request model for volume control."""
volume: int = Field(ge=0, le=100, description="Volume level (0-100)")
class PlayerModeRequest(BaseModel):
"""Request model for mode change."""
mode: PlayerMode = Field(description="Playback mode")
class PlayerStateResponse(BaseModel):
"""Response model for player state."""
status: str = Field(description="Player status (playing, paused, stopped)")
current_sound: dict[str, Any] | None = Field(
None, description="Current sound information"
)
playlist: dict[str, Any] | None = Field(
None, description="Current playlist information"
)
position_ms: int = Field(description="Current position in milliseconds")
duration_ms: int | None = Field(
None, description="Total duration in milliseconds",
)
volume: int = Field(description="Current volume (0-100)")
mode: str = Field(description="Current playback mode")
index: int | None = Field(
None, description="Current track index in playlist",
)

104
app/schemas/playlist.py Normal file
View File

@@ -0,0 +1,104 @@
"""Playlist schemas."""
from pydantic import BaseModel, Field
from app.models.playlist import Playlist
from app.models.sound import Sound
class PlaylistCreateRequest(BaseModel):
"""Request model for creating a playlist."""
name: str
description: str | None = None
genre: str | None = None
class PlaylistUpdateRequest(BaseModel):
"""Request model for updating a playlist."""
name: str | None = None
description: str | None = None
genre: str | None = None
is_current: bool | None = None
class PlaylistResponse(BaseModel):
"""Response model for playlist data."""
id: int
name: str
description: str | None
genre: str | None
is_main: bool
is_current: bool
is_deletable: bool
created_at: str
updated_at: str | None
@classmethod
def from_playlist(cls, playlist: Playlist) -> "PlaylistResponse":
"""Create response from playlist model."""
if playlist.id is None:
raise ValueError("Playlist ID cannot be None")
return cls(
id=playlist.id,
name=playlist.name,
description=playlist.description,
genre=playlist.genre,
is_main=playlist.is_main,
is_current=playlist.is_current,
is_deletable=playlist.is_deletable,
created_at=playlist.created_at.isoformat(),
updated_at=playlist.updated_at.isoformat() if playlist.updated_at else None,
)
class PlaylistSoundResponse(BaseModel):
"""Response model for sound data in playlists."""
id: int
name: str
filename: str
type: str
duration: int | None
size: int | None
play_count: int
created_at: str
@classmethod
def from_sound(cls, sound: Sound) -> "PlaylistSoundResponse":
"""Create response from sound model."""
if sound.id is None:
raise ValueError("Sound ID cannot be None")
return cls(
id=sound.id,
name=sound.name,
filename=sound.filename,
type=sound.type,
duration=sound.duration,
size=sound.size,
play_count=sound.play_count,
created_at=sound.created_at.isoformat(),
)
class PlaylistAddSoundRequest(BaseModel):
"""Request model for adding a sound to a playlist."""
sound_id: int
position: int | None = None
class PlaylistReorderRequest(BaseModel):
"""Request model for reordering sounds in a playlist."""
sound_positions: list[tuple[int, int]]
class PlaylistStatsResponse(BaseModel):
"""Response model for playlist statistics."""
sound_count: int
total_duration_ms: int
total_play_count: int