feat: Implement search and sorting functionality for playlists in API and repository
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
@@ -19,6 +19,7 @@ from app.schemas.playlist import (
|
|||||||
PlaylistUpdateRequest,
|
PlaylistUpdateRequest,
|
||||||
)
|
)
|
||||||
from app.services.playlist import PlaylistService
|
from app.services.playlist import PlaylistService
|
||||||
|
from app.repositories.playlist import PlaylistSortField, SortOrder
|
||||||
|
|
||||||
router = APIRouter(prefix="/playlists", tags=["playlists"])
|
router = APIRouter(prefix="/playlists", tags=["playlists"])
|
||||||
|
|
||||||
@@ -34,10 +35,38 @@ async def get_playlist_service(
|
|||||||
async def get_all_playlists(
|
async def get_all_playlists(
|
||||||
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
||||||
playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)],
|
playlist_service: Annotated[PlaylistService, Depends(get_playlist_service)],
|
||||||
) -> list[PlaylistResponse]:
|
search: Annotated[
|
||||||
"""Get all playlists from all users."""
|
str | None,
|
||||||
playlists = await playlist_service.get_all_playlists()
|
Query(description="Search playlists by name"),
|
||||||
return [PlaylistResponse.from_playlist(playlist) for playlist in playlists]
|
] = None,
|
||||||
|
sort_by: Annotated[
|
||||||
|
PlaylistSortField | None,
|
||||||
|
Query(description="Sort by field"),
|
||||||
|
] = None,
|
||||||
|
sort_order: Annotated[
|
||||||
|
SortOrder,
|
||||||
|
Query(description="Sort order (asc or desc)"),
|
||||||
|
] = SortOrder.ASC,
|
||||||
|
limit: Annotated[
|
||||||
|
int | None,
|
||||||
|
Query(description="Maximum number of results", ge=1, le=1000),
|
||||||
|
] = None,
|
||||||
|
offset: Annotated[
|
||||||
|
int,
|
||||||
|
Query(description="Number of results to skip", ge=0),
|
||||||
|
] = 0,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get all playlists from all users with search and sorting."""
|
||||||
|
playlists = await playlist_service.search_and_sort_playlists(
|
||||||
|
search_query=search,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_order=sort_order,
|
||||||
|
user_id=None,
|
||||||
|
include_stats=True,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
return playlists
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user")
|
@router.get("/user")
|
||||||
|
|||||||
@@ -1,19 +1,39 @@
|
|||||||
"""Playlist repository for database operations."""
|
"""Playlist repository for database operations."""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlmodel import select
|
from sqlmodel import col, select
|
||||||
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.playlist import Playlist
|
from app.models.playlist import Playlist
|
||||||
from app.models.playlist_sound import PlaylistSound
|
from app.models.playlist_sound import PlaylistSound
|
||||||
from app.models.sound import Sound
|
from app.models.sound import Sound
|
||||||
|
from app.models.user import User
|
||||||
from app.repositories.base import BaseRepository
|
from app.repositories.base import BaseRepository
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistSortField(str, Enum):
|
||||||
|
"""Playlist sort field enumeration."""
|
||||||
|
|
||||||
|
NAME = "name"
|
||||||
|
GENRE = "genre"
|
||||||
|
CREATED_AT = "created_at"
|
||||||
|
UPDATED_AT = "updated_at"
|
||||||
|
SOUND_COUNT = "sound_count"
|
||||||
|
TOTAL_DURATION = "total_duration"
|
||||||
|
|
||||||
|
|
||||||
|
class SortOrder(str, Enum):
|
||||||
|
"""Sort order enumeration."""
|
||||||
|
|
||||||
|
ASC = "asc"
|
||||||
|
DESC = "desc"
|
||||||
|
|
||||||
|
|
||||||
class PlaylistRepository(BaseRepository[Playlist]):
|
class PlaylistRepository(BaseRepository[Playlist]):
|
||||||
"""Repository for playlist operations."""
|
"""Repository for playlist operations."""
|
||||||
|
|
||||||
@@ -237,3 +257,153 @@ class PlaylistRepository(BaseRepository[Playlist]):
|
|||||||
playlist_id,
|
playlist_id,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
async def search_and_sort(
|
||||||
|
self,
|
||||||
|
search_query: str | None = None,
|
||||||
|
sort_by: PlaylistSortField | None = None,
|
||||||
|
sort_order: SortOrder = SortOrder.ASC,
|
||||||
|
user_id: int | None = None,
|
||||||
|
include_stats: bool = False,
|
||||||
|
limit: int | None = None,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Search and sort playlists with optional statistics."""
|
||||||
|
try:
|
||||||
|
if include_stats and sort_by in (PlaylistSortField.SOUND_COUNT, PlaylistSortField.TOTAL_DURATION):
|
||||||
|
# Use subquery for sorting by stats
|
||||||
|
subquery = (
|
||||||
|
select(
|
||||||
|
Playlist.id,
|
||||||
|
Playlist.name,
|
||||||
|
Playlist.description,
|
||||||
|
Playlist.genre,
|
||||||
|
Playlist.user_id,
|
||||||
|
Playlist.is_main,
|
||||||
|
Playlist.is_current,
|
||||||
|
Playlist.is_deletable,
|
||||||
|
Playlist.created_at,
|
||||||
|
Playlist.updated_at,
|
||||||
|
func.count(PlaylistSound.id).label("sound_count"),
|
||||||
|
func.coalesce(func.sum(Sound.duration), 0).label("total_duration"),
|
||||||
|
User.name.label("user_name"),
|
||||||
|
)
|
||||||
|
.select_from(Playlist)
|
||||||
|
.join(User, Playlist.user_id == User.id, isouter=True)
|
||||||
|
.join(PlaylistSound, Playlist.id == PlaylistSound.playlist_id, isouter=True)
|
||||||
|
.join(Sound, PlaylistSound.sound_id == Sound.id, isouter=True)
|
||||||
|
.group_by(Playlist.id, User.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply filters
|
||||||
|
if search_query and search_query.strip():
|
||||||
|
search_pattern = f"%{search_query.strip().lower()}%"
|
||||||
|
subquery = subquery.where(func.lower(Playlist.name).like(search_pattern))
|
||||||
|
|
||||||
|
if user_id is not None:
|
||||||
|
subquery = subquery.where(Playlist.user_id == user_id)
|
||||||
|
|
||||||
|
# Apply sorting
|
||||||
|
if sort_by == PlaylistSortField.SOUND_COUNT:
|
||||||
|
if sort_order == SortOrder.DESC:
|
||||||
|
subquery = subquery.order_by(func.count(PlaylistSound.id).desc())
|
||||||
|
else:
|
||||||
|
subquery = subquery.order_by(func.count(PlaylistSound.id).asc())
|
||||||
|
elif sort_by == PlaylistSortField.TOTAL_DURATION:
|
||||||
|
if sort_order == SortOrder.DESC:
|
||||||
|
subquery = subquery.order_by(func.coalesce(func.sum(Sound.duration), 0).desc())
|
||||||
|
else:
|
||||||
|
subquery = subquery.order_by(func.coalesce(func.sum(Sound.duration), 0).asc())
|
||||||
|
else:
|
||||||
|
# Default sorting by name
|
||||||
|
subquery = subquery.order_by(Playlist.name.asc())
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Simple query without stats-based sorting
|
||||||
|
subquery = (
|
||||||
|
select(
|
||||||
|
Playlist.id,
|
||||||
|
Playlist.name,
|
||||||
|
Playlist.description,
|
||||||
|
Playlist.genre,
|
||||||
|
Playlist.user_id,
|
||||||
|
Playlist.is_main,
|
||||||
|
Playlist.is_current,
|
||||||
|
Playlist.is_deletable,
|
||||||
|
Playlist.created_at,
|
||||||
|
Playlist.updated_at,
|
||||||
|
func.count(PlaylistSound.id).label("sound_count"),
|
||||||
|
func.coalesce(func.sum(Sound.duration), 0).label("total_duration"),
|
||||||
|
User.name.label("user_name"),
|
||||||
|
)
|
||||||
|
.select_from(Playlist)
|
||||||
|
.join(User, Playlist.user_id == User.id, isouter=True)
|
||||||
|
.join(PlaylistSound, Playlist.id == PlaylistSound.playlist_id, isouter=True)
|
||||||
|
.join(Sound, PlaylistSound.sound_id == Sound.id, isouter=True)
|
||||||
|
.group_by(Playlist.id, User.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply filters
|
||||||
|
if search_query and search_query.strip():
|
||||||
|
search_pattern = f"%{search_query.strip().lower()}%"
|
||||||
|
subquery = subquery.where(func.lower(Playlist.name).like(search_pattern))
|
||||||
|
|
||||||
|
if user_id is not None:
|
||||||
|
subquery = subquery.where(Playlist.user_id == user_id)
|
||||||
|
|
||||||
|
# Apply sorting
|
||||||
|
if sort_by:
|
||||||
|
if sort_by == PlaylistSortField.NAME:
|
||||||
|
sort_column = Playlist.name
|
||||||
|
elif sort_by == PlaylistSortField.GENRE:
|
||||||
|
sort_column = Playlist.genre
|
||||||
|
elif sort_by == PlaylistSortField.CREATED_AT:
|
||||||
|
sort_column = Playlist.created_at
|
||||||
|
elif sort_by == PlaylistSortField.UPDATED_AT:
|
||||||
|
sort_column = Playlist.updated_at
|
||||||
|
else:
|
||||||
|
sort_column = Playlist.name
|
||||||
|
|
||||||
|
if sort_order == SortOrder.DESC:
|
||||||
|
subquery = subquery.order_by(sort_column.desc())
|
||||||
|
else:
|
||||||
|
subquery = subquery.order_by(sort_column.asc())
|
||||||
|
else:
|
||||||
|
# Default sorting by name ascending
|
||||||
|
subquery = subquery.order_by(Playlist.name.asc())
|
||||||
|
|
||||||
|
# Apply pagination
|
||||||
|
if offset > 0:
|
||||||
|
subquery = subquery.offset(offset)
|
||||||
|
if limit is not None:
|
||||||
|
subquery = subquery.limit(limit)
|
||||||
|
|
||||||
|
result = await self.session.exec(subquery)
|
||||||
|
rows = result.all()
|
||||||
|
|
||||||
|
# Convert to dictionary format
|
||||||
|
playlists = []
|
||||||
|
for row in rows:
|
||||||
|
playlists.append({
|
||||||
|
"id": row.id,
|
||||||
|
"name": row.name,
|
||||||
|
"description": row.description,
|
||||||
|
"genre": row.genre,
|
||||||
|
"user_id": row.user_id,
|
||||||
|
"user_name": row.user_name,
|
||||||
|
"is_main": row.is_main,
|
||||||
|
"is_current": row.is_current,
|
||||||
|
"is_deletable": row.is_deletable,
|
||||||
|
"created_at": row.created_at,
|
||||||
|
"updated_at": row.updated_at,
|
||||||
|
"sound_count": row.sound_count or 0,
|
||||||
|
"total_duration": row.total_duration or 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return playlists
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to search and sort playlists: query=%s, sort_by=%s, sort_order=%s",
|
||||||
|
search_query, sort_by, sort_order
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from sqlmodel.ext.asyncio.session import AsyncSession
|
|||||||
from app.core.logging import get_logger
|
from app.core.logging import get_logger
|
||||||
from app.models.playlist import Playlist
|
from app.models.playlist import Playlist
|
||||||
from app.models.sound import Sound
|
from app.models.sound import Sound
|
||||||
from app.repositories.playlist import PlaylistRepository
|
from app.repositories.playlist import PlaylistRepository, PlaylistSortField, SortOrder
|
||||||
from app.repositories.sound import SoundRepository
|
from app.repositories.sound import SoundRepository
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -212,6 +212,27 @@ class PlaylistService:
|
|||||||
"""Search all playlists by name."""
|
"""Search all playlists by name."""
|
||||||
return await self.playlist_repo.search_by_name(query)
|
return await self.playlist_repo.search_by_name(query)
|
||||||
|
|
||||||
|
async def search_and_sort_playlists(
|
||||||
|
self,
|
||||||
|
search_query: str | None = None,
|
||||||
|
sort_by: PlaylistSortField | None = None,
|
||||||
|
sort_order: SortOrder = SortOrder.ASC,
|
||||||
|
user_id: int | None = None,
|
||||||
|
include_stats: bool = False,
|
||||||
|
limit: int | None = None,
|
||||||
|
offset: int = 0,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Search and sort playlists with optional statistics."""
|
||||||
|
return await self.playlist_repo.search_and_sort(
|
||||||
|
search_query=search_query,
|
||||||
|
sort_by=sort_by,
|
||||||
|
sort_order=sort_order,
|
||||||
|
user_id=user_id,
|
||||||
|
include_stats=include_stats,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
async def get_playlist_sounds(self, playlist_id: int) -> list[Sound]:
|
async def get_playlist_sounds(self, playlist_id: int) -> list[Sound]:
|
||||||
"""Get all sounds in a playlist."""
|
"""Get all sounds in a playlist."""
|
||||||
await self.get_playlist_by_id(playlist_id) # Verify playlist exists
|
await self.get_playlist_by_id(playlist_id) # Verify playlist exists
|
||||||
|
|||||||
Reference in New Issue
Block a user