feat: Implement sound playback with credit validation in VLCPlayerService and update WebSocket handling
Some checks failed
Backend CI / lint (push) Failing after 5m0s
Backend CI / test (push) Failing after 2m0s

This commit is contained in:
JSC
2025-08-19 22:16:48 +02:00
parent 560ccd3f7e
commit a82acfae50
3 changed files with 104 additions and 75 deletions

View File

@@ -7,11 +7,9 @@ from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.database import get_db, get_session_factory from app.core.database import get_db, get_session_factory
from app.core.dependencies import get_current_active_user_flexible from app.core.dependencies import get_current_active_user_flexible
from app.models.credit_action import CreditActionType
from app.models.user import User from app.models.user import User
from app.repositories.sound import SortOrder, SoundRepository, SoundSortField from app.repositories.sound import SortOrder, SoundRepository, SoundSortField
from app.schemas.sound import SoundResponse, SoundsListResponse from app.schemas.sound import SoundResponse, SoundsListResponse
from app.services.credit import CreditService, InsufficientCreditsError
from app.services.favorite import FavoriteService from app.services.favorite import FavoriteService
from app.services.vlc_player import VLCPlayerService, get_vlc_player_service from app.services.vlc_player import VLCPlayerService, get_vlc_player_service
@@ -23,78 +21,11 @@ def get_vlc_player() -> VLCPlayerService:
return get_vlc_player_service(get_session_factory()) return get_vlc_player_service(get_session_factory())
def get_credit_service() -> CreditService:
"""Get the credit service."""
return CreditService(get_session_factory())
def get_favorite_service() -> FavoriteService: def get_favorite_service() -> FavoriteService:
"""Get the favorite service.""" """Get the favorite service."""
return FavoriteService(get_session_factory()) return FavoriteService(get_session_factory())
async def play_sound_internal(
sound_id: int, user_id: str,
) -> dict[str, str | int | bool]:
"""Play sound with VLC internally (used by HTTP and WebSocket endpoints)."""
session_factory = get_session_factory()
# Create services
vlc_player = get_vlc_player()
credit_service = get_credit_service()
async with session_factory() as session:
sound_repo = SoundRepository(session)
# Get the sound
sound = await sound_repo.get_by_id(sound_id)
if not sound:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Sound with ID {sound_id} not found",
)
# Check and validate credits before playing
try:
await credit_service.validate_and_reserve_credits(
int(user_id),
CreditActionType.VLC_PLAY_SOUND,
)
except InsufficientCreditsError as e:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail=(
f"Insufficient credits: {e.required} required, "
f"{e.available} available"
),
) from e
# Play the sound using VLC
success = await vlc_player.play_sound(sound)
# Deduct credits based on success
await credit_service.deduct_credits(
int(user_id),
CreditActionType.VLC_PLAY_SOUND,
success=success,
metadata={"sound_id": sound_id, "sound_name": sound.name},
)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to launch VLC for sound playback",
)
return {
"message": f"Sound '{sound.name}' is now playing via VLC",
"sound_id": sound_id,
"sound_name": sound.name,
"success": True,
"credits_deducted": 1,
}
async def get_sound_repository( async def get_sound_repository(
session: Annotated[AsyncSession, Depends(get_db)], session: Annotated[AsyncSession, Depends(get_db)],
) -> SoundRepository: ) -> SoundRepository:
@@ -178,10 +109,16 @@ async def get_sounds( # noqa: PLR0913
async def play_sound_with_vlc( async def play_sound_with_vlc(
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)],
vlc_player: Annotated[VLCPlayerService, Depends(get_vlc_player)],
) -> dict[str, str | int | bool]: ) -> dict[str, str | int | bool]:
"""Play a sound using VLC subprocess (requires 1 credit).""" """Play a sound using VLC subprocess (requires 1 credit)."""
try: try:
return await play_sound_internal(sound_id, str(current_user.id)) if not current_user.id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User ID is required",
)
return await vlc_player.play_sound_with_credits(sound_id, current_user.id)
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:

View File

@@ -111,20 +111,28 @@ class SocketManager:
sound_id = data.get("sound_id") sound_id = data.get("sound_id")
if not sound_id: if not sound_id:
logger.warning( logger.warning(
"Play sound request missing sound_id from user %s", user_id, "Play sound request missing sound_id from user %s",
user_id,
) )
return return
try: try:
# Import here to avoid circular imports # Import here to avoid circular imports
from app.api.v1.sounds import play_sound_internal from app.services.vlc_player import get_vlc_player_service
from app.core.database import get_session_factory
# Call the internal play sound function # Get VLC player service with database factory
await play_sound_internal(int(sound_id), user_id) vlc_player = get_vlc_player_service(get_session_factory())
# Call the service method
await vlc_player.play_sound_with_credits(int(sound_id), int(user_id))
logger.info("User %s played sound %s via WebSocket", user_id, sound_id) logger.info("User %s played sound %s via WebSocket", user_id, sound_id)
except Exception as e: except Exception as e:
logger.exception( logger.exception(
"Error playing sound %s for user %s: %s", sound_id, user_id, e, "Error playing sound %s for user %s: %s",
sound_id,
user_id,
e,
) )
# Emit error back to user # Emit error back to user
await self.sio.emit( await self.sio.emit(

View File

@@ -9,6 +9,7 @@ from typing import Any
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.logging import get_logger from app.core.logging import get_logger
from app.models.credit_action import CreditActionType
from app.models.sound import Sound from app.models.sound import Sound
from app.models.sound_played import SoundPlayed from app.models.sound_played import SoundPlayed
from app.repositories.sound import SoundRepository from app.repositories.sound import SoundRepository
@@ -307,6 +308,89 @@ class VLCPlayerService:
finally: finally:
await session.close() await session.close()
async def play_sound_with_credits(
self, sound_id: int, user_id: int
) -> dict[str, str | int | bool]:
"""Play sound with VLC with credit validation and deduction.
This method combines credit checking, sound playing, and credit deduction
in a single operation. Used by both HTTP and WebSocket endpoints.
Args:
sound_id: ID of the sound to play
user_id: ID of the user playing the sound
Returns:
dict: Result information including success status and message
Raises:
HTTPException: For various error conditions (sound not found,
insufficient credits, VLC failure)
"""
from fastapi import HTTPException, status
from app.services.credit import CreditService, InsufficientCreditsError
if not self.db_session_factory:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database session factory not configured",
)
async with self.db_session_factory() as session:
sound_repo = SoundRepository(session)
# Get the sound
sound = await sound_repo.get_by_id(sound_id)
if not sound:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Sound with ID {sound_id} not found",
)
# Get credit service
credit_service = CreditService(self.db_session_factory)
# Check and validate credits before playing
try:
await credit_service.validate_and_reserve_credits(
user_id,
CreditActionType.VLC_PLAY_SOUND,
)
except InsufficientCreditsError as e:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail=(
f"Insufficient credits: {e.required} required, "
f"{e.available} available"
),
) from e
# Play the sound using VLC
success = await self.play_sound(sound)
# Deduct credits based on success
await credit_service.deduct_credits(
user_id,
CreditActionType.VLC_PLAY_SOUND,
success=success,
metadata={"sound_id": sound_id, "sound_name": sound.name},
)
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to launch VLC for sound playback",
)
return {
"message": f"Sound '{sound.name}' is now playing via VLC",
"sound_id": sound_id,
"sound_name": sound.name,
"success": True,
"credits_deducted": 1,
}
# Global VLC player service instance # Global VLC player service instance
vlc_player_service: VLCPlayerService | None = None vlc_player_service: VLCPlayerService | None = None