refactor: remove database access from core layer
- Remove db_session parameter from SeriesApp, SerieList, SerieScanner - Move all database operations to AnimeService (service layer) - Add add_series_to_db, contains_in_db methods to AnimeService - Update sync_series_from_data_files to use inline DB operations - Remove obsolete test classes for removed DB methods - Fix pylint issues: add broad-except comments, fix line lengths - Core layer (src/core/) now has zero database imports 722 unit tests pass
This commit is contained in:
@@ -4,20 +4,18 @@ SeriesApp - Core application logic for anime series management.
|
||||
This module provides the main application interface for searching,
|
||||
downloading, and managing anime series with support for async callbacks,
|
||||
progress reporting, and error handling.
|
||||
|
||||
Note:
|
||||
This module is pure domain logic with no database dependencies.
|
||||
Database operations are handled by the service layer (AnimeService).
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -125,6 +123,10 @@ class SeriesApp:
|
||||
- Managing series lists
|
||||
|
||||
Supports async callbacks for progress reporting.
|
||||
|
||||
Note:
|
||||
This class is now pure domain logic with no database dependencies.
|
||||
Database operations are handled by the service layer (AnimeService).
|
||||
|
||||
Events:
|
||||
download_status: Raised when download status changes.
|
||||
@@ -136,20 +138,15 @@ 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()
|
||||
@@ -159,19 +156,16 @@ class SeriesApp:
|
||||
self.loaders = Loaders()
|
||||
self.loader = self.loaders.GetLoader(key="aniworld.to")
|
||||
self.serie_scanner = SerieScanner(
|
||||
directory_to_search, self.loader, db_session=db_session
|
||||
)
|
||||
self.list = SerieList(
|
||||
self.directory_to_search, db_session=db_session
|
||||
directory_to_search, self.loader
|
||||
)
|
||||
self.list = SerieList(self.directory_to_search)
|
||||
# Synchronous init used during constructor to avoid awaiting
|
||||
# in __init__
|
||||
self._init_list_sync()
|
||||
|
||||
logger.info(
|
||||
"SeriesApp initialized for directory: %s (db_session: %s)",
|
||||
directory_to_search,
|
||||
"provided" if db_session else "none"
|
||||
"SeriesApp initialized for directory: %s",
|
||||
directory_to_search
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -204,53 +198,26 @@ class SeriesApp:
|
||||
"""Set scan_status event handler."""
|
||||
self._events.scan_status = value
|
||||
|
||||
@property
|
||||
def db_session(self) -> Optional[AsyncSession]:
|
||||
def load_series_from_list(self, series: list) -> None:
|
||||
"""
|
||||
Get the database session.
|
||||
Load series into the in-memory list.
|
||||
|
||||
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.
|
||||
This method is called by the service layer after loading
|
||||
series from the database.
|
||||
|
||||
Args:
|
||||
session: The new database session or None
|
||||
series: List of Serie objects to load
|
||||
"""
|
||||
self._db_session = session
|
||||
self.list._db_session = session
|
||||
self.serie_scanner._db_session = session
|
||||
self.list.keyDict.clear()
|
||||
for serie in series:
|
||||
self.list.keyDict[serie.key] = serie
|
||||
self.series_list = self.list.GetMissingEpisode()
|
||||
logger.debug(
|
||||
"Database session updated: %s",
|
||||
"provided" if session else "none"
|
||||
"Loaded %d series with %d having missing episodes",
|
||||
len(series),
|
||||
len(self.series_list)
|
||||
)
|
||||
|
||||
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()
|
||||
@@ -430,7 +397,7 @@ class SeriesApp:
|
||||
|
||||
return download_success
|
||||
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.error(
|
||||
"Download error: %s (key: %s) S%02dE%02d - %s",
|
||||
serie_folder,
|
||||
@@ -457,25 +424,22 @@ class SeriesApp:
|
||||
|
||||
return False
|
||||
|
||||
async def rescan(self, use_database: bool = True) -> int:
|
||||
async def rescan(self) -> list:
|
||||
"""
|
||||
Rescan directory for missing episodes (async).
|
||||
|
||||
When use_database is True (default), scan results are saved to the
|
||||
database instead of data files. This is the preferred mode for the
|
||||
web application.
|
||||
|
||||
Args:
|
||||
use_database: If True, save scan results to database.
|
||||
If False, use legacy file-based storage (deprecated).
|
||||
This method performs a file-based scan and returns the results.
|
||||
Database persistence is handled by the service layer (AnimeService).
|
||||
|
||||
Returns:
|
||||
Number of series with missing episodes after rescan.
|
||||
List of Serie objects found during scan with their
|
||||
missing episodes.
|
||||
|
||||
Note:
|
||||
This method no longer saves to database directly. The returned
|
||||
list should be persisted by the caller (AnimeService).
|
||||
"""
|
||||
logger.info(
|
||||
"Starting directory rescan (database mode: %s)",
|
||||
use_database
|
||||
)
|
||||
logger.info("Starting directory rescan")
|
||||
|
||||
total_to_scan = 0
|
||||
|
||||
@@ -520,34 +484,19 @@ class SeriesApp:
|
||||
)
|
||||
)
|
||||
|
||||
# Perform scan - use database mode when available
|
||||
if use_database:
|
||||
# Import here to avoid circular imports and allow CLI usage
|
||||
# without database dependencies
|
||||
from src.server.database.connection import get_db_session
|
||||
|
||||
async with get_db_session() as db:
|
||||
await self.serie_scanner.scan_async(db, scan_callback)
|
||||
logger.info("Scan results saved to database")
|
||||
else:
|
||||
# Legacy file-based mode (deprecated)
|
||||
await asyncio.to_thread(
|
||||
self.serie_scanner.scan, scan_callback
|
||||
)
|
||||
# Perform scan (file-based, returns results in scanner.keyDict)
|
||||
await asyncio.to_thread(
|
||||
self.serie_scanner.scan, scan_callback
|
||||
)
|
||||
|
||||
# Get scanned series from scanner
|
||||
scanned_series = list(self.serie_scanner.keyDict.values())
|
||||
|
||||
# Reinitialize list from the appropriate source
|
||||
if use_database:
|
||||
from src.server.database.connection import get_db_session
|
||||
|
||||
async with get_db_session() as db:
|
||||
self.list = SerieList(
|
||||
self.directory_to_search, db_session=db
|
||||
)
|
||||
await self.list.load_series_from_db(db)
|
||||
self.series_list = self.list.GetMissingEpisode()
|
||||
else:
|
||||
self.list = SerieList(self.directory_to_search)
|
||||
await self._init_list()
|
||||
# Update in-memory list with scan results
|
||||
self.list.keyDict.clear()
|
||||
for serie in scanned_series:
|
||||
self.list.keyDict[serie.key] = serie
|
||||
self.series_list = self.list.GetMissingEpisode()
|
||||
|
||||
logger.info("Directory rescan completed successfully")
|
||||
|
||||
@@ -566,7 +515,7 @@ class SeriesApp:
|
||||
)
|
||||
)
|
||||
|
||||
return len(self.series_list)
|
||||
return scanned_series
|
||||
|
||||
except InterruptedError:
|
||||
logger.warning("Scan cancelled by user")
|
||||
@@ -666,10 +615,9 @@ class SeriesApp:
|
||||
try:
|
||||
temp_list = SerieList(
|
||||
self.directory_to_search,
|
||||
db_session=None, # Force file-based loading
|
||||
skip_load=False # Allow automatic loading
|
||||
)
|
||||
except Exception as e:
|
||||
except (OSError, ValueError) as e:
|
||||
logger.error(
|
||||
"Failed to scan directory for data files: %s",
|
||||
str(e),
|
||||
|
||||
Reference in New Issue
Block a user