diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py index 2b260a7..a02f9df 100644 --- a/app/api/v1/__init__.py +++ b/app/api/v1/__init__.py @@ -2,13 +2,14 @@ from fastapi import APIRouter -from app.api.v1 import admin, auth, main, player, playlists, socket, sounds +from app.api.v1 import admin, auth, extractions, main, player, playlists, socket, sounds # V1 API router with v1 prefix api_router = APIRouter(prefix="/v1") # Include all route modules api_router.include_router(auth.router, tags=["authentication"]) +api_router.include_router(extractions.router, tags=["extractions"]) api_router.include_router(main.router, tags=["main"]) api_router.include_router(player.router, tags=["player"]) api_router.include_router(playlists.router, tags=["playlists"]) diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index 9dfdab6..bd24a55 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -2,9 +2,10 @@ from fastapi import APIRouter -from app.api.v1.admin import sounds +from app.api.v1.admin import extractions, sounds router = APIRouter(prefix="/admin") # Include all admin sub-routers +router.include_router(extractions.router) router.include_router(sounds.router) diff --git a/app/api/v1/admin/extractions.py b/app/api/v1/admin/extractions.py new file mode 100644 index 0000000..f0a9ba5 --- /dev/null +++ b/app/api/v1/admin/extractions.py @@ -0,0 +1,19 @@ +"""Admin audio extraction API endpoints.""" + +from typing import Annotated + +from fastapi import APIRouter, Depends + +from app.core.dependencies import get_admin_user +from app.models.user import User +from app.services.extraction_processor import extraction_processor + +router = APIRouter(prefix="/extractions", tags=["admin-extractions"]) + + +@router.get("/status") +async def get_extraction_processor_status( + current_user: Annotated[User, Depends(get_admin_user)], # noqa: ARG001 +) -> dict: + """Get the status of the extraction processor. Admin only.""" + return extraction_processor.get_status() diff --git a/app/api/v1/admin/sounds.py b/app/api/v1/admin/sounds.py index bbb8e02..b890d5b 100644 --- a/app/api/v1/admin/sounds.py +++ b/app/api/v1/admin/sounds.py @@ -8,7 +8,6 @@ from sqlmodel.ext.asyncio.session import AsyncSession from app.core.database import get_db from app.core.dependencies import get_admin_user from app.models.user import User -from app.services.extraction_processor import extraction_processor from app.services.sound_normalizer import NormalizationResults, SoundNormalizerService from app.services.sound_scanner import ScanResults, SoundScannerService @@ -229,10 +228,3 @@ async def normalize_sound_by_id( ) from e -# EXTRACTION PROCESSOR STATUS -@router.get("/extract/status") -async def get_extraction_processor_status( - current_user: Annotated[User, Depends(get_admin_user)], # noqa: ARG001 -) -> dict: - """Get the status of the extraction processor. Admin only.""" - return extraction_processor.get_status() diff --git a/app/api/v1/extractions.py b/app/api/v1/extractions.py new file mode 100644 index 0000000..feba222 --- /dev/null +++ b/app/api/v1/extractions.py @@ -0,0 +1,113 @@ +"""Audio extraction API endpoints.""" + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.core.database import get_db +from app.core.dependencies import get_current_active_user_flexible +from app.models.user import User +from app.services.extraction import ExtractionInfo, ExtractionService +from app.services.extraction_processor import extraction_processor + +router = APIRouter(prefix="/extractions", tags=["extractions"]) + + +async def get_extraction_service( + session: Annotated[AsyncSession, Depends(get_db)], +) -> ExtractionService: + """Get the extraction service.""" + return ExtractionService(session) + + +@router.post("/") +async def create_extraction( + url: str, + current_user: Annotated[User, Depends(get_current_active_user_flexible)], + extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)], +) -> dict[str, ExtractionInfo | str]: + """Create a new extraction job for a URL.""" + try: + if current_user.id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User ID not available", + ) + + extraction_info = await extraction_service.create_extraction( + url, + current_user.id, + ) + + # Queue the extraction for background processing + await extraction_processor.queue_extraction(extraction_info["id"]) + + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e), + ) from e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create extraction: {e!s}", + ) from e + else: + return { + "message": "Extraction queued successfully", + "extraction": extraction_info, + } + + +@router.get("/{extraction_id}") +async def get_extraction( + extraction_id: int, + current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 + extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)], +) -> ExtractionInfo: + """Get extraction information by ID.""" + try: + extraction_info = await extraction_service.get_extraction_by_id(extraction_id) + + if not extraction_info: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Extraction {extraction_id} not found", + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get extraction: {e!s}", + ) from e + else: + return extraction_info + + +@router.get("/") +async def get_user_extractions( + current_user: Annotated[User, Depends(get_current_active_user_flexible)], + extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)], +) -> dict[str, list[ExtractionInfo]]: + """Get all extractions for the current user.""" + try: + if current_user.id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User ID not available", + ) + + extractions = await extraction_service.get_user_extractions(current_user.id) + + 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, + } diff --git a/app/api/v1/sounds.py b/app/api/v1/sounds.py index cf0e1fe..2ee11c8 100644 --- a/app/api/v1/sounds.py +++ b/app/api/v1/sounds.py @@ -11,19 +11,11 @@ from app.models.credit_action import CreditActionType from app.models.user import User from app.repositories.sound import SoundRepository from app.services.credit import CreditService, InsufficientCreditsError -from app.services.extraction import ExtractionInfo, ExtractionService -from app.services.extraction_processor import extraction_processor from app.services.vlc_player import VLCPlayerService, get_vlc_player_service router = APIRouter(prefix="/sounds", tags=["sounds"]) -async def get_extraction_service( - session: Annotated[AsyncSession, Depends(get_db)], -) -> ExtractionService: - """Get the extraction service.""" - return ExtractionService(session) - def get_vlc_player() -> VLCPlayerService: """Get the VLC player service.""" @@ -42,98 +34,6 @@ async def get_sound_repository( return SoundRepository(session) -# EXTRACT -@router.post("/extract") -async def create_extraction( - url: str, - current_user: Annotated[User, Depends(get_current_active_user_flexible)], - extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)], -) -> dict[str, ExtractionInfo | str]: - """Create a new extraction job for a URL.""" - try: - if current_user.id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User ID not available", - ) - - extraction_info = await extraction_service.create_extraction( - url, - current_user.id, - ) - - # Queue the extraction for background processing - await extraction_processor.queue_extraction(extraction_info["id"]) - - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=str(e), - ) from e - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to create extraction: {e!s}", - ) from e - else: - return { - "message": "Extraction queued successfully", - "extraction": extraction_info, - } - - -@router.get("/extract/{extraction_id}") -async def get_extraction( - extraction_id: int, - current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001 - extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)], -) -> ExtractionInfo: - """Get extraction information by ID.""" - try: - extraction_info = await extraction_service.get_extraction_by_id(extraction_id) - - if not extraction_info: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Extraction {extraction_id} not found", - ) - - except HTTPException: - raise - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=f"Failed to get extraction: {e!s}", - ) from e - else: - return extraction_info - - -@router.get("/extract") -async def get_user_extractions( - current_user: Annotated[User, Depends(get_current_active_user_flexible)], - extraction_service: Annotated[ExtractionService, Depends(get_extraction_service)], -) -> dict[str, list[ExtractionInfo]]: - """Get all extractions for the current user.""" - try: - if current_user.id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User ID not available", - ) - - extractions = await extraction_service.get_user_extractions(current_user.id) - - 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, - } - # VLC PLAYER @router.post("/play/{sound_id}") diff --git a/tests/api/v1/admin/test_sound_endpoints.py b/tests/api/v1/admin/test_sound_endpoints.py index f92b6a9..772333e 100644 --- a/tests/api/v1/admin/test_sound_endpoints.py +++ b/tests/api/v1/admin/test_sound_endpoints.py @@ -513,7 +513,7 @@ class TestAdminSoundEndpoints: mock_get_status.return_value = mock_status response = await authenticated_admin_client.get( - "/api/v1/admin/sounds/extract/status", + "/api/v1/admin/extractions/status", ) assert response.status_code == 200 @@ -526,7 +526,7 @@ class TestAdminSoundEndpoints: client: AsyncClient, ) -> None: """Test getting extraction processor status without authentication.""" - response = await client.get("/api/v1/admin/sounds/extract/status") + response = await client.get("/api/v1/admin/extractions/status") assert response.status_code == 401 data = response.json() @@ -556,7 +556,7 @@ class TestAdminSoundEndpoints: base_url="http://test", ) as client: response = await client.get( - "/api/v1/admin/sounds/extract/status", + "/api/v1/admin/extractions/status", headers=headers, ) diff --git a/tests/api/v1/test_extraction_endpoints.py b/tests/api/v1/test_extraction_endpoints.py index cb57ad7..d333fc9 100644 --- a/tests/api/v1/test_extraction_endpoints.py +++ b/tests/api/v1/test_extraction_endpoints.py @@ -18,7 +18,7 @@ class TestExtractionEndpoints: test_client.cookies.update(auth_cookies) response = await test_client.post( - "/api/v1/sounds/extract", + "/api/v1/extractions/", params={"url": "https://www.youtube.com/watch?v=test"}, ) @@ -32,7 +32,7 @@ class TestExtractionEndpoints: ) -> None: """Test extraction creation without authentication.""" response = await test_client.post( - "/api/v1/sounds/extract", + "/api/v1/extractions/", params={"url": "https://www.youtube.com/watch?v=test"}, ) @@ -44,7 +44,7 @@ class TestExtractionEndpoints: self, test_client: AsyncClient, ) -> None: """Test extraction retrieval without authentication.""" - response = await test_client.get("/api/v1/sounds/extract/1") + response = await test_client.get("/api/v1/extractions/1") # Should return 401 for missing authentication assert response.status_code == 401 @@ -60,7 +60,7 @@ class TestExtractionEndpoints: test_client.cookies.update(admin_cookies) # The new admin endpoint should work - response = await test_client.get("/api/v1/admin/sounds/extract/status") + response = await test_client.get("/api/v1/admin/extractions/status") assert response.status_code == 200 data = response.json() assert "running" in data or "is_running" in data @@ -76,7 +76,7 @@ class TestExtractionEndpoints: # Set cookies on client instance to avoid deprecation warning test_client.cookies.update(auth_cookies) - response = await test_client.get("/api/v1/sounds/extract") + response = await test_client.get("/api/v1/extractions/") # Should succeed and return empty list (no extractions in test DB) assert response.status_code == 200 diff --git a/tests/api/v1/test_sound_endpoints.py b/tests/api/v1/test_sound_endpoints.py index f5f8bf1..ddb7b94 100644 --- a/tests/api/v1/test_sound_endpoints.py +++ b/tests/api/v1/test_sound_endpoints.py @@ -45,7 +45,7 @@ class TestSoundEndpoints: mock_queue.return_value = None response = await authenticated_client.post( - "/api/v1/sounds/extract", + "/api/v1/extractions/", params={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"}, ) @@ -62,7 +62,7 @@ class TestSoundEndpoints: async def test_create_extraction_unauthenticated(self, client: AsyncClient) -> None: """Test extraction creation without authentication.""" response = await client.post( - "/api/v1/sounds/extract", + "/api/v1/extractions/", params={"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"}, ) @@ -83,7 +83,7 @@ class TestSoundEndpoints: mock_create.side_effect = ValueError("Invalid URL") response = await authenticated_client.post( - "/api/v1/sounds/extract", + "/api/v1/extractions/", params={"url": "invalid-url"}, ) @@ -114,7 +114,7 @@ class TestSoundEndpoints: ) as mock_get: mock_get.return_value = mock_extraction_info - response = await authenticated_client.get("/api/v1/sounds/extract/1") + response = await authenticated_client.get("/api/v1/extractions/1") assert response.status_code == 200 data = response.json() @@ -135,7 +135,7 @@ class TestSoundEndpoints: ) as mock_get: mock_get.return_value = None - response = await authenticated_client.get("/api/v1/sounds/extract/999") + response = await authenticated_client.get("/api/v1/extractions/999") assert response.status_code == 404 data = response.json() @@ -176,7 +176,7 @@ class TestSoundEndpoints: ) as mock_get: mock_get.return_value = mock_extractions - response = await authenticated_client.get("/api/v1/sounds/extract") + response = await authenticated_client.get("/api/v1/extractions/") assert response.status_code == 200 data = response.json()