Files
sdb2-backend/app/services/favorite.py

380 lines
12 KiB
Python

"""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)