feat: Add filtering, searching, and sorting to extraction retrieval endpoints

This commit is contained in:
JSC
2025-08-17 01:44:43 +02:00
parent 3326e406f8
commit 66d22df7dd
3 changed files with 126 additions and 11 deletions

View File

@@ -88,6 +88,34 @@ async def get_extraction(
@router.get("/") @router.get("/")
async def get_all_extractions(
extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)],
search: Annotated[str | None, Query(description="Search in title, URL, or service")] = None,
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]]:
"""Get all extractions with optional filtering, search, and sorting."""
try:
extractions = await extraction_service.get_all_extractions(
search=search,
sort_by=sort_by,
sort_order=sort_order,
status_filter=status_filter,
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to get extractions: {e!s}",
) from e
else:
return {
"extractions": extractions,
}
@router.get("/user")
async def get_user_extractions( async def get_user_extractions(
current_user: Annotated[User, Depends(get_current_active_user_flexible)], current_user: Annotated[User, Depends(get_current_active_user_flexible)],
extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)], extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)],

View File

@@ -5,6 +5,7 @@ from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from app.models.extraction import Extraction from app.models.extraction import Extraction
from app.models.user import User
from app.repositories.base import BaseRepository from app.repositories.base import BaseRepository
@@ -38,10 +39,11 @@ class ExtractionRepository(BaseRepository[Extraction]):
) )
return list(result.all()) return list(result.all())
async def get_pending_extractions(self) -> list[Extraction]: async def get_pending_extractions(self) -> list[tuple[Extraction, User]]:
"""Get all pending extractions.""" """Get all pending extractions."""
result = await self.session.exec( result = await self.session.exec(
select(Extraction) select(Extraction, User)
.join(User, Extraction.user_id == User.id)
.where(Extraction.status == "pending") .where(Extraction.status == "pending")
.order_by(Extraction.created_at), .order_by(Extraction.created_at),
) )
@@ -63,9 +65,13 @@ class ExtractionRepository(BaseRepository[Extraction]):
sort_by: str = "created_at", sort_by: str = "created_at",
sort_order: str = "desc", sort_order: str = "desc",
status_filter: str | None = None, status_filter: str | None = None,
) -> list[Extraction]: ) -> list[tuple[Extraction, User]]:
"""Get extractions for a user with filtering, search, and sorting.""" """Get extractions for a user with filtering, search, and sorting."""
query = select(Extraction).where(Extraction.user_id == user_id) query = (
select(Extraction, User)
.join(User, Extraction.user_id == User.id)
.where(Extraction.user_id == user_id)
)
# Apply search filter # Apply search filter
if search: if search:
@@ -75,7 +81,42 @@ class ExtractionRepository(BaseRepository[Extraction]):
Extraction.title.ilike(search_pattern), Extraction.title.ilike(search_pattern),
Extraction.url.ilike(search_pattern), Extraction.url.ilike(search_pattern),
Extraction.service.ilike(search_pattern), Extraction.service.ilike(search_pattern),
) ),
)
# Apply status filter
if status_filter:
query = query.where(Extraction.status == status_filter)
# Apply sorting
sort_column = getattr(Extraction, sort_by, Extraction.created_at)
if sort_order.lower() == "asc":
query = query.order_by(asc(sort_column))
else:
query = query.order_by(desc(sort_column))
result = await self.session.exec(query)
return list(result.all())
async def get_all_extractions_filtered(
self,
search: str | None = None,
sort_by: str = "created_at",
sort_order: str = "desc",
status_filter: str | None = None,
) -> list[tuple[Extraction, User]]:
"""Get all extractions with filtering, search, and sorting."""
query = select(Extraction, User).join(User, Extraction.user_id == User.id)
# Apply search filter
if search:
search_pattern = f"%{search}%"
query = query.where(
or_(
Extraction.title.ilike(search_pattern),
Extraction.url.ilike(search_pattern),
Extraction.service.ilike(search_pattern),
),
) )
# Apply status filter # Apply status filter

View File

@@ -13,6 +13,7 @@ from app.core.logging import get_logger
from app.models.sound import Sound from app.models.sound import Sound
from app.repositories.extraction import ExtractionRepository from app.repositories.extraction import ExtractionRepository
from app.repositories.sound import SoundRepository from app.repositories.sound import SoundRepository
from app.repositories.user import UserRepository
from app.services.playlist import PlaylistService from app.services.playlist import PlaylistService
from app.services.sound_normalizer import SoundNormalizerService from app.services.sound_normalizer import SoundNormalizerService
from app.utils.audio import get_audio_duration, get_file_hash, get_file_size from app.utils.audio import get_audio_duration, get_file_hash, get_file_size
@@ -32,6 +33,7 @@ class ExtractionInfo(TypedDict):
error: str | None error: str | None
sound_id: int | None sound_id: int | None
user_id: int user_id: int
user_name: str | None
created_at: str created_at: str
updated_at: str updated_at: str
@@ -44,6 +46,7 @@ class ExtractionService:
self.session = session self.session = session
self.extraction_repo = ExtractionRepository(session) self.extraction_repo = ExtractionRepository(session)
self.sound_repo = SoundRepository(session) self.sound_repo = SoundRepository(session)
self.user_repo = UserRepository(session)
self.playlist_service = PlaylistService(session) self.playlist_service = PlaylistService(session)
# Ensure required directories exist # Ensure required directories exist
@@ -66,6 +69,12 @@ class ExtractionService:
logger.info("Creating extraction for URL: %s (user: %d)", url, user_id) logger.info("Creating extraction for URL: %s (user: %d)", url, user_id)
try: try:
# Get user information
user = await self.user_repo.get_by_id(user_id)
if not user:
msg = f"User {user_id} not found"
raise ValueError(msg)
# Create the extraction record without service detection for fast response # Create the extraction record without service detection for fast response
extraction_data = { extraction_data = {
"url": url, "url": url,
@@ -92,6 +101,7 @@ class ExtractionService:
"error": extraction.error, "error": extraction.error,
"sound_id": extraction.sound_id, "sound_id": extraction.sound_id,
"user_id": extraction.user_id, "user_id": extraction.user_id,
"user_name": user.name,
"created_at": extraction.created_at.isoformat(), "created_at": extraction.created_at.isoformat(),
"updated_at": extraction.updated_at.isoformat(), "updated_at": extraction.updated_at.isoformat(),
} }
@@ -554,7 +564,7 @@ class ExtractionService:
status_filter: str | None = None, status_filter: str | None = None,
) -> list[ExtractionInfo]: ) -> list[ExtractionInfo]:
"""Get all extractions for a user with filtering, search, and sorting.""" """Get all extractions for a user with filtering, search, and sorting."""
extractions = await self.extraction_repo.get_user_extractions_filtered( extraction_user_tuples = await self.extraction_repo.get_user_extractions_filtered(
user_id=user_id, user_id=user_id,
search=search, search=search,
sort_by=sort_by, sort_by=sort_by,
@@ -574,15 +584,27 @@ class ExtractionService:
"error": extraction.error, "error": extraction.error,
"sound_id": extraction.sound_id, "sound_id": extraction.sound_id,
"user_id": extraction.user_id, "user_id": extraction.user_id,
"user_name": user.name,
"created_at": extraction.created_at.isoformat(), "created_at": extraction.created_at.isoformat(),
"updated_at": extraction.updated_at.isoformat(), "updated_at": extraction.updated_at.isoformat(),
} }
for extraction in extractions for extraction, user in extraction_user_tuples
] ]
async def get_pending_extractions(self) -> list[ExtractionInfo]: async def get_all_extractions(
"""Get all pending extractions.""" self,
extractions = await self.extraction_repo.get_pending_extractions() search: str | None = None,
sort_by: str = "created_at",
sort_order: str = "desc",
status_filter: str | None = None,
) -> list[ExtractionInfo]:
"""Get all extractions with filtering, search, and sorting."""
extraction_user_tuples = await self.extraction_repo.get_all_extractions_filtered(
search=search,
sort_by=sort_by,
sort_order=sort_order,
status_filter=status_filter,
)
return [ return [
{ {
@@ -596,8 +618,32 @@ class ExtractionService:
"error": extraction.error, "error": extraction.error,
"sound_id": extraction.sound_id, "sound_id": extraction.sound_id,
"user_id": extraction.user_id, "user_id": extraction.user_id,
"user_name": user.name,
"created_at": extraction.created_at.isoformat(), "created_at": extraction.created_at.isoformat(),
"updated_at": extraction.updated_at.isoformat(), "updated_at": extraction.updated_at.isoformat(),
} }
for extraction in extractions for extraction, user in extraction_user_tuples
]
async def get_pending_extractions(self) -> list[ExtractionInfo]:
"""Get all pending extractions."""
extraction_user_tuples = await self.extraction_repo.get_pending_extractions()
return [
{
"id": extraction.id
or 0, # Should never be None for existing extraction
"url": extraction.url,
"service": extraction.service,
"service_id": extraction.service_id,
"title": extraction.title,
"status": extraction.status,
"error": extraction.error,
"sound_id": extraction.sound_id,
"user_id": extraction.user_id,
"user_name": user.name,
"created_at": extraction.created_at.isoformat(),
"updated_at": extraction.updated_at.isoformat(),
}
for extraction, user in extraction_user_tuples
] ]