feat(core): Add database support to SeriesApp (Task 7)

- Added db_session parameter to SeriesApp.__init__()
- Added db_session property and set_db_session() method
- Added init_from_db_async() for async database initialization
- Pass db_session to SerieList and SerieScanner during construction
- Added get_series_app_with_db() dependency for FastAPI endpoints
- All 815 unit tests and 55 API tests pass
This commit is contained in:
2025-12-01 19:42:04 +01:00
parent 246782292f
commit cb014cf547
6 changed files with 291 additions and 330 deletions

View File

@@ -8,10 +8,16 @@ progress reporting, and error handling.
import asyncio
import logging
import warnings
from typing import Any, Dict, List, Optional
from events import Events
try:
from sqlalchemy.ext.asyncio import AsyncSession
except ImportError: # pragma: no cover - optional dependency
AsyncSession = object # type: ignore
from src.core.entities.SerieList import SerieList
from src.core.entities.series import Serie
from src.core.providers.provider_factory import Loaders
@@ -130,15 +136,20 @@ class SeriesApp:
def __init__(
self,
directory_to_search: str,
db_session: Optional[AsyncSession] = None,
):
"""
Initialize SeriesApp.
Args:
directory_to_search: Base directory for anime series
db_session: Optional database session for database-backed
storage. When provided, SerieList and SerieScanner will
use the database instead of file-based storage.
"""
self.directory_to_search = directory_to_search
self._db_session = db_session
# Initialize events
self._events = Events()
@@ -147,15 +158,20 @@ class SeriesApp:
self.loaders = Loaders()
self.loader = self.loaders.GetLoader(key="aniworld.to")
self.serie_scanner = SerieScanner(directory_to_search, self.loader)
self.list = SerieList(self.directory_to_search)
self.serie_scanner = SerieScanner(
directory_to_search, self.loader, db_session=db_session
)
self.list = SerieList(
self.directory_to_search, db_session=db_session
)
# Synchronous init used during constructor to avoid awaiting
# in __init__
self._init_list_sync()
logger.info(
"SeriesApp initialized for directory: %s",
directory_to_search
"SeriesApp initialized for directory: %s (db_session: %s)",
directory_to_search,
"provided" if db_session else "none"
)
@property
@@ -188,6 +204,53 @@ class SeriesApp:
"""Set scan_status event handler."""
self._events.scan_status = value
@property
def db_session(self) -> Optional[AsyncSession]:
"""
Get the database session.
Returns:
AsyncSession or None: The database session if configured
"""
return self._db_session
def set_db_session(self, session: Optional[AsyncSession]) -> None:
"""
Update the database session.
Also updates the db_session on SerieList and SerieScanner.
Args:
session: The new database session or None
"""
self._db_session = session
self.list._db_session = session
self.serie_scanner._db_session = session
logger.debug(
"Database session updated: %s",
"provided" if session else "none"
)
async def init_from_db_async(self) -> None:
"""
Initialize series list from database (async).
This should be called when using database storage instead of
the synchronous file-based initialization.
"""
if self._db_session:
await self.list.load_series_from_db(self._db_session)
self.series_list = self.list.GetMissingEpisode()
logger.debug(
"Loaded %d series with missing episodes from database",
len(self.series_list)
)
else:
warnings.warn(
"init_from_db_async called without db_session configured",
UserWarning
)
def _init_list_sync(self) -> None:
"""Synchronous initialization helper for constructor."""
self.series_list = self.list.GetMissingEpisode()

View File

@@ -65,6 +65,10 @@ def get_series_app() -> SeriesApp:
Raises:
HTTPException: If SeriesApp is not initialized or anime directory
is not configured
Note:
This creates a SeriesApp without database support. For database-
backed storage, use get_series_app_with_db() instead.
"""
global _series_app
@@ -103,7 +107,6 @@ def reset_series_app() -> None:
_series_app = None
async def get_database_session() -> AsyncGenerator:
"""
Dependency to get database session.
@@ -166,6 +169,43 @@ async def get_optional_database_session() -> AsyncGenerator:
yield None
async def get_series_app_with_db(
db: AsyncSession = Depends(get_optional_database_session),
) -> SeriesApp:
"""
Dependency to get SeriesApp instance with database support.
This creates or returns a SeriesApp instance and injects the
database session for database-backed storage.
Args:
db: Optional database session from dependency injection
Returns:
SeriesApp: The main application instance with database support
Raises:
HTTPException: If SeriesApp is not initialized or anime directory
is not configured
Example:
@app.post("/api/anime/scan")
async def scan_anime(
series_app: SeriesApp = Depends(get_series_app_with_db)
):
# series_app has db_session configured
await series_app.serie_scanner.scan_async()
"""
# Get the base SeriesApp
app = get_series_app()
# Inject database session if available
if db:
app.set_db_session(db)
return app
def get_current_user(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(
http_bearer_security