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:
parent
46ca4c9aac
commit
246782292f
@ -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)}",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user