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,12 +4,9 @@ SerieScanner - Scans directories for anime series and missing episodes.
|
||||
This module provides functionality to scan anime directories, identify
|
||||
missing episodes, and report progress through callback interfaces.
|
||||
|
||||
The scanner supports two modes of operation:
|
||||
1. File-based mode (legacy): Saves scan results to data files
|
||||
2. Database mode (preferred): Saves scan results to SQLite database
|
||||
|
||||
Database mode is preferred for new code. File-based mode is kept for
|
||||
backward compatibility with CLI usage.
|
||||
Note:
|
||||
This module is pure domain logic. Database operations are handled
|
||||
by the service layer (AnimeService).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -18,8 +15,7 @@ import os
|
||||
import re
|
||||
import traceback
|
||||
import uuid
|
||||
import warnings
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional
|
||||
from typing import Callable, Iterable, Iterator, Optional
|
||||
|
||||
from src.core.entities.series import Serie
|
||||
from src.core.exceptions.Exceptions import MatchNotFoundError, NoKeyFoundException
|
||||
@@ -33,11 +29,6 @@ from src.core.interfaces.callbacks import (
|
||||
)
|
||||
from src.core.providers.base_provider import Loader
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.server.database.models import AnimeSeries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
error_logger = logging.getLogger("error")
|
||||
no_key_found_logger = logging.getLogger("series.nokey")
|
||||
@@ -49,19 +40,15 @@ class SerieScanner:
|
||||
|
||||
Supports progress callbacks for real-time scanning updates.
|
||||
|
||||
The scanner supports two modes:
|
||||
1. File-based (legacy): Set db_session=None, saves to data files
|
||||
2. Database mode: Provide db_session, saves to SQLite database
|
||||
Note:
|
||||
This class is pure domain logic. Database operations are handled
|
||||
by the service layer (AnimeService). Scan results are stored
|
||||
in keyDict and can be retrieved after scanning.
|
||||
|
||||
Example:
|
||||
# File-based mode (legacy)
|
||||
scanner = SerieScanner("/path/to/anime", loader)
|
||||
scanner.scan()
|
||||
|
||||
# Database mode (preferred)
|
||||
async with get_db_session() as db:
|
||||
scanner = SerieScanner("/path/to/anime", loader, db_session=db)
|
||||
await scanner.scan_async()
|
||||
# Results are in scanner.keyDict
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -69,7 +56,6 @@ class SerieScanner:
|
||||
basePath: str,
|
||||
loader: Loader,
|
||||
callback_manager: Optional[CallbackManager] = None,
|
||||
db_session: Optional["AsyncSession"] = None
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the SerieScanner.
|
||||
@@ -78,8 +64,6 @@ class SerieScanner:
|
||||
basePath: Base directory containing anime series
|
||||
loader: Loader instance for fetching series information
|
||||
callback_manager: Optional callback manager for progress updates
|
||||
db_session: Optional database session for database mode.
|
||||
If provided, scan_async() should be used instead of scan().
|
||||
|
||||
Raises:
|
||||
ValueError: If basePath is invalid or doesn't exist
|
||||
@@ -102,7 +86,6 @@ class SerieScanner:
|
||||
callback_manager or CallbackManager()
|
||||
)
|
||||
self._current_operation_id: Optional[str] = None
|
||||
self._db_session: Optional["AsyncSession"] = db_session
|
||||
|
||||
logger.info("Initialized SerieScanner with base path: %s", abs_path)
|
||||
|
||||
@@ -129,27 +112,18 @@ class SerieScanner:
|
||||
callback: Optional[Callable[[str, int], None]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Scan directories for anime series and missing episodes (file-based).
|
||||
Scan directories for anime series and missing episodes.
|
||||
|
||||
This method saves results to data files. For database storage,
|
||||
use scan_async() instead.
|
||||
|
||||
.. deprecated:: 2.0.0
|
||||
Use :meth:`scan_async` for database-backed storage.
|
||||
File-based storage will be removed in a future version.
|
||||
Results are stored in self.keyDict and can be retrieved after
|
||||
scanning. Data files are also saved to disk for persistence.
|
||||
|
||||
Args:
|
||||
callback: Optional legacy callback function (folder, count)
|
||||
callback: Optional callback function (folder, count) for
|
||||
progress updates
|
||||
|
||||
Raises:
|
||||
Exception: If scan fails critically
|
||||
"""
|
||||
warnings.warn(
|
||||
"File-based scan() is deprecated. Use scan_async() for "
|
||||
"database storage.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
# Generate unique operation ID
|
||||
self._current_operation_id = str(uuid.uuid4())
|
||||
|
||||
@@ -336,365 +310,6 @@ class SerieScanner:
|
||||
|
||||
raise
|
||||
|
||||
async def scan_async(
|
||||
self,
|
||||
db: "AsyncSession",
|
||||
callback: Optional[Callable[[str, int], None]] = None
|
||||
) -> None:
|
||||
"""
|
||||
Scan directories for anime series and save to database.
|
||||
|
||||
This is the preferred method for scanning when using database
|
||||
storage. Results are saved to the database instead of files.
|
||||
|
||||
Args:
|
||||
db: Database session for async operations
|
||||
callback: Optional legacy callback function (folder, count)
|
||||
|
||||
Raises:
|
||||
Exception: If scan fails critically
|
||||
|
||||
Example:
|
||||
async with get_db_session() as db:
|
||||
scanner = SerieScanner("/path/to/anime", loader)
|
||||
await scanner.scan_async(db)
|
||||
"""
|
||||
# Generate unique operation ID
|
||||
self._current_operation_id = str(uuid.uuid4())
|
||||
|
||||
logger.info("Starting async scan for missing episodes (database mode)")
|
||||
|
||||
# Notify scan starting
|
||||
self._callback_manager.notify_progress(
|
||||
ProgressContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
phase=ProgressPhase.STARTING,
|
||||
current=0,
|
||||
total=0,
|
||||
percentage=0.0,
|
||||
message="Initializing scan (database mode)"
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
# Get total items to process
|
||||
total_to_scan = self.get_total_to_scan()
|
||||
logger.info("Total folders to scan: %d", total_to_scan)
|
||||
|
||||
result = self.__find_mp4_files()
|
||||
counter = 0
|
||||
saved_to_db = 0
|
||||
|
||||
for folder, mp4_files in result:
|
||||
try:
|
||||
counter += 1
|
||||
|
||||
# Calculate progress
|
||||
if total_to_scan > 0:
|
||||
percentage = (counter / total_to_scan) * 100
|
||||
else:
|
||||
percentage = 0.0
|
||||
|
||||
# Notify progress
|
||||
self._callback_manager.notify_progress(
|
||||
ProgressContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
phase=ProgressPhase.IN_PROGRESS,
|
||||
current=counter,
|
||||
total=total_to_scan,
|
||||
percentage=percentage,
|
||||
message=f"Scanning: {folder}",
|
||||
details=f"Found {len(mp4_files)} episodes"
|
||||
)
|
||||
)
|
||||
|
||||
# Call legacy callback if provided
|
||||
if callback:
|
||||
callback(folder, counter)
|
||||
|
||||
serie = self.__read_data_from_file(folder)
|
||||
if (
|
||||
serie is not None
|
||||
and serie.key
|
||||
and serie.key.strip()
|
||||
):
|
||||
# Get missing episodes from provider
|
||||
missing_episodes, _site = (
|
||||
self.__get_missing_episodes_and_season(
|
||||
serie.key, mp4_files
|
||||
)
|
||||
)
|
||||
serie.episodeDict = missing_episodes
|
||||
serie.folder = folder
|
||||
|
||||
# Save to database instead of file
|
||||
await self._save_serie_to_db(serie, db)
|
||||
saved_to_db += 1
|
||||
|
||||
# Store by key in memory cache
|
||||
if serie.key in self.keyDict:
|
||||
logger.error(
|
||||
"Duplicate series found with key '%s' "
|
||||
"(folder: '%s')",
|
||||
serie.key,
|
||||
folder
|
||||
)
|
||||
else:
|
||||
self.keyDict[serie.key] = serie
|
||||
logger.debug(
|
||||
"Stored series with key '%s' (folder: '%s')",
|
||||
serie.key,
|
||||
folder
|
||||
)
|
||||
|
||||
except NoKeyFoundException as nkfe:
|
||||
error_msg = f"Error processing folder '{folder}': {nkfe}"
|
||||
logger.error(error_msg)
|
||||
self._callback_manager.notify_error(
|
||||
ErrorContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
error=nkfe,
|
||||
message=error_msg,
|
||||
recoverable=True,
|
||||
metadata={"folder": folder, "key": None}
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
error_msg = (
|
||||
f"Folder: '{folder}' - Unexpected error: {e}"
|
||||
)
|
||||
error_logger.error(
|
||||
"%s\n%s",
|
||||
error_msg,
|
||||
traceback.format_exc()
|
||||
)
|
||||
self._callback_manager.notify_error(
|
||||
ErrorContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
error=e,
|
||||
message=error_msg,
|
||||
recoverable=True,
|
||||
metadata={"folder": folder, "key": None}
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
# Notify scan completion
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
success=True,
|
||||
message=f"Scan completed. Processed {counter} folders.",
|
||||
statistics={
|
||||
"total_folders": counter,
|
||||
"series_found": len(self.keyDict),
|
||||
"saved_to_db": saved_to_db
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Async scan completed. Processed %d folders, "
|
||||
"found %d series, saved %d to database",
|
||||
counter,
|
||||
len(self.keyDict),
|
||||
saved_to_db
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Critical async scan error: {e}"
|
||||
logger.error("%s\n%s", error_msg, traceback.format_exc())
|
||||
|
||||
self._callback_manager.notify_error(
|
||||
ErrorContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
error=e,
|
||||
message=error_msg,
|
||||
recoverable=False
|
||||
)
|
||||
)
|
||||
|
||||
self._callback_manager.notify_completion(
|
||||
CompletionContext(
|
||||
operation_type=OperationType.SCAN,
|
||||
operation_id=self._current_operation_id,
|
||||
success=False,
|
||||
message=error_msg
|
||||
)
|
||||
)
|
||||
|
||||
raise
|
||||
|
||||
async def _save_serie_to_db(
|
||||
self,
|
||||
serie: Serie,
|
||||
db: "AsyncSession"
|
||||
) -> Optional["AnimeSeries"]:
|
||||
"""
|
||||
Save or update a series in the database.
|
||||
|
||||
Creates a new record if the series doesn't exist, or updates
|
||||
the episodes if they have changed.
|
||||
|
||||
Args:
|
||||
serie: Serie instance to save
|
||||
db: Database session for async operations
|
||||
|
||||
Returns:
|
||||
Created or updated AnimeSeries instance, or None if unchanged
|
||||
"""
|
||||
from src.server.database.service import AnimeSeriesService, EpisodeService
|
||||
|
||||
# Check if series already exists
|
||||
existing = await AnimeSeriesService.get_by_key(db, serie.key)
|
||||
|
||||
if existing:
|
||||
# Build existing episode dict from episodes for comparison
|
||||
existing_episodes = await EpisodeService.get_by_series(
|
||||
db, existing.id
|
||||
)
|
||||
existing_dict: dict[int, list[int]] = {}
|
||||
for ep in existing_episodes:
|
||||
if ep.season not in existing_dict:
|
||||
existing_dict[ep.season] = []
|
||||
existing_dict[ep.season].append(ep.episode_number)
|
||||
for season in existing_dict:
|
||||
existing_dict[season].sort()
|
||||
|
||||
# Update episodes if changed
|
||||
if existing_dict != serie.episodeDict:
|
||||
# Add new episodes
|
||||
new_dict = serie.episodeDict or {}
|
||||
for season, episode_numbers in new_dict.items():
|
||||
existing_eps = set(existing_dict.get(season, []))
|
||||
for ep_num in episode_numbers:
|
||||
if ep_num not in existing_eps:
|
||||
await EpisodeService.create(
|
||||
db=db,
|
||||
series_id=existing.id,
|
||||
season=season,
|
||||
episode_number=ep_num,
|
||||
)
|
||||
|
||||
# Update folder if changed
|
||||
if existing.folder != serie.folder:
|
||||
await AnimeSeriesService.update(
|
||||
db,
|
||||
existing.id,
|
||||
folder=serie.folder
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Updated series in database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
return existing
|
||||
else:
|
||||
logger.debug(
|
||||
"Series unchanged in database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
return None
|
||||
else:
|
||||
# Create new series
|
||||
anime_series = await AnimeSeriesService.create(
|
||||
db=db,
|
||||
key=serie.key,
|
||||
name=serie.name,
|
||||
site=serie.site,
|
||||
folder=serie.folder,
|
||||
)
|
||||
|
||||
# Create Episode records
|
||||
if serie.episodeDict:
|
||||
for season, episode_numbers in serie.episodeDict.items():
|
||||
for ep_num in episode_numbers:
|
||||
await EpisodeService.create(
|
||||
db=db,
|
||||
series_id=anime_series.id,
|
||||
season=season,
|
||||
episode_number=ep_num,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Created series in database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
return anime_series
|
||||
|
||||
async def _update_serie_in_db(
|
||||
self,
|
||||
serie: Serie,
|
||||
db: "AsyncSession"
|
||||
) -> Optional["AnimeSeries"]:
|
||||
"""
|
||||
Update an existing series in the database.
|
||||
|
||||
Args:
|
||||
serie: Serie instance to update
|
||||
db: Database session for async operations
|
||||
|
||||
Returns:
|
||||
Updated AnimeSeries instance, or None if not found
|
||||
"""
|
||||
from src.server.database.service import AnimeSeriesService, EpisodeService
|
||||
|
||||
existing = await AnimeSeriesService.get_by_key(db, serie.key)
|
||||
if not existing:
|
||||
logger.warning(
|
||||
"Cannot update non-existent series: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
return None
|
||||
|
||||
# Update basic fields
|
||||
await AnimeSeriesService.update(
|
||||
db,
|
||||
existing.id,
|
||||
name=serie.name,
|
||||
site=serie.site,
|
||||
folder=serie.folder,
|
||||
)
|
||||
|
||||
# Update episodes - add any new ones
|
||||
if serie.episodeDict:
|
||||
existing_episodes = await EpisodeService.get_by_series(
|
||||
db, existing.id
|
||||
)
|
||||
existing_dict: dict[int, set[int]] = {}
|
||||
for ep in existing_episodes:
|
||||
if ep.season not in existing_dict:
|
||||
existing_dict[ep.season] = set()
|
||||
existing_dict[ep.season].add(ep.episode_number)
|
||||
|
||||
for season, episode_numbers in serie.episodeDict.items():
|
||||
existing_eps = existing_dict.get(season, set())
|
||||
for ep_num in episode_numbers:
|
||||
if ep_num not in existing_eps:
|
||||
await EpisodeService.create(
|
||||
db=db,
|
||||
series_id=existing.id,
|
||||
season=season,
|
||||
episode_number=ep_num,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Updated series in database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
return existing
|
||||
|
||||
def __find_mp4_files(self) -> Iterator[tuple[str, list[str]]]:
|
||||
"""Find all .mp4 files in the directory structure."""
|
||||
logger.info("Scanning for .mp4 files")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
import warnings
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ class AnimeService:
|
||||
),
|
||||
loop
|
||||
)
|
||||
except Exception as exc:
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.error(
|
||||
"Error handling download status event",
|
||||
error=str(exc)
|
||||
@@ -221,7 +221,7 @@ class AnimeService:
|
||||
),
|
||||
loop
|
||||
)
|
||||
except Exception as exc:
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.error("Error handling scan status event: %s", exc)
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
@@ -288,6 +288,8 @@ class AnimeService:
|
||||
The SeriesApp handles progress tracking via events which are
|
||||
forwarded to the ProgressService through event handlers.
|
||||
|
||||
After scanning, results are persisted to the database.
|
||||
|
||||
All series are identified by their 'key' (provider identifier),
|
||||
with 'folder' stored as metadata.
|
||||
"""
|
||||
@@ -295,19 +297,268 @@ class AnimeService:
|
||||
# Store event loop for event handlers
|
||||
self._event_loop = asyncio.get_running_loop()
|
||||
|
||||
# SeriesApp.rescan is now async and handles events internally
|
||||
await self._app.rescan()
|
||||
# SeriesApp.rescan returns scanned series list
|
||||
scanned_series = await self._app.rescan()
|
||||
|
||||
# Persist scan results to database
|
||||
if scanned_series:
|
||||
await self._save_scan_results_to_db(scanned_series)
|
||||
|
||||
# Reload series from database to ensure consistency
|
||||
await self._load_series_from_db()
|
||||
|
||||
# invalidate cache
|
||||
try:
|
||||
self._cached_list_missing.cache_clear()
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
except Exception as exc:
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
logger.exception("rescan failed")
|
||||
raise AnimeServiceError("Rescan failed") from exc
|
||||
|
||||
async def _save_scan_results_to_db(self, series_list: list) -> int:
|
||||
"""
|
||||
Save scan results to the database.
|
||||
|
||||
Creates or updates series records in the database based on
|
||||
scan results.
|
||||
|
||||
Args:
|
||||
series_list: List of Serie objects from scan
|
||||
|
||||
Returns:
|
||||
Number of series saved/updated
|
||||
"""
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
saved_count = 0
|
||||
|
||||
async with get_db_session() as db:
|
||||
for serie in series_list:
|
||||
try:
|
||||
# Check if series already exists
|
||||
existing = await AnimeSeriesService.get_by_key(
|
||||
db, serie.key
|
||||
)
|
||||
|
||||
if existing:
|
||||
# Update existing series
|
||||
await self._update_series_in_db(
|
||||
serie, existing, db
|
||||
)
|
||||
else:
|
||||
# Create new series
|
||||
await self._create_series_in_db(serie, db)
|
||||
|
||||
saved_count += 1
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.warning(
|
||||
"Failed to save series to database: %s (key=%s) - %s",
|
||||
serie.name,
|
||||
serie.key,
|
||||
str(e)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Saved %d series to database from scan results",
|
||||
saved_count
|
||||
)
|
||||
return saved_count
|
||||
|
||||
async def _create_series_in_db(self, serie, db) -> None:
|
||||
"""Create a new series in the database."""
|
||||
from src.server.database.service import (
|
||||
AnimeSeriesService, EpisodeService
|
||||
)
|
||||
|
||||
anime_series = await AnimeSeriesService.create(
|
||||
db=db,
|
||||
key=serie.key,
|
||||
name=serie.name,
|
||||
site=serie.site,
|
||||
folder=serie.folder,
|
||||
)
|
||||
|
||||
# Create Episode records
|
||||
if serie.episodeDict:
|
||||
for season, episode_numbers in serie.episodeDict.items():
|
||||
for ep_num in episode_numbers:
|
||||
await EpisodeService.create(
|
||||
db=db,
|
||||
series_id=anime_series.id,
|
||||
season=season,
|
||||
episode_number=ep_num,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Created series in database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
|
||||
async def _update_series_in_db(self, serie, existing, db) -> None:
|
||||
"""Update an existing series in the database."""
|
||||
from src.server.database.service import (
|
||||
AnimeSeriesService, EpisodeService
|
||||
)
|
||||
|
||||
# Get existing episodes
|
||||
existing_episodes = await EpisodeService.get_by_series(db, existing.id)
|
||||
existing_dict: dict[int, list[int]] = {}
|
||||
for ep in existing_episodes:
|
||||
if ep.season not in existing_dict:
|
||||
existing_dict[ep.season] = []
|
||||
existing_dict[ep.season].append(ep.episode_number)
|
||||
for season in existing_dict:
|
||||
existing_dict[season].sort()
|
||||
|
||||
# Update episodes if changed
|
||||
if existing_dict != serie.episodeDict:
|
||||
new_dict = serie.episodeDict or {}
|
||||
for season, episode_numbers in new_dict.items():
|
||||
existing_eps = set(existing_dict.get(season, []))
|
||||
for ep_num in episode_numbers:
|
||||
if ep_num not in existing_eps:
|
||||
await EpisodeService.create(
|
||||
db=db,
|
||||
series_id=existing.id,
|
||||
season=season,
|
||||
episode_number=ep_num,
|
||||
)
|
||||
|
||||
# Update folder if changed
|
||||
if existing.folder != serie.folder:
|
||||
await AnimeSeriesService.update(
|
||||
db,
|
||||
existing.id,
|
||||
folder=serie.folder
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Updated series in database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
|
||||
async def _load_series_from_db(self) -> None:
|
||||
"""
|
||||
Load series from the database into SeriesApp.
|
||||
|
||||
This method is called during initialization and after rescans
|
||||
to ensure the in-memory series list is in sync with the database.
|
||||
"""
|
||||
from src.core.entities.series import Serie
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
async with get_db_session() as db:
|
||||
anime_series_list = await AnimeSeriesService.get_all(
|
||||
db, with_episodes=True
|
||||
)
|
||||
|
||||
# Convert to Serie objects
|
||||
series_list = []
|
||||
for anime_series in anime_series_list:
|
||||
# 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
|
||||
for season in episode_dict:
|
||||
episode_dict[season].sort()
|
||||
|
||||
serie = Serie(
|
||||
key=anime_series.key,
|
||||
name=anime_series.name,
|
||||
site=anime_series.site,
|
||||
folder=anime_series.folder,
|
||||
episodeDict=episode_dict
|
||||
)
|
||||
series_list.append(serie)
|
||||
|
||||
# Load into SeriesApp
|
||||
self._app.load_series_from_list(series_list)
|
||||
|
||||
async def add_series_to_db(
|
||||
self,
|
||||
serie,
|
||||
db
|
||||
):
|
||||
"""
|
||||
Add a series to the database if it doesn't already exist.
|
||||
|
||||
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
|
||||
"""
|
||||
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,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Added series to database: %s (key=%s)",
|
||||
serie.name,
|
||||
serie.key
|
||||
)
|
||||
|
||||
return anime_series
|
||||
|
||||
async def contains_in_db(self, key: str, db) -> 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
|
||||
|
||||
async def download(
|
||||
self,
|
||||
serie_folder: str,
|
||||
@@ -365,7 +616,7 @@ def get_anime_service(series_app: SeriesApp) -> AnimeService:
|
||||
|
||||
async def sync_series_from_data_files(
|
||||
anime_directory: str,
|
||||
logger=None
|
||||
log_instance=None
|
||||
) -> int:
|
||||
"""
|
||||
Sync series from data files to the database.
|
||||
@@ -379,17 +630,17 @@ async def sync_series_from_data_files(
|
||||
|
||||
Args:
|
||||
anime_directory: Path to the anime directory with data files
|
||||
logger: Optional logger instance for logging operations.
|
||||
log_instance: Optional logger instance for logging operations.
|
||||
If not provided, uses structlog.
|
||||
|
||||
Returns:
|
||||
Number of new series added to the database
|
||||
"""
|
||||
log = logger or structlog.get_logger(__name__)
|
||||
log = log_instance or structlog.get_logger(__name__)
|
||||
|
||||
try:
|
||||
from src.core.entities.SerieList import SerieList
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.service import AnimeSeriesService, EpisodeService
|
||||
|
||||
log.info(
|
||||
"Starting data file to database sync",
|
||||
@@ -412,11 +663,6 @@ async def sync_series_from_data_files(
|
||||
)
|
||||
|
||||
async with get_db_session() as db:
|
||||
serie_list = SerieList(
|
||||
anime_directory,
|
||||
db_session=db,
|
||||
skip_load=True
|
||||
)
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
for serie in all_series:
|
||||
@@ -438,15 +684,43 @@ async def sync_series_from_data_files(
|
||||
continue
|
||||
|
||||
try:
|
||||
result = await serie_list.add_to_db(serie, db)
|
||||
if result:
|
||||
added_count += 1
|
||||
# Check if series already exists in DB
|
||||
existing = await AnimeSeriesService.get_by_key(db, serie.key)
|
||||
if existing:
|
||||
log.debug(
|
||||
"Added series to database",
|
||||
"Series already exists in database",
|
||||
name=serie.name,
|
||||
key=serie.key
|
||||
)
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
# 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,
|
||||
)
|
||||
|
||||
added_count += 1
|
||||
log.debug(
|
||||
"Added series to database",
|
||||
name=serie.name,
|
||||
key=serie.key
|
||||
)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
log.warning(
|
||||
"Failed to add series to database",
|
||||
key=serie.key,
|
||||
@@ -462,7 +736,7 @@ async def sync_series_from_data_files(
|
||||
)
|
||||
return added_count
|
||||
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
log.warning(
|
||||
"Failed to sync series to database",
|
||||
error=str(e),
|
||||
|
||||
@@ -169,43 +169,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user