"""Service for managing user favorites.""" from collections.abc import Callable from sqlmodel.ext.asyncio.session import AsyncSession from app.core.logging import get_logger from app.models.favorite import Favorite from app.repositories.favorite import FavoriteRepository from app.repositories.playlist import PlaylistRepository from app.repositories.sound import SoundRepository from app.repositories.user import UserRepository from app.services.socket import socket_manager logger = get_logger(__name__) class FavoriteService: """Service for managing user favorites.""" def __init__(self, db_session_factory: Callable[[], AsyncSession]) -> None: """Initialize the favorite service. Args: db_session_factory: Factory function to create database sessions """ self.db_session_factory = db_session_factory async def add_sound_favorite(self, user_id: int, sound_id: int) -> Favorite: """Add a sound to user's favorites. Args: user_id: The user ID sound_id: The sound ID Returns: The created favorite Raises: ValueError: If user or sound not found, or already favorited """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) user_repo = UserRepository(session) sound_repo = SoundRepository(session) # Verify user exists user = await user_repo.get_by_id(user_id) if not user: raise ValueError(f"User with ID {user_id} not found") # Verify sound exists sound = await sound_repo.get_by_id(sound_id) if not sound: raise ValueError(f"Sound with ID {sound_id} not found") # Get data for the event immediately after loading sound_name = sound.name user_name = user.name # Check if already favorited existing = await favorite_repo.get_by_user_and_sound(user_id, sound_id) if existing: raise ValueError( f"Sound {sound_id} is already favorited by user {user_id}", ) # Create favorite favorite_data = { "user_id": user_id, "sound_id": sound_id, "playlist_id": None, } favorite = await favorite_repo.create(favorite_data) logger.info("User %s favorited sound %s", user_id, sound_id) # Get updated favorite count within the same session favorite_count = await favorite_repo.count_sound_favorites(sound_id) # Emit sound_favorited event via WebSocket (outside the session) try: event_data = { "sound_id": sound_id, "sound_name": sound_name, "user_id": user_id, "user_name": user_name, "favorite_count": favorite_count, } await socket_manager.broadcast_to_all("sound_favorited", event_data) logger.info("Broadcasted sound_favorited event for sound %s", sound_id) except Exception: logger.exception( "Failed to broadcast sound_favorited event for sound %s", sound_id, ) return favorite async def add_playlist_favorite(self, user_id: int, playlist_id: int) -> Favorite: """Add a playlist to user's favorites. Args: user_id: The user ID playlist_id: The playlist ID Returns: The created favorite Raises: ValueError: If user or playlist not found, or already favorited """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) user_repo = UserRepository(session) playlist_repo = PlaylistRepository(session) # Verify user exists user = await user_repo.get_by_id(user_id) if not user: raise ValueError(f"User with ID {user_id} not found") # Verify playlist exists playlist = await playlist_repo.get_by_id(playlist_id) if not playlist: raise ValueError(f"Playlist with ID {playlist_id} not found") # Check if already favorited existing = await favorite_repo.get_by_user_and_playlist( user_id, playlist_id, ) if existing: raise ValueError( f"Playlist {playlist_id} is already favorited by user {user_id}", ) # Create favorite favorite_data = { "user_id": user_id, "sound_id": None, "playlist_id": playlist_id, } favorite = await favorite_repo.create(favorite_data) logger.info("User %s favorited playlist %s", user_id, playlist_id) return favorite async def remove_sound_favorite(self, user_id: int, sound_id: int) -> None: """Remove a sound from user's favorites. Args: user_id: The user ID sound_id: The sound ID Raises: ValueError: If favorite not found """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) favorite = await favorite_repo.get_by_user_and_sound(user_id, sound_id) if not favorite: raise ValueError(f"Sound {sound_id} is not favorited by user {user_id}") # Get user and sound info before deletion for the event user_repo = UserRepository(session) sound_repo = SoundRepository(session) user = await user_repo.get_by_id(user_id) sound = await sound_repo.get_by_id(sound_id) # Get data for the event immediately after loading sound_name = sound.name if sound else "Unknown" user_name = user.name if user else "Unknown" await favorite_repo.delete(favorite) logger.info("User %s removed sound %s from favorites", user_id, sound_id) # Get updated favorite count after deletion within the same session favorite_count = await favorite_repo.count_sound_favorites(sound_id) # Emit sound_favorited event via WebSocket (outside the session) try: event_data = { "sound_id": sound_id, "sound_name": sound_name, "user_id": user_id, "user_name": user_name, "favorite_count": favorite_count, } await socket_manager.broadcast_to_all("sound_favorited", event_data) logger.info( "Broadcasted sound_favorited event for sound %s removal", sound_id ) except Exception: logger.exception( "Failed to broadcast sound_favorited event for sound %s removal", sound_id, ) async def remove_playlist_favorite(self, user_id: int, playlist_id: int) -> None: """Remove a playlist from user's favorites. Args: user_id: The user ID playlist_id: The playlist ID Raises: ValueError: If favorite not found """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) favorite = await favorite_repo.get_by_user_and_playlist( user_id, playlist_id, ) if not favorite: raise ValueError( f"Playlist {playlist_id} is not favorited by user {user_id}", ) await favorite_repo.delete(favorite) logger.info( "User %s removed playlist %s from favorites", user_id, playlist_id, ) async def get_user_favorites( self, user_id: int, limit: int = 100, offset: int = 0, ) -> list[Favorite]: """Get all favorites for a user. Args: user_id: The user ID limit: Maximum number of favorites to return offset: Number of favorites to skip Returns: List of user favorites """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) return await favorite_repo.get_user_favorites(user_id, limit, offset) async def get_user_sound_favorites( self, user_id: int, limit: int = 100, offset: int = 0, ) -> list[Favorite]: """Get sound favorites for a user. Args: user_id: The user ID limit: Maximum number of favorites to return offset: Number of favorites to skip Returns: List of user sound favorites """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) return await favorite_repo.get_user_sound_favorites(user_id, limit, offset) async def get_user_playlist_favorites( self, user_id: int, limit: int = 100, offset: int = 0, ) -> list[Favorite]: """Get playlist favorites for a user. Args: user_id: The user ID limit: Maximum number of favorites to return offset: Number of favorites to skip Returns: List of user playlist favorites """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) return await favorite_repo.get_user_playlist_favorites( user_id, limit, offset, ) async def is_sound_favorited(self, user_id: int, sound_id: int) -> bool: """Check if a sound is favorited by a user. Args: user_id: The user ID sound_id: The sound ID Returns: True if the sound is favorited, False otherwise """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) return await favorite_repo.is_sound_favorited(user_id, sound_id) async def is_playlist_favorited(self, user_id: int, playlist_id: int) -> bool: """Check if a playlist is favorited by a user. Args: user_id: The user ID playlist_id: The playlist ID Returns: True if the playlist is favorited, False otherwise """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) return await favorite_repo.is_playlist_favorited(user_id, playlist_id) async def get_favorite_counts(self, user_id: int) -> dict[str, int]: """Get favorite counts for a user. Args: user_id: The user ID Returns: Dictionary with favorite counts """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) total = await favorite_repo.count_user_favorites(user_id) sounds = len(await favorite_repo.get_user_sound_favorites(user_id)) playlists = len(await favorite_repo.get_user_playlist_favorites(user_id)) return { "total": total, "sounds": sounds, "playlists": playlists, } async def get_sound_favorite_count(self, sound_id: int) -> int: """Get the number of users who have favorited a sound. Args: sound_id: The sound ID Returns: Number of users who favorited this sound """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) return await favorite_repo.count_sound_favorites(sound_id) async def get_playlist_favorite_count(self, playlist_id: int) -> int: """Get the number of users who have favorited a playlist. Args: playlist_id: The playlist ID Returns: Number of users who favorited this playlist """ async with self.db_session_factory() as session: favorite_repo = FavoriteRepository(session) return await favorite_repo.count_playlist_favorites(playlist_id)