feat: Implement pagination for extractions and playlists with total count in responses
This commit is contained in:
@@ -94,14 +94,18 @@ async def get_all_extractions(
|
||||
sort_by: Annotated[str, Query(description="Sort by field")] = "created_at",
|
||||
sort_order: Annotated[str, Query(description="Sort order (asc/desc)")] = "desc",
|
||||
status_filter: Annotated[str | None, Query(description="Filter by status")] = None,
|
||||
) -> dict[str, list[ExtractionInfo]]:
|
||||
page: Annotated[int, Query(description="Page number", ge=1)] = 1,
|
||||
limit: Annotated[int, Query(description="Items per page", ge=1, le=100)] = 50,
|
||||
) -> dict:
|
||||
"""Get all extractions with optional filtering, search, and sorting."""
|
||||
try:
|
||||
extractions = await extraction_service.get_all_extractions(
|
||||
result = await extraction_service.get_all_extractions(
|
||||
search=search,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
status_filter=status_filter,
|
||||
page=page,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -110,9 +114,7 @@ async def get_all_extractions(
|
||||
detail=f"Failed to get extractions: {e!s}",
|
||||
) from e
|
||||
else:
|
||||
return {
|
||||
"extractions": extractions,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/user")
|
||||
@@ -123,7 +125,9 @@ async def get_user_extractions(
|
||||
sort_by: Annotated[str, Query(description="Sort by field")] = "created_at",
|
||||
sort_order: Annotated[str, Query(description="Sort order (asc/desc)")] = "desc",
|
||||
status_filter: Annotated[str | None, Query(description="Filter by status")] = None,
|
||||
) -> dict[str, list[ExtractionInfo]]:
|
||||
page: Annotated[int, Query(description="Page number", ge=1)] = 1,
|
||||
limit: Annotated[int, Query(description="Items per page", ge=1, le=100)] = 50,
|
||||
) -> dict:
|
||||
"""Get all extractions for the current user."""
|
||||
try:
|
||||
if current_user.id is None:
|
||||
@@ -132,12 +136,14 @@ async def get_user_extractions(
|
||||
detail="User ID not available",
|
||||
)
|
||||
|
||||
extractions = await extraction_service.get_user_extractions(
|
||||
result = await extraction_service.get_user_extractions(
|
||||
user_id=current_user.id,
|
||||
search=search,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
status_filter=status_filter,
|
||||
page=page,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -146,6 +152,4 @@ async def get_user_extractions(
|
||||
detail=f"Failed to get extractions: {e!s}",
|
||||
) from e
|
||||
else:
|
||||
return {
|
||||
"extractions": extractions,
|
||||
}
|
||||
return result
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Playlist management API endpoints."""
|
||||
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
@@ -55,35 +55,29 @@ async def get_all_playlists( # noqa: PLR0913
|
||||
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,
|
||||
page: Annotated[int, Query(description="Page number", ge=1)] = 1,
|
||||
limit: Annotated[int, Query(description="Items per page", ge=1, le=100)] = 50,
|
||||
favorites_only: Annotated[
|
||||
bool,
|
||||
Query(description="Show only favorited playlists"),
|
||||
] = False,
|
||||
) -> list[PlaylistResponse]:
|
||||
) -> dict[str, Any]:
|
||||
"""Get all playlists from all users with search and sorting."""
|
||||
playlists = await playlist_service.search_and_sort_playlists(
|
||||
result = await playlist_service.search_and_sort_playlists_paginated(
|
||||
search_query=search,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
user_id=None,
|
||||
include_stats=True,
|
||||
page=page,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
favorites_only=favorites_only,
|
||||
current_user_id=current_user.id,
|
||||
)
|
||||
|
||||
# Convert to PlaylistResponse with favorite indicators
|
||||
playlist_responses = []
|
||||
for playlist_dict in playlists:
|
||||
for playlist_dict in result["playlists"]:
|
||||
# The playlist service returns dict, need to create playlist object-like structure
|
||||
is_favorited = await favorite_service.is_playlist_favorited(current_user.id, playlist_dict["id"])
|
||||
favorite_count = await favorite_service.get_playlist_favorite_count(playlist_dict["id"])
|
||||
@@ -98,7 +92,13 @@ async def get_all_playlists( # noqa: PLR0913
|
||||
}
|
||||
playlist_responses.append(playlist_response)
|
||||
|
||||
return playlist_responses
|
||||
return {
|
||||
"playlists": playlist_responses,
|
||||
"total": result["total"],
|
||||
"page": result["page"],
|
||||
"limit": result["limit"],
|
||||
"total_pages": result["total_pages"],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/user")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Extraction repository for database operations."""
|
||||
|
||||
from sqlalchemy import asc, desc, or_
|
||||
from sqlalchemy import asc, desc, func, or_
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
@@ -65,9 +65,11 @@ class ExtractionRepository(BaseRepository[Extraction]):
|
||||
sort_by: str = "created_at",
|
||||
sort_order: str = "desc",
|
||||
status_filter: str | None = None,
|
||||
) -> list[tuple[Extraction, User]]:
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[tuple[Extraction, User]], int]:
|
||||
"""Get extractions for a user with filtering, search, and sorting."""
|
||||
query = (
|
||||
base_query = (
|
||||
select(Extraction, User)
|
||||
.join(User, Extraction.user_id == User.id)
|
||||
.where(Extraction.user_id == user_id)
|
||||
@@ -76,7 +78,7 @@ class ExtractionRepository(BaseRepository[Extraction]):
|
||||
# Apply search filter
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
query = query.where(
|
||||
base_query = base_query.where(
|
||||
or_(
|
||||
Extraction.title.ilike(search_pattern),
|
||||
Extraction.url.ilike(search_pattern),
|
||||
@@ -86,17 +88,26 @@ class ExtractionRepository(BaseRepository[Extraction]):
|
||||
|
||||
# Apply status filter
|
||||
if status_filter:
|
||||
query = query.where(Extraction.status == status_filter)
|
||||
base_query = base_query.where(Extraction.status == status_filter)
|
||||
|
||||
# Apply sorting
|
||||
# Get total count before pagination
|
||||
count_query = select(func.count()).select_from(
|
||||
base_query.subquery()
|
||||
)
|
||||
count_result = await self.session.exec(count_query)
|
||||
total_count = count_result.one()
|
||||
|
||||
# Apply sorting and pagination
|
||||
sort_column = getattr(Extraction, sort_by, Extraction.created_at)
|
||||
if sort_order.lower() == "asc":
|
||||
query = query.order_by(asc(sort_column))
|
||||
base_query = base_query.order_by(asc(sort_column))
|
||||
else:
|
||||
query = query.order_by(desc(sort_column))
|
||||
base_query = base_query.order_by(desc(sort_column))
|
||||
|
||||
result = await self.session.exec(query)
|
||||
return list(result.all())
|
||||
paginated_query = base_query.limit(limit).offset(offset)
|
||||
result = await self.session.exec(paginated_query)
|
||||
|
||||
return list(result.all()), total_count
|
||||
|
||||
async def get_all_extractions_filtered(
|
||||
self,
|
||||
@@ -104,14 +115,16 @@ class ExtractionRepository(BaseRepository[Extraction]):
|
||||
sort_by: str = "created_at",
|
||||
sort_order: str = "desc",
|
||||
status_filter: str | None = None,
|
||||
) -> list[tuple[Extraction, User]]:
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[tuple[Extraction, User]], int]:
|
||||
"""Get all extractions with filtering, search, and sorting."""
|
||||
query = select(Extraction, User).join(User, Extraction.user_id == User.id)
|
||||
base_query = select(Extraction, User).join(User, Extraction.user_id == User.id)
|
||||
|
||||
# Apply search filter
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
query = query.where(
|
||||
base_query = base_query.where(
|
||||
or_(
|
||||
Extraction.title.ilike(search_pattern),
|
||||
Extraction.url.ilike(search_pattern),
|
||||
@@ -121,14 +134,23 @@ class ExtractionRepository(BaseRepository[Extraction]):
|
||||
|
||||
# Apply status filter
|
||||
if status_filter:
|
||||
query = query.where(Extraction.status == status_filter)
|
||||
base_query = base_query.where(Extraction.status == status_filter)
|
||||
|
||||
# Apply sorting
|
||||
# Get total count before pagination
|
||||
count_query = select(func.count()).select_from(
|
||||
base_query.subquery()
|
||||
)
|
||||
count_result = await self.session.exec(count_query)
|
||||
total_count = count_result.one()
|
||||
|
||||
# Apply sorting and pagination
|
||||
sort_column = getattr(Extraction, sort_by, Extraction.created_at)
|
||||
if sort_order.lower() == "asc":
|
||||
query = query.order_by(asc(sort_column))
|
||||
base_query = base_query.order_by(asc(sort_column))
|
||||
else:
|
||||
query = query.order_by(desc(sort_column))
|
||||
base_query = base_query.order_by(desc(sort_column))
|
||||
|
||||
result = await self.session.exec(query)
|
||||
return list(result.all())
|
||||
paginated_query = base_query.limit(limit).offset(offset)
|
||||
result = await self.session.exec(paginated_query)
|
||||
|
||||
return list(result.all()), total_count
|
||||
|
||||
@@ -343,7 +343,9 @@ class PlaylistRepository(BaseRepository[Playlist]):
|
||||
offset: int = 0,
|
||||
favorites_only: bool = False,
|
||||
current_user_id: int | None = None,
|
||||
) -> list[dict]:
|
||||
*,
|
||||
return_count: bool = False,
|
||||
) -> list[dict] | tuple[list[dict], int]:
|
||||
"""Search and sort playlists with optional statistics."""
|
||||
try:
|
||||
if include_stats and sort_by in (
|
||||
@@ -491,6 +493,14 @@ class PlaylistRepository(BaseRepository[Playlist]):
|
||||
# Default sorting by name ascending
|
||||
subquery = subquery.order_by(Playlist.name.asc())
|
||||
|
||||
# Get total count if requested
|
||||
total_count = 0
|
||||
if return_count:
|
||||
# Create count query from the subquery before pagination
|
||||
count_query = select(func.count()).select_from(subquery.subquery())
|
||||
count_result = await self.session.exec(count_query)
|
||||
total_count = count_result.one()
|
||||
|
||||
# Apply pagination
|
||||
if offset > 0:
|
||||
subquery = subquery.offset(offset)
|
||||
@@ -532,4 +542,6 @@ class PlaylistRepository(BaseRepository[Playlist]):
|
||||
)
|
||||
raise
|
||||
else:
|
||||
if return_count:
|
||||
return playlists, total_count
|
||||
return playlists
|
||||
|
||||
@@ -38,6 +38,16 @@ class ExtractionInfo(TypedDict):
|
||||
updated_at: str
|
||||
|
||||
|
||||
class PaginatedExtractionsResponse(TypedDict):
|
||||
"""Type definition for paginated extractions response."""
|
||||
|
||||
extractions: list[ExtractionInfo]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
class ExtractionService:
|
||||
"""Service for extracting audio from external services using yt-dlp."""
|
||||
|
||||
@@ -565,17 +575,22 @@ class ExtractionService:
|
||||
sort_by: str = "created_at",
|
||||
sort_order: str = "desc",
|
||||
status_filter: str | None = None,
|
||||
) -> list[ExtractionInfo]:
|
||||
page: int = 1,
|
||||
limit: int = 50,
|
||||
) -> PaginatedExtractionsResponse:
|
||||
"""Get all extractions for a user with filtering, search, and sorting."""
|
||||
extraction_user_tuples = await self.extraction_repo.get_user_extractions_filtered(
|
||||
offset = (page - 1) * limit
|
||||
extraction_user_tuples, total_count = await self.extraction_repo.get_user_extractions_filtered(
|
||||
user_id=user_id,
|
||||
search=search,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
status_filter=status_filter,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return [
|
||||
extractions = [
|
||||
{
|
||||
"id": extraction.id
|
||||
or 0, # Should never be None for existing extraction
|
||||
@@ -594,22 +609,37 @@ class ExtractionService:
|
||||
for extraction, user in extraction_user_tuples
|
||||
]
|
||||
|
||||
total_pages = (total_count + limit - 1) // limit # Ceiling division
|
||||
|
||||
return {
|
||||
"extractions": extractions,
|
||||
"total": total_count,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total_pages": total_pages,
|
||||
}
|
||||
|
||||
async def get_all_extractions(
|
||||
self,
|
||||
search: str | None = None,
|
||||
sort_by: str = "created_at",
|
||||
sort_order: str = "desc",
|
||||
status_filter: str | None = None,
|
||||
) -> list[ExtractionInfo]:
|
||||
page: int = 1,
|
||||
limit: int = 50,
|
||||
) -> PaginatedExtractionsResponse:
|
||||
"""Get all extractions with filtering, search, and sorting."""
|
||||
extraction_user_tuples = await self.extraction_repo.get_all_extractions_filtered(
|
||||
offset = (page - 1) * limit
|
||||
extraction_user_tuples, total_count = await self.extraction_repo.get_all_extractions_filtered(
|
||||
search=search,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
status_filter=status_filter,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
return [
|
||||
extractions = [
|
||||
{
|
||||
"id": extraction.id
|
||||
or 0, # Should never be None for existing extraction
|
||||
@@ -628,6 +658,16 @@ class ExtractionService:
|
||||
for extraction, user in extraction_user_tuples
|
||||
]
|
||||
|
||||
total_pages = (total_count + limit - 1) // limit # Ceiling division
|
||||
|
||||
return {
|
||||
"extractions": extractions,
|
||||
"total": total_count,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total_pages": total_pages,
|
||||
}
|
||||
|
||||
async def get_pending_extractions(self) -> list[ExtractionInfo]:
|
||||
"""Get all pending extractions."""
|
||||
extraction_user_tuples = await self.extraction_repo.get_pending_extractions()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Playlist service for business logic operations."""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
@@ -14,6 +14,15 @@ from app.repositories.sound import SoundRepository
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PaginatedPlaylistsResponse(TypedDict):
|
||||
"""Response type for paginated playlists."""
|
||||
playlists: list[dict]
|
||||
total: int
|
||||
page: int
|
||||
limit: int
|
||||
total_pages: int
|
||||
|
||||
|
||||
async def _reload_player_playlist() -> None:
|
||||
"""Reload the player playlist after current playlist changes."""
|
||||
try:
|
||||
@@ -262,6 +271,45 @@ class PlaylistService:
|
||||
current_user_id=current_user_id,
|
||||
)
|
||||
|
||||
async def search_and_sort_playlists_paginated( # noqa: PLR0913
|
||||
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,
|
||||
page: int = 1,
|
||||
limit: int = 50,
|
||||
favorites_only: bool = False,
|
||||
current_user_id: int | None = None,
|
||||
) -> PaginatedPlaylistsResponse:
|
||||
"""Search and sort playlists with pagination."""
|
||||
offset = (page - 1) * limit
|
||||
|
||||
playlists, total_count = 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,
|
||||
favorites_only=favorites_only,
|
||||
current_user_id=current_user_id,
|
||||
return_count=True,
|
||||
)
|
||||
|
||||
total_pages = (total_count + limit - 1) // limit # Ceiling division
|
||||
|
||||
return PaginatedPlaylistsResponse(
|
||||
playlists=playlists,
|
||||
total=total_count,
|
||||
page=page,
|
||||
limit=limit,
|
||||
total_pages=total_pages,
|
||||
)
|
||||
|
||||
async def get_playlist_sounds(self, playlist_id: int) -> list[Sound]:
|
||||
"""Get all sounds in a playlist."""
|
||||
await self.get_playlist_by_id(playlist_id) # Verify playlist exists
|
||||
|
||||
Reference in New Issue
Block a user