- Move src/core/ → src/server/ - Split SerieList.py (531 lines) and series.py (414 lines) into src/server/database/ - Add database/models.py for SQLAlchemy models - Update all test imports to reflect new structure - Remove deprecated test files (test_serie_class.py, test_serie_folder_with_year.py)
289 lines
9.5 KiB
Python
289 lines
9.5 KiB
Python
"""Utilities for loading and managing stored anime series metadata.
|
|
|
|
This module provides the SerieList class for managing collections of anime
|
|
series metadata loaded from the database.
|
|
|
|
Note:
|
|
This module is part of the server database layer. All persistence
|
|
is handled by the service layer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Dict, List, Optional
|
|
|
|
from src.server.database.models import AnimeSeries
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SerieList:
|
|
"""
|
|
Represents the collection of cached series loaded from database.
|
|
|
|
Series are identified by their unique 'key' (provider identifier).
|
|
The 'folder' is metadata only and not used for lookups.
|
|
|
|
This class manages in-memory series data loaded from database.
|
|
|
|
Example:
|
|
# Load from database
|
|
serie_list = SerieList("/path/to/anime")
|
|
await serie_list.load_all_from_db()
|
|
series = serie_list.get_all()
|
|
|
|
Attributes:
|
|
directory: Path to the anime directory
|
|
keyDict: Internal dictionary mapping serie.key to AnimeSeries objects
|
|
"""
|
|
|
|
def __init__(self, base_path: str) -> None:
|
|
"""Initialize the SerieList.
|
|
|
|
Args:
|
|
base_path: Path to the anime directory
|
|
"""
|
|
self.directory: str = base_path
|
|
# Internal storage using serie.key as the dictionary key
|
|
self.keyDict: Dict[str, AnimeSeries] = {}
|
|
|
|
async def add_to_db(self, anime: AnimeSeries) -> bool:
|
|
"""Persist a new series to the database.
|
|
|
|
Creates the filesystem folder using anime.folder, then persists
|
|
the series metadata to the database.
|
|
|
|
Args:
|
|
anime: The AnimeSeries instance to add
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
try:
|
|
from src.server.database.connection import get_async_session_factory
|
|
from src.server.database.service import AnimeSeriesService, EpisodeService
|
|
|
|
folder_name = anime.folder
|
|
anime_path = self.directory + "/" + folder_name
|
|
import os
|
|
os.makedirs(anime_path, exist_ok=True)
|
|
|
|
session_factory = get_async_session_factory()
|
|
db = session_factory()
|
|
try:
|
|
existing = await AnimeSeriesService.get_by_key(db, anime.key)
|
|
if existing:
|
|
logger.debug(
|
|
"Series '%s' (key=%s) already exists in DB, skipping",
|
|
anime.name, anime.key
|
|
)
|
|
return True
|
|
|
|
db_anime_series = await AnimeSeriesService.create(
|
|
db=db,
|
|
key=anime.key,
|
|
name=anime.name,
|
|
site=anime.site,
|
|
folder=folder_name,
|
|
year=anime.year
|
|
)
|
|
for ep in anime.episodes:
|
|
await EpisodeService.create(
|
|
db=db,
|
|
series_id=db_anime_series.id,
|
|
season=ep.season,
|
|
episode_number=ep.episode_number
|
|
)
|
|
await db.commit()
|
|
self.keyDict[anime.key] = anime
|
|
logger.info(
|
|
"Persisted series '%s' to database",
|
|
anime.name
|
|
)
|
|
return True
|
|
except Exception as e:
|
|
await db.rollback()
|
|
logger.error(
|
|
"Failed to persist series '%s' to DB: %s",
|
|
anime.key, e, exc_info=True
|
|
)
|
|
return False
|
|
finally:
|
|
await db.close()
|
|
except Exception as e:
|
|
logger.error(
|
|
"Could not add series '%s' to DB (DB unavailable?): %s",
|
|
anime.key, e
|
|
)
|
|
return False
|
|
|
|
def contains(self, key: str) -> bool:
|
|
"""
|
|
Return True when a series identified by ``key`` already exists.
|
|
|
|
Args:
|
|
key: The unique provider identifier for the series
|
|
|
|
Returns:
|
|
True if the series exists in the collection
|
|
"""
|
|
return key in self.keyDict
|
|
|
|
def GetMissingEpisode(self) -> List[AnimeSeries]:
|
|
"""Return all series that still contain missing episodes."""
|
|
return [
|
|
anime for anime in self.keyDict.values()
|
|
if anime.episodeDict
|
|
]
|
|
|
|
def get_missing_episodes(self) -> List[AnimeSeries]:
|
|
"""PEP8-friendly alias for :meth:`GetMissingEpisode`."""
|
|
return self.GetMissingEpisode()
|
|
|
|
def GetList(self) -> List[AnimeSeries]:
|
|
"""Return all series instances stored in the list."""
|
|
return list(self.keyDict.values())
|
|
|
|
def get_all(self) -> List[AnimeSeries]:
|
|
"""PEP8-friendly alias for :meth:`GetList`."""
|
|
return self.GetList()
|
|
|
|
def get_by_key(self, key: str) -> Optional[AnimeSeries]:
|
|
"""
|
|
Get a series by its unique provider key.
|
|
|
|
This is the primary method for series lookup.
|
|
|
|
Args:
|
|
key: The unique provider identifier (e.g., "attack-on-titan")
|
|
|
|
Returns:
|
|
The AnimeSeries instance if found, None otherwise
|
|
"""
|
|
return self.keyDict.get(key)
|
|
|
|
def get_by_folder(self, folder: str) -> Optional[AnimeSeries]:
|
|
"""
|
|
Get a series by its folder name.
|
|
|
|
.. deprecated:: 2.0.0
|
|
Use :meth:`get_by_key` instead. Folder-based lookups will be
|
|
removed in version 3.0.0. The `folder` field is metadata only
|
|
and should not be used for identification.
|
|
|
|
This method is provided for backward compatibility only.
|
|
Prefer using get_by_key() for new code.
|
|
|
|
Args:
|
|
folder: The filesystem folder name (e.g., "Attack on Titan (2013)")
|
|
|
|
Returns:
|
|
The AnimeSeries instance if found, None otherwise
|
|
"""
|
|
import warnings
|
|
warnings.warn(
|
|
"get_by_folder() is deprecated and will be removed in v3.0.0. "
|
|
"Use get_by_key() instead. The 'folder' field is metadata only.",
|
|
DeprecationWarning,
|
|
stacklevel=2
|
|
)
|
|
for anime in self.keyDict.values():
|
|
if anime.folder == folder:
|
|
return anime
|
|
return None
|
|
|
|
async def load_all_from_db(self) -> int:
|
|
"""Load all series from database into in-memory cache.
|
|
|
|
Retrieves all anime series from the database with their episodes
|
|
and populates the in-memory keyDict for fast access.
|
|
|
|
Returns:
|
|
int: Number of series loaded into cache
|
|
"""
|
|
from src.server.database.connection import get_async_session_factory
|
|
from src.server.database.service import AnimeSeriesService
|
|
|
|
try:
|
|
session_factory = get_async_session_factory()
|
|
db = session_factory()
|
|
try:
|
|
anime_series_list = await AnimeSeriesService.get_all(
|
|
db, with_episodes=True
|
|
)
|
|
|
|
count = 0
|
|
for anime_series in anime_series_list:
|
|
self.keyDict[anime_series.key] = anime_series
|
|
count += 1
|
|
|
|
logger.info(
|
|
"Loaded %d series from database into in-memory cache",
|
|
count
|
|
)
|
|
return count
|
|
finally:
|
|
await db.close()
|
|
except RuntimeError:
|
|
logger.warning(
|
|
"Database not available, skipping DB load"
|
|
)
|
|
return 0
|
|
|
|
async def _load_single_series_from_db(
|
|
self,
|
|
anime_folder: str
|
|
) -> Optional[AnimeSeries]:
|
|
"""Load a single series from database by folder name.
|
|
|
|
Looks up a series in the database by its folder name and adds
|
|
it to the in-memory cache.
|
|
|
|
Args:
|
|
anime_folder: The filesystem folder name to look up
|
|
|
|
Returns:
|
|
AnimeSeries if found and loaded, None otherwise
|
|
"""
|
|
from src.server.database.connection import get_async_session_factory
|
|
from src.server.database.service import AnimeSeriesService
|
|
|
|
try:
|
|
session_factory = get_async_session_factory()
|
|
db = session_factory()
|
|
try:
|
|
anime_series = await AnimeSeriesService.get_by_folder(
|
|
db, anime_folder
|
|
)
|
|
if not anime_series:
|
|
logger.debug(
|
|
"Series with folder '%s' not found in DB",
|
|
anime_folder
|
|
)
|
|
return None
|
|
|
|
self.keyDict[anime_series.key] = anime_series
|
|
logger.debug(
|
|
"Loaded series '%s' (key=%s) from DB",
|
|
anime_series.name, anime_series.key
|
|
)
|
|
return anime_series
|
|
finally:
|
|
await db.close()
|
|
except RuntimeError:
|
|
logger.warning(
|
|
"Database not available, cannot load series '%s'",
|
|
anime_folder
|
|
)
|
|
return None
|
|
|
|
def invalidate_cache(self) -> None:
|
|
"""Clear the in-memory cache.
|
|
|
|
Use after database modifications to force reload from DB
|
|
on next access.
|
|
"""
|
|
self.keyDict.clear()
|
|
logger.debug("SerieList in-memory cache invalidated")
|