feat(api): Update anime API endpoints to use database storage

Task 6: Update Anime API endpoints to use database
- Modified add_series endpoint to save series to database when available
- Added get_optional_database_session dependency for graceful fallback
- Falls back to file-based storage when database unavailable
- All 55 API tests and 809 unit tests pass
This commit is contained in:
Lukas 2025-12-01 19:34:41 +01:00
parent 46ca4c9aac
commit 246782292f
2 changed files with 93 additions and 27 deletions

View File

@ -4,11 +4,14 @@ from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.entities.series import Serie from src.core.entities.series import Serie
from src.server.database.service import AnimeSeriesService
from src.server.services.anime_service import AnimeService, AnimeServiceError from src.server.services.anime_service import AnimeService, AnimeServiceError
from src.server.utils.dependencies import ( from src.server.utils.dependencies import (
get_anime_service, get_anime_service,
get_optional_database_session,
get_series_app, get_series_app,
require_auth, require_auth,
) )
@ -582,6 +585,7 @@ async def add_series(
request: AddSeriesRequest, request: AddSeriesRequest,
_auth: dict = Depends(require_auth), _auth: dict = Depends(require_auth),
series_app: Any = Depends(get_series_app), series_app: Any = Depends(get_series_app),
db: Optional[AsyncSession] = Depends(get_optional_database_session),
) -> dict: ) -> dict:
"""Add a new series to the library. """Add a new series to the library.
@ -589,6 +593,9 @@ async def add_series(
The `key` is the URL-safe identifier used for all lookups. The `key` is the URL-safe identifier used for all lookups.
The `name` is stored as display metadata along with a The `name` is stored as display metadata along with a
filesystem-friendly `folder` name derived from the name. filesystem-friendly `folder` name derived from the name.
Series are saved to the database using AnimeSeriesService when
database is available, falling back to in-memory storage otherwise.
Args: Args:
request: Request containing the series link and name. request: Request containing the series link and name.
@ -596,9 +603,10 @@ async def add_series(
- name: Display name for the series - name: Display name for the series
_auth: Ensures the caller is authenticated (value unused) _auth: Ensures the caller is authenticated (value unused)
series_app: Core `SeriesApp` instance provided via dependency series_app: Core `SeriesApp` instance provided via dependency
db: Optional database session for async operations
Returns: Returns:
Dict[str, Any]: Status payload with success message and key Dict[str, Any]: Status payload with success message, key, and db_id
Raises: Raises:
HTTPException: If adding the series fails or link is invalid HTTPException: If adding the series fails or link is invalid
@ -617,13 +625,6 @@ async def add_series(
detail="Series name cannot be empty", detail="Series name cannot be empty",
) )
# Check if series_app has the list attribute
if not hasattr(series_app, "list"):
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Series list functionality not available",
)
# Extract key from link URL # Extract key from link URL
# Expected format: https://aniworld.to/anime/stream/{key} # Expected format: https://aniworld.to/anime/stream/{key}
link = request.link.strip() link = request.link.strip()
@ -646,36 +647,69 @@ async def add_series(
# Create folder from name (filesystem-friendly) # Create folder from name (filesystem-friendly)
folder = request.name.strip() folder = request.name.strip()
db_id = None
# Create a new Serie object # Try to save to database if available
# key: unique identifier extracted from link if db is not None:
# name: display name from request # Check if series already exists in database
# folder: filesystem folder name (derived from name) existing = await AnimeSeriesService.get_by_key(db, key)
# episodeDict: empty for new series if existing:
serie = Serie( return {
key=key, "status": "exists",
name=request.name.strip(), "message": f"Series already exists: {request.name}",
site="aniworld.to", "key": key,
folder=folder, "folder": existing.folder,
episodeDict={} "db_id": existing.id
) }
# Save to database using AnimeSeriesService
anime_series = await AnimeSeriesService.create(
db=db,
key=key,
name=request.name.strip(),
site="aniworld.to",
folder=folder,
episode_dict={}, # Empty for new series
)
db_id = anime_series.id
logger.info(
"Added series to database: %s (key=%s, db_id=%d)",
request.name,
key,
db_id
)
# Add the series to the list # Also add to in-memory cache if series_app has the list attribute
series_app.list.add(serie) if series_app and hasattr(series_app, "list"):
serie = Serie(
# Refresh the series list to update the cache key=key,
if hasattr(series_app, "refresh_series_list"): name=request.name.strip(),
series_app.refresh_series_list() site="aniworld.to",
folder=folder,
episodeDict={}
)
# Add to in-memory cache
if hasattr(series_app.list, 'keyDict'):
# Direct update without file saving
series_app.list.keyDict[key] = serie
elif hasattr(series_app.list, 'add'):
# Legacy: use add method (may create file with deprecation warning)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
series_app.list.add(serie)
return { return {
"status": "success", "status": "success",
"message": f"Successfully added series: {request.name}", "message": f"Successfully added series: {request.name}",
"key": key, "key": key,
"folder": folder "folder": folder,
"db_id": db_id
} }
except HTTPException: except HTTPException:
raise raise
except Exception as exc: except Exception as exc:
logger.error("Failed to add series: %s", exc, exc_info=True)
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to add series: {str(exc)}", detail=f"Failed to add series: {str(exc)}",

View File

@ -134,6 +134,38 @@ async def get_database_session() -> AsyncGenerator:
) )
async def get_optional_database_session() -> AsyncGenerator:
"""
Dependency to get optional database session.
Unlike get_database_session(), this returns None if the database
is not available, allowing endpoints to fall back to other storage.
Yields:
AsyncSession or None: Database session if available, None otherwise
Example:
@app.post("/anime/add")
async def add_anime(
db: Optional[AsyncSession] = Depends(get_optional_database_session)
):
if db:
# Use database
await AnimeSeriesService.create(db, ...)
else:
# Fall back to file-based storage
series_app.list.add(serie)
"""
try:
from src.server.database import get_db_session
async with get_db_session() as session:
yield session
except (ImportError, RuntimeError):
# Database not available - yield None
yield None
def get_current_user( def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends( credentials: Optional[HTTPAuthorizationCredentials] = Depends(
http_bearer_security http_bearer_security