feat: Implement favorites management API; add endpoints for adding, removing, and retrieving favorites for sounds and playlists
feat: Create Favorite model and repository for managing user favorites in the database feat: Add FavoriteService to handle business logic for favorites management feat: Enhance Playlist and Sound response schemas to include favorite indicators and counts refactor: Update API routes to include favorites functionality in playlists and sounds
This commit is contained in:
@@ -7,6 +7,7 @@ from app.api.v1 import (
|
|||||||
auth,
|
auth,
|
||||||
dashboard,
|
dashboard,
|
||||||
extractions,
|
extractions,
|
||||||
|
favorites,
|
||||||
files,
|
files,
|
||||||
main,
|
main,
|
||||||
player,
|
player,
|
||||||
@@ -22,6 +23,7 @@ api_router = APIRouter(prefix="/v1")
|
|||||||
api_router.include_router(auth.router, tags=["authentication"])
|
api_router.include_router(auth.router, tags=["authentication"])
|
||||||
api_router.include_router(dashboard.router, tags=["dashboard"])
|
api_router.include_router(dashboard.router, tags=["dashboard"])
|
||||||
api_router.include_router(extractions.router, tags=["extractions"])
|
api_router.include_router(extractions.router, tags=["extractions"])
|
||||||
|
api_router.include_router(favorites.router, tags=["favorites"])
|
||||||
api_router.include_router(files.router, tags=["files"])
|
api_router.include_router(files.router, tags=["files"])
|
||||||
api_router.include_router(main.router, tags=["main"])
|
api_router.include_router(main.router, tags=["main"])
|
||||||
api_router.include_router(player.router, tags=["player"])
|
api_router.include_router(player.router, tags=["player"])
|
||||||
|
|||||||
194
app/api/v1/favorites.py
Normal file
194
app/api/v1/favorites.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
"""Favorites management API endpoints."""
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.dependencies import get_current_active_user
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.common import MessageResponse
|
||||||
|
from app.schemas.favorite import (
|
||||||
|
FavoriteCountsResponse,
|
||||||
|
FavoriteResponse,
|
||||||
|
FavoritesListResponse,
|
||||||
|
)
|
||||||
|
from app.services.favorite import FavoriteService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/favorites", tags=["favorites"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_favorite_service() -> FavoriteService:
|
||||||
|
"""Get the favorite service."""
|
||||||
|
from app.core.database import get_session_factory
|
||||||
|
|
||||||
|
return FavoriteService(get_session_factory())
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=FavoritesListResponse)
|
||||||
|
async def get_user_favorites(
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
limit: Annotated[int, Query(ge=1, le=100)] = 50,
|
||||||
|
offset: Annotated[int, Query(ge=0)] = 0,
|
||||||
|
) -> FavoritesListResponse:
|
||||||
|
"""Get all favorites for the current user."""
|
||||||
|
favorites = await favorite_service.get_user_favorites(
|
||||||
|
current_user.id, limit, offset
|
||||||
|
)
|
||||||
|
return FavoritesListResponse(favorites=favorites)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sounds", response_model=FavoritesListResponse)
|
||||||
|
async def get_user_sound_favorites(
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
limit: Annotated[int, Query(ge=1, le=100)] = 50,
|
||||||
|
offset: Annotated[int, Query(ge=0)] = 0,
|
||||||
|
) -> FavoritesListResponse:
|
||||||
|
"""Get sound favorites for the current user."""
|
||||||
|
favorites = await favorite_service.get_user_sound_favorites(
|
||||||
|
current_user.id, limit, offset
|
||||||
|
)
|
||||||
|
return FavoritesListResponse(favorites=favorites)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/playlists", response_model=FavoritesListResponse)
|
||||||
|
async def get_user_playlist_favorites(
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
limit: Annotated[int, Query(ge=1, le=100)] = 50,
|
||||||
|
offset: Annotated[int, Query(ge=0)] = 0,
|
||||||
|
) -> FavoritesListResponse:
|
||||||
|
"""Get playlist favorites for the current user."""
|
||||||
|
favorites = await favorite_service.get_user_playlist_favorites(
|
||||||
|
current_user.id, limit, offset
|
||||||
|
)
|
||||||
|
return FavoritesListResponse(favorites=favorites)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/counts", response_model=FavoriteCountsResponse)
|
||||||
|
async def get_favorite_counts(
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
) -> FavoriteCountsResponse:
|
||||||
|
"""Get favorite counts for the current user."""
|
||||||
|
counts = await favorite_service.get_favorite_counts(current_user.id)
|
||||||
|
return FavoriteCountsResponse(**counts)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/sounds/{sound_id}", response_model=FavoriteResponse)
|
||||||
|
async def add_sound_favorite(
|
||||||
|
sound_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
) -> FavoriteResponse:
|
||||||
|
"""Add a sound to favorites."""
|
||||||
|
try:
|
||||||
|
favorite = await favorite_service.add_sound_favorite(current_user.id, sound_id)
|
||||||
|
return FavoriteResponse.model_validate(favorite)
|
||||||
|
except ValueError as e:
|
||||||
|
if "not found" in str(e):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
|
elif "already favorited" in str(e):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/playlists/{playlist_id}", response_model=FavoriteResponse)
|
||||||
|
async def add_playlist_favorite(
|
||||||
|
playlist_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
) -> FavoriteResponse:
|
||||||
|
"""Add a playlist to favorites."""
|
||||||
|
try:
|
||||||
|
favorite = await favorite_service.add_playlist_favorite(
|
||||||
|
current_user.id, playlist_id
|
||||||
|
)
|
||||||
|
return FavoriteResponse.model_validate(favorite)
|
||||||
|
except ValueError as e:
|
||||||
|
if "not found" in str(e):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
|
elif "already favorited" in str(e):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/sounds/{sound_id}", response_model=MessageResponse)
|
||||||
|
async def remove_sound_favorite(
|
||||||
|
sound_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
) -> MessageResponse:
|
||||||
|
"""Remove a sound from favorites."""
|
||||||
|
try:
|
||||||
|
await favorite_service.remove_sound_favorite(current_user.id, sound_id)
|
||||||
|
return MessageResponse(message="Sound removed from favorites")
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/playlists/{playlist_id}", response_model=MessageResponse)
|
||||||
|
async def remove_playlist_favorite(
|
||||||
|
playlist_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
) -> MessageResponse:
|
||||||
|
"""Remove a playlist from favorites."""
|
||||||
|
try:
|
||||||
|
await favorite_service.remove_playlist_favorite(current_user.id, playlist_id)
|
||||||
|
return MessageResponse(message="Playlist removed from favorites")
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=str(e),
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sounds/{sound_id}/check")
|
||||||
|
async def check_sound_favorited(
|
||||||
|
sound_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
) -> dict[str, bool]:
|
||||||
|
"""Check if a sound is favorited by the current user."""
|
||||||
|
is_favorited = await favorite_service.is_sound_favorited(current_user.id, sound_id)
|
||||||
|
return {"is_favorited": is_favorited}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/playlists/{playlist_id}/check")
|
||||||
|
async def check_playlist_favorited(
|
||||||
|
playlist_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
|
) -> dict[str, bool]:
|
||||||
|
"""Check if a playlist is favorited by the current user."""
|
||||||
|
is_favorited = await favorite_service.is_playlist_favorited(
|
||||||
|
current_user.id, playlist_id
|
||||||
|
)
|
||||||
|
return {"is_favorited": is_favorited}
|
||||||
@@ -19,6 +19,7 @@ from app.schemas.playlist import (
|
|||||||
PlaylistStatsResponse,
|
PlaylistStatsResponse,
|
||||||
PlaylistUpdateRequest,
|
PlaylistUpdateRequest,
|
||||||
)
|
)
|
||||||
|
from app.services.favorite import FavoriteService
|
||||||
from app.services.playlist import PlaylistService
|
from app.services.playlist import PlaylistService
|
||||||
|
|
||||||
router = APIRouter(prefix="/playlists", tags=["playlists"])
|
router = APIRouter(prefix="/playlists", tags=["playlists"])
|
||||||
@@ -31,6 +32,12 @@ async def get_playlist_service(
|
|||||||
return PlaylistService(session)
|
return PlaylistService(session)
|
||||||
|
|
||||||
|
|
||||||
|
def get_favorite_service() -> FavoriteService:
|
||||||
|
"""Get the favorite service."""
|
||||||
|
from app.core.database import get_session_factory
|
||||||
|
return FavoriteService(get_session_factory())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def get_all_playlists( # noqa: PLR0913
|
async def get_all_playlists( # noqa: PLR0913
|
||||||
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
||||||
@@ -72,30 +79,46 @@ async def get_all_playlists( # noqa: PLR0913
|
|||||||
async def get_user_playlists(
|
async def get_user_playlists(
|
||||||
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)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
) -> list[PlaylistResponse]:
|
) -> list[PlaylistResponse]:
|
||||||
"""Get playlists for the current user only."""
|
"""Get playlists for the current user only."""
|
||||||
playlists = await playlist_service.get_user_playlists(current_user.id)
|
playlists = await playlist_service.get_user_playlists(current_user.id)
|
||||||
return [PlaylistResponse.from_playlist(playlist) for playlist in playlists]
|
|
||||||
|
# Add favorite indicators for each playlist
|
||||||
|
playlist_responses = []
|
||||||
|
for playlist in playlists:
|
||||||
|
is_favorited = await favorite_service.is_playlist_favorited(current_user.id, playlist.id)
|
||||||
|
favorite_count = await favorite_service.get_playlist_favorite_count(playlist.id)
|
||||||
|
playlist_response = PlaylistResponse.from_playlist(playlist, is_favorited, favorite_count)
|
||||||
|
playlist_responses.append(playlist_response)
|
||||||
|
|
||||||
|
return playlist_responses
|
||||||
|
|
||||||
|
|
||||||
@router.get("/main")
|
@router.get("/main")
|
||||||
async def get_main_playlist(
|
async def get_main_playlist(
|
||||||
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
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)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
) -> PlaylistResponse:
|
) -> PlaylistResponse:
|
||||||
"""Get the global main playlist."""
|
"""Get the global main playlist."""
|
||||||
playlist = await playlist_service.get_main_playlist()
|
playlist = await playlist_service.get_main_playlist()
|
||||||
return PlaylistResponse.from_playlist(playlist)
|
is_favorited = await favorite_service.is_playlist_favorited(current_user.id, playlist.id)
|
||||||
|
favorite_count = await favorite_service.get_playlist_favorite_count(playlist.id)
|
||||||
|
return PlaylistResponse.from_playlist(playlist, is_favorited, favorite_count)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/current")
|
@router.get("/current")
|
||||||
async def get_current_playlist(
|
async def get_current_playlist(
|
||||||
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
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)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
) -> PlaylistResponse:
|
) -> PlaylistResponse:
|
||||||
"""Get the global current playlist (falls back to main playlist)."""
|
"""Get the global current playlist (falls back to main playlist)."""
|
||||||
playlist = await playlist_service.get_current_playlist()
|
playlist = await playlist_service.get_current_playlist()
|
||||||
return PlaylistResponse.from_playlist(playlist)
|
is_favorited = await favorite_service.is_playlist_favorited(current_user.id, playlist.id)
|
||||||
|
favorite_count = await favorite_service.get_playlist_favorite_count(playlist.id)
|
||||||
|
return PlaylistResponse.from_playlist(playlist, is_favorited, favorite_count)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
@@ -117,12 +140,15 @@ async def create_playlist(
|
|||||||
@router.get("/{playlist_id}")
|
@router.get("/{playlist_id}")
|
||||||
async def get_playlist(
|
async def get_playlist(
|
||||||
playlist_id: int,
|
playlist_id: int,
|
||||||
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
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)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
) -> PlaylistResponse:
|
) -> PlaylistResponse:
|
||||||
"""Get a specific playlist."""
|
"""Get a specific playlist."""
|
||||||
playlist = await playlist_service.get_playlist_by_id(playlist_id)
|
playlist = await playlist_service.get_playlist_by_id(playlist_id)
|
||||||
return PlaylistResponse.from_playlist(playlist)
|
is_favorited = await favorite_service.is_playlist_favorited(current_user.id, playlist.id)
|
||||||
|
favorite_count = await favorite_service.get_playlist_favorite_count(playlist.id)
|
||||||
|
return PlaylistResponse.from_playlist(playlist, is_favorited, favorite_count)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{playlist_id}")
|
@router.put("/{playlist_id}")
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ from app.models.credit_action import CreditActionType
|
|||||||
from app.models.sound import Sound
|
from app.models.sound import Sound
|
||||||
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.services.credit import CreditService, InsufficientCreditsError
|
from app.services.credit import CreditService, InsufficientCreditsError
|
||||||
|
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
|
||||||
|
|
||||||
router = APIRouter(prefix="/sounds", tags=["sounds"])
|
router = APIRouter(prefix="/sounds", tags=["sounds"])
|
||||||
@@ -27,6 +29,11 @@ def get_credit_service() -> CreditService:
|
|||||||
return CreditService(get_session_factory())
|
return CreditService(get_session_factory())
|
||||||
|
|
||||||
|
|
||||||
|
def get_favorite_service() -> FavoriteService:
|
||||||
|
"""Get the favorite service."""
|
||||||
|
return FavoriteService(get_session_factory())
|
||||||
|
|
||||||
|
|
||||||
async def get_sound_repository(
|
async def get_sound_repository(
|
||||||
session: Annotated[AsyncSession, Depends(get_db)],
|
session: Annotated[AsyncSession, Depends(get_db)],
|
||||||
) -> SoundRepository:
|
) -> SoundRepository:
|
||||||
@@ -34,10 +41,11 @@ async def get_sound_repository(
|
|||||||
return SoundRepository(session)
|
return SoundRepository(session)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/", response_model=SoundsListResponse)
|
||||||
async def get_sounds( # noqa: PLR0913
|
async def get_sounds( # noqa: PLR0913
|
||||||
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
current_user: Annotated[User, Depends(get_current_active_user_flexible)],
|
||||||
sound_repo: Annotated[SoundRepository, Depends(get_sound_repository)],
|
sound_repo: Annotated[SoundRepository, Depends(get_sound_repository)],
|
||||||
|
favorite_service: Annotated[FavoriteService, Depends(get_favorite_service)],
|
||||||
types: Annotated[
|
types: Annotated[
|
||||||
list[str] | None,
|
list[str] | None,
|
||||||
Query(description="Filter by sound types (e.g., SDB, TTS, EXT)"),
|
Query(description="Filter by sound types (e.g., SDB, TTS, EXT)"),
|
||||||
@@ -62,7 +70,7 @@ async def get_sounds( # noqa: PLR0913
|
|||||||
int,
|
int,
|
||||||
Query(description="Number of results to skip", ge=0),
|
Query(description="Number of results to skip", ge=0),
|
||||||
] = 0,
|
] = 0,
|
||||||
) -> dict[str, list[Sound]]:
|
) -> SoundsListResponse:
|
||||||
"""Get sounds with optional search, filtering, and sorting."""
|
"""Get sounds with optional search, filtering, and sorting."""
|
||||||
try:
|
try:
|
||||||
sounds = await sound_repo.search_and_sort(
|
sounds = await sound_repo.search_and_sort(
|
||||||
@@ -73,13 +81,22 @@ async def get_sounds( # noqa: PLR0913
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add favorite indicators for each sound
|
||||||
|
sound_responses = []
|
||||||
|
for sound in sounds:
|
||||||
|
is_favorited = await favorite_service.is_sound_favorited(current_user.id, sound.id)
|
||||||
|
favorite_count = await favorite_service.get_sound_favorite_count(sound.id)
|
||||||
|
sound_response = SoundResponse.from_sound(sound, is_favorited, favorite_count)
|
||||||
|
sound_responses.append(sound_response)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail=f"Failed to get sounds: {e!s}",
|
detail=f"Failed to get sounds: {e!s}",
|
||||||
) from e
|
) from e
|
||||||
else:
|
else:
|
||||||
return {"sounds": sounds}
|
return SoundsListResponse(sounds=sound_responses)
|
||||||
|
|
||||||
|
|
||||||
# VLC PLAYER
|
# VLC PLAYER
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from app.core.logging import get_logger
|
|||||||
from app.core.seeds import seed_all_data
|
from app.core.seeds import seed_all_data
|
||||||
from app.models import ( # noqa: F401
|
from app.models import ( # noqa: F401
|
||||||
extraction,
|
extraction,
|
||||||
|
favorite,
|
||||||
plan,
|
plan,
|
||||||
playlist,
|
playlist,
|
||||||
playlist_sound,
|
playlist_sound,
|
||||||
|
|||||||
29
app/models/favorite.py
Normal file
29
app/models/favorite.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlmodel import Field, Relationship, UniqueConstraint
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.models.playlist import Playlist
|
||||||
|
from app.models.sound import Sound
|
||||||
|
from app.models.user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Favorite(BaseModel, table=True):
|
||||||
|
"""Database model for user favorites (sounds and playlists)."""
|
||||||
|
|
||||||
|
user_id: int = Field(foreign_key="user.id", nullable=False)
|
||||||
|
sound_id: int | None = Field(foreign_key="sound.id", default=None)
|
||||||
|
playlist_id: int | None = Field(foreign_key="playlist.id", default=None)
|
||||||
|
|
||||||
|
# constraints
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("user_id", "sound_id", name="uq_favorite_user_sound"),
|
||||||
|
UniqueConstraint("user_id", "playlist_id", name="uq_favorite_user_playlist"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# relationships
|
||||||
|
user: "User" = Relationship(back_populates="favorites")
|
||||||
|
sound: "Sound" = Relationship(back_populates="favorites")
|
||||||
|
playlist: "Playlist" = Relationship(back_populates="favorites")
|
||||||
@@ -5,6 +5,7 @@ from sqlmodel import Field, Relationship
|
|||||||
from app.models.base import BaseModel
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from app.models.favorite import Favorite
|
||||||
from app.models.playlist_sound import PlaylistSound
|
from app.models.playlist_sound import PlaylistSound
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
|
|
||||||
@@ -23,3 +24,4 @@ class Playlist(BaseModel, table=True):
|
|||||||
# relationships
|
# relationships
|
||||||
user: "User" = Relationship(back_populates="playlists")
|
user: "User" = Relationship(back_populates="playlists")
|
||||||
playlist_sounds: list["PlaylistSound"] = Relationship(back_populates="playlist")
|
playlist_sounds: list["PlaylistSound"] = Relationship(back_populates="playlist")
|
||||||
|
favorites: list["Favorite"] = Relationship(back_populates="playlist")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from app.models.base import BaseModel
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.models.extraction import Extraction
|
from app.models.extraction import Extraction
|
||||||
|
from app.models.favorite import Favorite
|
||||||
from app.models.playlist_sound import PlaylistSound
|
from app.models.playlist_sound import PlaylistSound
|
||||||
from app.models.sound_played import SoundPlayed
|
from app.models.sound_played import SoundPlayed
|
||||||
|
|
||||||
@@ -36,3 +37,4 @@ class Sound(BaseModel, table=True):
|
|||||||
playlist_sounds: list["PlaylistSound"] = Relationship(back_populates="sound")
|
playlist_sounds: list["PlaylistSound"] = Relationship(back_populates="sound")
|
||||||
extractions: list["Extraction"] = Relationship(back_populates="sound")
|
extractions: list["Extraction"] = Relationship(back_populates="sound")
|
||||||
play_history: list["SoundPlayed"] = Relationship(back_populates="sound")
|
play_history: list["SoundPlayed"] = Relationship(back_populates="sound")
|
||||||
|
favorites: list["Favorite"] = Relationship(back_populates="sound")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from app.models.base import BaseModel
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from app.models.credit_transaction import CreditTransaction
|
from app.models.credit_transaction import CreditTransaction
|
||||||
from app.models.extraction import Extraction
|
from app.models.extraction import Extraction
|
||||||
|
from app.models.favorite import Favorite
|
||||||
from app.models.plan import Plan
|
from app.models.plan import Plan
|
||||||
from app.models.playlist import Playlist
|
from app.models.playlist import Playlist
|
||||||
from app.models.sound_played import SoundPlayed
|
from app.models.sound_played import SoundPlayed
|
||||||
@@ -37,3 +38,4 @@ class User(BaseModel, table=True):
|
|||||||
sounds_played: list["SoundPlayed"] = Relationship(back_populates="user")
|
sounds_played: list["SoundPlayed"] = Relationship(back_populates="user")
|
||||||
extractions: list["Extraction"] = Relationship(back_populates="user")
|
extractions: list["Extraction"] = Relationship(back_populates="user")
|
||||||
credit_transactions: list["CreditTransaction"] = Relationship(back_populates="user")
|
credit_transactions: list["CreditTransaction"] = Relationship(back_populates="user")
|
||||||
|
favorites: list["Favorite"] = Relationship(back_populates="user")
|
||||||
|
|||||||
252
app/repositories/favorite.py
Normal file
252
app/repositories/favorite.py
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
"""Repository for managing favorites."""
|
||||||
|
|
||||||
|
from sqlmodel import and_, select
|
||||||
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
from app.core.logging import get_logger
|
||||||
|
from app.models.favorite import Favorite
|
||||||
|
from app.repositories.base import BaseRepository
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class FavoriteRepository(BaseRepository[Favorite]):
|
||||||
|
"""Repository for managing favorites."""
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession) -> None:
|
||||||
|
"""Initialize the favorite repository.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: Database session
|
||||||
|
|
||||||
|
"""
|
||||||
|
super().__init__(Favorite, session)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statement = (
|
||||||
|
select(Favorite)
|
||||||
|
.where(Favorite.user_id == user_id)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
.order_by(Favorite.created_at.desc())
|
||||||
|
)
|
||||||
|
result = await self.session.exec(statement)
|
||||||
|
return list(result.all())
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to get favorites for user: %s", user_id)
|
||||||
|
raise
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statement = (
|
||||||
|
select(Favorite)
|
||||||
|
.where(and_(Favorite.user_id == user_id, Favorite.sound_id.isnot(None)))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
.order_by(Favorite.created_at.desc())
|
||||||
|
)
|
||||||
|
result = await self.session.exec(statement)
|
||||||
|
return list(result.all())
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to get sound favorites for user: %s", user_id)
|
||||||
|
raise
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statement = (
|
||||||
|
select(Favorite)
|
||||||
|
.where(
|
||||||
|
and_(Favorite.user_id == user_id, Favorite.playlist_id.isnot(None))
|
||||||
|
)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
.order_by(Favorite.created_at.desc())
|
||||||
|
)
|
||||||
|
result = await self.session.exec(statement)
|
||||||
|
return list(result.all())
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to get playlist favorites for user: %s", user_id)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_by_user_and_sound(
|
||||||
|
self, user_id: int, sound_id: int
|
||||||
|
) -> Favorite | None:
|
||||||
|
"""Get a favorite by user and sound.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user ID
|
||||||
|
sound_id: The sound ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The favorite if found, None otherwise
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statement = select(Favorite).where(
|
||||||
|
and_(Favorite.user_id == user_id, Favorite.sound_id == sound_id)
|
||||||
|
)
|
||||||
|
result = await self.session.exec(statement)
|
||||||
|
return result.first()
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to get favorite for user %s and sound %s", user_id, sound_id
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def get_by_user_and_playlist(
|
||||||
|
self, user_id: int, playlist_id: int
|
||||||
|
) -> Favorite | None:
|
||||||
|
"""Get a favorite by user and playlist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user ID
|
||||||
|
playlist_id: The playlist ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The favorite if found, None otherwise
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statement = select(Favorite).where(
|
||||||
|
and_(Favorite.user_id == user_id, Favorite.playlist_id == playlist_id)
|
||||||
|
)
|
||||||
|
result = await self.session.exec(statement)
|
||||||
|
return result.first()
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to get favorite for user %s and playlist %s",
|
||||||
|
user_id,
|
||||||
|
playlist_id,
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
favorite = await self.get_by_user_and_sound(user_id, sound_id)
|
||||||
|
return favorite is not None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
favorite = await self.get_by_user_and_playlist(user_id, playlist_id)
|
||||||
|
return favorite is not None
|
||||||
|
|
||||||
|
async def count_user_favorites(self, user_id: int) -> int:
|
||||||
|
"""Count total favorites for a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Total number of favorites
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statement = select(Favorite).where(Favorite.user_id == user_id)
|
||||||
|
result = await self.session.exec(statement)
|
||||||
|
return len(list(result.all()))
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to count favorites for user: %s", user_id)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def count_sound_favorites(self, sound_id: int) -> int:
|
||||||
|
"""Count how many users have favorited a sound.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sound_id: The sound ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of users who favorited this sound
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statement = select(Favorite).where(Favorite.sound_id == sound_id)
|
||||||
|
result = await self.session.exec(statement)
|
||||||
|
return len(list(result.all()))
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to count favorites for sound: %s", sound_id)
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def count_playlist_favorites(self, playlist_id: int) -> int:
|
||||||
|
"""Count how many users have favorited a playlist.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
playlist_id: The playlist ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of users who favorited this playlist
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
statement = select(Favorite).where(Favorite.playlist_id == playlist_id)
|
||||||
|
result = await self.session.exec(statement)
|
||||||
|
return len(list(result.all()))
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to count favorites for playlist: %s", playlist_id)
|
||||||
|
raise
|
||||||
39
app/schemas/favorite.py
Normal file
39
app/schemas/favorite.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"""Favorite response schemas."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class FavoriteResponse(BaseModel):
|
||||||
|
"""Response schema for a favorite."""
|
||||||
|
|
||||||
|
id: int = Field(description="Favorite ID")
|
||||||
|
user_id: int = Field(description="User ID")
|
||||||
|
sound_id: int | None = Field(
|
||||||
|
description="Sound ID if this is a sound favorite", default=None
|
||||||
|
)
|
||||||
|
playlist_id: int | None = Field(
|
||||||
|
description="Playlist ID if this is a playlist favorite", default=None
|
||||||
|
)
|
||||||
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Pydantic config."""
|
||||||
|
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class FavoritesListResponse(BaseModel):
|
||||||
|
"""Response schema for a list of favorites."""
|
||||||
|
|
||||||
|
favorites: list[FavoriteResponse] = Field(description="List of favorites")
|
||||||
|
|
||||||
|
|
||||||
|
class FavoriteCountsResponse(BaseModel):
|
||||||
|
"""Response schema for favorite counts."""
|
||||||
|
|
||||||
|
total: int = Field(description="Total number of favorites")
|
||||||
|
sounds: int = Field(description="Number of favorited sounds")
|
||||||
|
playlists: int = Field(description="Number of favorited playlists")
|
||||||
@@ -33,12 +33,23 @@ class PlaylistResponse(BaseModel):
|
|||||||
is_main: bool
|
is_main: bool
|
||||||
is_current: bool
|
is_current: bool
|
||||||
is_deletable: bool
|
is_deletable: bool
|
||||||
|
is_favorited: bool = False
|
||||||
|
favorite_count: int = 0
|
||||||
created_at: str
|
created_at: str
|
||||||
updated_at: str | None
|
updated_at: str | None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_playlist(cls, playlist: Playlist) -> "PlaylistResponse":
|
def from_playlist(cls, playlist: Playlist, is_favorited: bool = False, favorite_count: int = 0) -> "PlaylistResponse":
|
||||||
"""Create response from playlist model."""
|
"""Create response from playlist model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
playlist: The Playlist model
|
||||||
|
is_favorited: Whether the playlist is favorited by the current user
|
||||||
|
favorite_count: Number of users who favorited this playlist
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PlaylistResponse instance
|
||||||
|
"""
|
||||||
if playlist.id is None:
|
if playlist.id is None:
|
||||||
msg = "Playlist ID cannot be None"
|
msg = "Playlist ID cannot be None"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
@@ -50,6 +61,8 @@ class PlaylistResponse(BaseModel):
|
|||||||
is_main=playlist.is_main,
|
is_main=playlist.is_main,
|
||||||
is_current=playlist.is_current,
|
is_current=playlist.is_current,
|
||||||
is_deletable=playlist.is_deletable,
|
is_deletable=playlist.is_deletable,
|
||||||
|
is_favorited=is_favorited,
|
||||||
|
favorite_count=favorite_count,
|
||||||
created_at=playlist.created_at.isoformat(),
|
created_at=playlist.created_at.isoformat(),
|
||||||
updated_at=playlist.updated_at.isoformat() if playlist.updated_at else None,
|
updated_at=playlist.updated_at.isoformat() if playlist.updated_at else None,
|
||||||
)
|
)
|
||||||
|
|||||||
95
app/schemas/sound.py
Normal file
95
app/schemas/sound.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""Sound response schemas."""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from app.models.sound import Sound
|
||||||
|
|
||||||
|
|
||||||
|
class SoundResponse(BaseModel):
|
||||||
|
"""Response schema for a sound with favorite indicator."""
|
||||||
|
|
||||||
|
id: int = Field(description="Sound ID")
|
||||||
|
type: str = Field(description="Sound type")
|
||||||
|
name: str = Field(description="Sound name")
|
||||||
|
filename: str = Field(description="Sound filename")
|
||||||
|
duration: int = Field(description="Duration in milliseconds")
|
||||||
|
size: int = Field(description="File size in bytes")
|
||||||
|
hash: str = Field(description="File hash")
|
||||||
|
normalized_filename: str | None = Field(
|
||||||
|
description="Normalized filename", default=None
|
||||||
|
)
|
||||||
|
normalized_duration: int | None = Field(
|
||||||
|
description="Normalized duration in milliseconds", default=None
|
||||||
|
)
|
||||||
|
normalized_size: int | None = Field(
|
||||||
|
description="Normalized file size in bytes", default=None
|
||||||
|
)
|
||||||
|
normalized_hash: str | None = Field(
|
||||||
|
description="Normalized file hash", default=None
|
||||||
|
)
|
||||||
|
thumbnail: str | None = Field(description="Thumbnail filename", default=None)
|
||||||
|
play_count: int = Field(description="Number of times played")
|
||||||
|
is_normalized: bool = Field(description="Whether the sound is normalized")
|
||||||
|
is_music: bool = Field(description="Whether the sound is music")
|
||||||
|
is_deletable: bool = Field(description="Whether the sound can be deleted")
|
||||||
|
is_favorited: bool = Field(
|
||||||
|
description="Whether the sound is favorited by the current user", default=False
|
||||||
|
)
|
||||||
|
favorite_count: int = Field(
|
||||||
|
description="Number of users who favorited this sound", default=0
|
||||||
|
)
|
||||||
|
created_at: datetime = Field(description="Creation timestamp")
|
||||||
|
updated_at: datetime = Field(description="Last update timestamp")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
"""Pydantic config."""
|
||||||
|
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_sound(
|
||||||
|
cls, sound: Sound, is_favorited: bool = False, favorite_count: int = 0
|
||||||
|
) -> "SoundResponse":
|
||||||
|
"""Create a SoundResponse from a Sound model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sound: The Sound model
|
||||||
|
is_favorited: Whether the sound is favorited by the current user
|
||||||
|
favorite_count: Number of users who favorited this sound
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SoundResponse instance
|
||||||
|
"""
|
||||||
|
if sound.id is None:
|
||||||
|
raise ValueError("Sound ID cannot be None")
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
id=sound.id,
|
||||||
|
type=sound.type,
|
||||||
|
name=sound.name,
|
||||||
|
filename=sound.filename,
|
||||||
|
duration=sound.duration,
|
||||||
|
size=sound.size,
|
||||||
|
hash=sound.hash,
|
||||||
|
normalized_filename=sound.normalized_filename,
|
||||||
|
normalized_duration=sound.normalized_duration,
|
||||||
|
normalized_size=sound.normalized_size,
|
||||||
|
normalized_hash=sound.normalized_hash,
|
||||||
|
thumbnail=sound.thumbnail,
|
||||||
|
play_count=sound.play_count,
|
||||||
|
is_normalized=sound.is_normalized,
|
||||||
|
is_music=sound.is_music,
|
||||||
|
is_deletable=sound.is_deletable,
|
||||||
|
is_favorited=is_favorited,
|
||||||
|
favorite_count=favorite_count,
|
||||||
|
created_at=sound.created_at,
|
||||||
|
updated_at=sound.updated_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SoundsListResponse(BaseModel):
|
||||||
|
"""Response schema for a list of sounds."""
|
||||||
|
|
||||||
|
sounds: list[SoundResponse] = Field(description="List of sounds")
|
||||||
318
app/services/favorite.py
Normal file
318
app/services/favorite.py
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
"""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.models.playlist import Playlist
|
||||||
|
from app.models.sound import Sound
|
||||||
|
from app.models.user import User
|
||||||
|
from app.repositories.favorite import FavoriteRepository
|
||||||
|
from app.repositories.playlist import PlaylistRepository
|
||||||
|
from app.repositories.sound import SoundRepository
|
||||||
|
from app.repositories.user import UserRepository
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
await favorite_repo.delete(favorite)
|
||||||
|
logger.info("User %s removed sound %s from favorites", user_id, 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)
|
||||||
Reference in New Issue
Block a user