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:
@@ -1,14 +1,11 @@
|
||||
"""Utilities for loading and managing stored anime series metadata.
|
||||
|
||||
This module provides the SerieList class for managing collections of anime
|
||||
series metadata. It supports both file-based and database-backed storage.
|
||||
series metadata. It uses file-based storage only.
|
||||
|
||||
The class can operate in two modes:
|
||||
1. File-based mode (legacy): Reads/writes data files from disk
|
||||
2. Database mode: Reads/writes to SQLite database via AnimeSeriesService
|
||||
|
||||
Database mode is preferred for new code. File-based mode is kept for
|
||||
backward compatibility with CLI usage.
|
||||
Note:
|
||||
This module is part of the core domain layer and has no database
|
||||
dependencies. All database operations are handled by the service layer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -17,74 +14,52 @@ import logging
|
||||
import os
|
||||
import warnings
|
||||
from json import JSONDecodeError
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
|
||||
from src.core.entities.series import Serie
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.server.database.models import AnimeSeries
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SerieList:
|
||||
"""
|
||||
Represents the collection of cached series stored on disk or database.
|
||||
Represents the collection of cached series stored on disk.
|
||||
|
||||
Series are identified by their unique 'key' (provider identifier).
|
||||
The 'folder' is metadata only and not used for lookups.
|
||||
|
||||
The class supports two modes of operation:
|
||||
|
||||
1. File-based mode (legacy):
|
||||
Initialize without db_session to use file-based storage.
|
||||
Series are loaded from 'data' files in the anime directory.
|
||||
|
||||
2. Database mode (preferred):
|
||||
Pass db_session to use database-backed storage via AnimeSeriesService.
|
||||
Series are loaded from the AnimeSeries table.
|
||||
This class manages in-memory series data loaded from filesystem.
|
||||
It has no database dependencies - all persistence is handled by
|
||||
the service layer.
|
||||
|
||||
Example:
|
||||
# File-based mode (legacy)
|
||||
# File-based mode
|
||||
serie_list = SerieList("/path/to/anime")
|
||||
|
||||
# Database mode (preferred)
|
||||
async with get_db_session() as db:
|
||||
serie_list = SerieList("/path/to/anime", db_session=db)
|
||||
await serie_list.load_series_from_db()
|
||||
series = serie_list.get_all()
|
||||
|
||||
Attributes:
|
||||
directory: Path to the anime directory
|
||||
keyDict: Internal dictionary mapping serie.key to Serie objects
|
||||
_db_session: Optional database session for database mode
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_path: str,
|
||||
db_session: Optional["AsyncSession"] = None,
|
||||
skip_load: bool = False
|
||||
) -> None:
|
||||
"""Initialize the SerieList.
|
||||
|
||||
Args:
|
||||
base_path: Path to the anime directory
|
||||
db_session: Optional database session for database mode.
|
||||
If provided, use load_series_from_db() instead of
|
||||
the automatic file-based loading.
|
||||
skip_load: If True, skip automatic loading of series.
|
||||
Useful when using database mode to allow async loading.
|
||||
skip_load: If True, skip automatic loading of series from files.
|
||||
Useful when planning to load from database instead.
|
||||
"""
|
||||
self.directory: str = base_path
|
||||
# Internal storage using serie.key as the dictionary key
|
||||
self.keyDict: Dict[str, Serie] = {}
|
||||
self._db_session: Optional["AsyncSession"] = db_session
|
||||
|
||||
# Only auto-load from files if no db_session and not skipping
|
||||
if not skip_load and db_session is None:
|
||||
# Only auto-load from files if not skipping
|
||||
if not skip_load:
|
||||
self.load_series()
|
||||
|
||||
def add(self, serie: Serie) -> None:
|
||||
@@ -94,10 +69,6 @@ class SerieList:
|
||||
Uses serie.key for identification. The serie.folder is used for
|
||||
filesystem operations only.
|
||||
|
||||
.. deprecated:: 2.0.0
|
||||
Use :meth:`add_to_db` for database-backed storage.
|
||||
File-based storage will be removed in a future version.
|
||||
|
||||
Args:
|
||||
serie: The Serie instance to add
|
||||
|
||||
@@ -108,13 +79,6 @@ class SerieList:
|
||||
if self.contains(serie.key):
|
||||
return
|
||||
|
||||
warnings.warn(
|
||||
"File-based storage via add() is deprecated. "
|
||||
"Use add_to_db() for database storage.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
|
||||
data_path = os.path.join(self.directory, serie.folder, "data")
|
||||
anime_path = os.path.join(self.directory, serie.folder)
|
||||
os.makedirs(anime_path, exist_ok=True)
|
||||
@@ -123,73 +87,6 @@ class SerieList:
|
||||
# Store by key, not folder
|
||||
self.keyDict[serie.key] = serie
|
||||
|
||||
async def add_to_db(
|
||||
self,
|
||||
serie: Serie,
|
||||
db: "AsyncSession"
|
||||
) -> Optional["AnimeSeries"]:
|
||||
"""
|
||||
Add a series to the database.
|
||||
|
||||
Uses serie.key for identification. Creates a new AnimeSeries
|
||||
record in the database if it doesn't already exist.
|
||||
|
||||
Args:
|
||||
serie: The Serie instance to add
|
||||
db: Database session for async operations
|
||||
|
||||
Returns:
|
||||
Created AnimeSeries instance, or None if already exists
|
||||
|
||||
Example:
|
||||
async with get_db_session() as db:
|
||||
result = await serie_list.add_to_db(serie, db)
|
||||
if result:
|
||||
print(f"Added series: {result.name}")
|
||||
"""
|
||||
from src.server.database.service import AnimeSeriesService, EpisodeService
|
||||
|
||||
# Check if series already exists in DB
|
||||
existing = await AnimeSeriesService.get_by_key(db, serie.key)
|
||||
if existing:
|
||||
logger.debug(
|
||||
"Series already exists in database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
return None
|
||||
|
||||
# Create new series in database
|
||||
anime_series = await AnimeSeriesService.create(
|
||||
db=db,
|
||||
key=serie.key,
|
||||
name=serie.name,
|
||||
site=serie.site,
|
||||
folder=serie.folder,
|
||||
)
|
||||
|
||||
# Create Episode records for each episode in episodeDict
|
||||
if serie.episodeDict:
|
||||
for season, episode_numbers in serie.episodeDict.items():
|
||||
for episode_number in episode_numbers:
|
||||
await EpisodeService.create(
|
||||
db=db,
|
||||
series_id=anime_series.id,
|
||||
season=season,
|
||||
episode_number=episode_number,
|
||||
)
|
||||
|
||||
# Also add to in-memory collection
|
||||
self.keyDict[serie.key] = serie
|
||||
|
||||
logger.info(
|
||||
"Added series to database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
|
||||
return anime_series
|
||||
|
||||
def contains(self, key: str) -> bool:
|
||||
"""
|
||||
Return True when a series identified by ``key`` already exists.
|
||||
@@ -253,112 +150,6 @@ class SerieList:
|
||||
error,
|
||||
)
|
||||
|
||||
async def load_series_from_db(self, db: "AsyncSession") -> int:
|
||||
"""
|
||||
Load all series from the database into the in-memory collection.
|
||||
|
||||
This is the preferred method for populating the series list
|
||||
when using database-backed storage.
|
||||
|
||||
Args:
|
||||
db: Database session for async operations
|
||||
|
||||
Returns:
|
||||
Number of series loaded from the database
|
||||
|
||||
Example:
|
||||
async with get_db_session() as db:
|
||||
serie_list = SerieList("/path/to/anime", skip_load=True)
|
||||
count = await serie_list.load_series_from_db(db)
|
||||
print(f"Loaded {count} series from database")
|
||||
"""
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
# Clear existing in-memory data
|
||||
self.keyDict.clear()
|
||||
|
||||
# Load all series from database (with episodes for episodeDict)
|
||||
anime_series_list = await AnimeSeriesService.get_all(
|
||||
db, with_episodes=True
|
||||
)
|
||||
|
||||
for anime_series in anime_series_list:
|
||||
serie = self._convert_from_db(anime_series)
|
||||
self.keyDict[serie.key] = serie
|
||||
|
||||
logger.info(
|
||||
"Loaded %d series from database",
|
||||
len(self.keyDict)
|
||||
)
|
||||
|
||||
return len(self.keyDict)
|
||||
|
||||
@staticmethod
|
||||
def _convert_from_db(anime_series: "AnimeSeries") -> Serie:
|
||||
"""
|
||||
Convert an AnimeSeries database model to a Serie entity.
|
||||
|
||||
Args:
|
||||
anime_series: AnimeSeries model from database
|
||||
(must have episodes relationship loaded)
|
||||
|
||||
Returns:
|
||||
Serie entity instance
|
||||
"""
|
||||
# Build episode_dict from episodes relationship
|
||||
episode_dict: dict[int, list[int]] = {}
|
||||
if anime_series.episodes:
|
||||
for episode in anime_series.episodes:
|
||||
season = episode.season
|
||||
if season not in episode_dict:
|
||||
episode_dict[season] = []
|
||||
episode_dict[season].append(episode.episode_number)
|
||||
# Sort episode numbers within each season
|
||||
for season in episode_dict:
|
||||
episode_dict[season].sort()
|
||||
|
||||
return Serie(
|
||||
key=anime_series.key,
|
||||
name=anime_series.name,
|
||||
site=anime_series.site,
|
||||
folder=anime_series.folder,
|
||||
episodeDict=episode_dict
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _convert_to_db_dict(serie: Serie) -> dict:
|
||||
"""
|
||||
Convert a Serie entity to a dictionary for database creation.
|
||||
|
||||
Args:
|
||||
serie: Serie entity instance
|
||||
|
||||
Returns:
|
||||
Dictionary suitable for AnimeSeriesService.create()
|
||||
"""
|
||||
return {
|
||||
"key": serie.key,
|
||||
"name": serie.name,
|
||||
"site": serie.site,
|
||||
"folder": serie.folder,
|
||||
}
|
||||
|
||||
async def contains_in_db(self, key: str, db: "AsyncSession") -> bool:
|
||||
"""
|
||||
Check if a series with the given key exists in the database.
|
||||
|
||||
Args:
|
||||
key: The unique provider identifier for the series
|
||||
db: Database session for async operations
|
||||
|
||||
Returns:
|
||||
True if the series exists in the database
|
||||
"""
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
existing = await AnimeSeriesService.get_by_key(db, key)
|
||||
return existing is not None
|
||||
|
||||
def GetMissingEpisode(self) -> List[Serie]:
|
||||
"""Return all series that still contain missing episodes."""
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user