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 pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
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.utils.dependencies import (
get_anime_service,
get_optional_database_session,
get_series_app,
require_auth,
)
@ -582,6 +585,7 @@ async def add_series(
request: AddSeriesRequest,
_auth: dict = Depends(require_auth),
series_app: Any = Depends(get_series_app),
db: Optional[AsyncSession] = Depends(get_optional_database_session),
) -> dict:
"""Add a new series to the library.
@ -590,15 +594,19 @@ async def add_series(
The `name` is stored as display metadata along with a
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:
request: Request containing the series link and name.
- link: URL to the series (e.g., aniworld.to/anime/stream/key)
- name: Display name for the series
_auth: Ensures the caller is authenticated (value unused)
series_app: Core `SeriesApp` instance provided via dependency
db: Optional database session for async operations
Returns:
Dict[str, Any]: Status payload with success message and key
Dict[str, Any]: Status payload with success message, key, and db_id
Raises:
HTTPException: If adding the series fails or link is invalid
@ -617,13 +625,6 @@ async def add_series(
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
# Expected format: https://aniworld.to/anime/stream/{key}
link = request.link.strip()
@ -646,12 +647,41 @@ async def add_series(
# Create folder from name (filesystem-friendly)
folder = request.name.strip()
db_id = None
# Create a new Serie object
# key: unique identifier extracted from link
# name: display name from request
# folder: filesystem folder name (derived from name)
# episodeDict: empty for new series
# Try to save to database if available
if db is not None:
# Check if series already exists in database
existing = await AnimeSeriesService.get_by_key(db, key)
if existing:
return {
"status": "exists",
"message": f"Series already exists: {request.name}",
"key": key,
"folder": existing.folder,
"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
)
# Also add to in-memory cache if series_app has the list attribute
if series_app and hasattr(series_app, "list"):
serie = Serie(
key=key,
name=request.name.strip(),
@ -659,23 +689,27 @@ async def add_series(
folder=folder,
episodeDict={}
)
# Add the series to the list
# 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)
# Refresh the series list to update the cache
if hasattr(series_app, "refresh_series_list"):
series_app.refresh_series_list()
return {
"status": "success",
"message": f"Successfully added series: {request.name}",
"key": key,
"folder": folder
"folder": folder,
"db_id": db_id
}
except HTTPException:
raise
except Exception as exc:
logger.error("Failed to add series: %s", exc, exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
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(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(
http_bearer_security