feat: Implement sound playback with credit validation in VLCPlayerService and update WebSocket handling
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user