Compare commits
7 Commits
288b03cbb4
...
v1.4.16
| Author | SHA1 | Date | |
|---|---|---|---|
| cbc44491e7 | |||
| e319cfecb8 | |||
| 4f61ded92a | |||
| d6082b5cf6 | |||
| e76cd3a708 | |||
| 08f7f7453c | |||
| 023ddd182f |
@@ -1 +1 @@
|
||||
v1.4.14
|
||||
v1.4.16
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aniworld-web",
|
||||
"version": "1.4.14",
|
||||
"version": "1.4.16",
|
||||
"description": "Aniworld Anime Download Manager - Web Frontend",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -279,30 +279,15 @@ async def update_directory(
|
||||
|
||||
config_service.save_config(app_config)
|
||||
|
||||
# Sync series from data files to database
|
||||
sync_count = 0
|
||||
try:
|
||||
import structlog
|
||||
|
||||
from src.server.services.anime_service import sync_legacy_series_to_db
|
||||
logger = structlog.get_logger(__name__)
|
||||
sync_count = await sync_legacy_series_to_db(directory, logger)
|
||||
logger.info(
|
||||
"Directory updated: synced series from data files",
|
||||
directory=directory,
|
||||
count=sync_count
|
||||
)
|
||||
except Exception as e:
|
||||
# Log but don't fail the directory update if sync fails
|
||||
import structlog
|
||||
structlog.get_logger(__name__).warning(
|
||||
"Failed to sync series after directory update",
|
||||
error=str(e)
|
||||
)
|
||||
# Series are now loaded directly from database, no sync needed
|
||||
logger.info(
|
||||
"Directory updated successfully",
|
||||
directory=directory
|
||||
)
|
||||
|
||||
response: Dict[str, Any] = {
|
||||
"message": "Anime directory updated successfully",
|
||||
"synced_series": sync_count
|
||||
"synced_series": 0
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
@@ -210,6 +210,15 @@ class AnimeSeries(Base, TimestampMixin):
|
||||
episode_dict[season].append(ep.episode_number or 0)
|
||||
return episode_dict
|
||||
|
||||
@episodeDict.setter
|
||||
def episodeDict(self, value: dict[int, list[int]]) -> None:
|
||||
"""Set the episode dictionary via private cache.
|
||||
|
||||
Args:
|
||||
value: Dictionary mapping season numbers to lists of episode numbers
|
||||
"""
|
||||
self._episode_dict_cache = value
|
||||
|
||||
@property
|
||||
def name_with_year(self) -> str:
|
||||
"""Get series name with year appended if available.
|
||||
|
||||
@@ -422,3 +422,32 @@ class TMDBClient:
|
||||
if expired_keys:
|
||||
logger.debug("Removed %d expired negative cache entries", len(expired_keys))
|
||||
return len(expired_keys)
|
||||
|
||||
|
||||
def get_tmdb_client() -> TMDBClient:
|
||||
"""Factory function to create a TMDBClient with settings configuration.
|
||||
|
||||
Returns:
|
||||
TMDBClient instance configured with settings.tmdb_api_key
|
||||
|
||||
Raises:
|
||||
ValueError: If TMDB API key is not configured
|
||||
"""
|
||||
from src.config.settings import settings
|
||||
|
||||
if not settings.tmdb_api_key:
|
||||
raise ValueError("TMDB API key is not configured")
|
||||
|
||||
return TMDBClient(api_key=settings.tmdb_api_key)
|
||||
|
||||
|
||||
def get_tmdb_image_base_url(tmdb_id: int) -> str:
|
||||
"""Get the base URL for TMDB images.
|
||||
|
||||
Args:
|
||||
tmdb_id: TMDB show ID (used for account-specific URLs)
|
||||
|
||||
Returns:
|
||||
Base URL string for TMDB images
|
||||
"""
|
||||
return "https://image.tmdb.org/t/p/"
|
||||
|
||||
@@ -1618,139 +1618,3 @@ class AnimeService:
|
||||
def get_anime_service(series_app: SeriesApp) -> AnimeService:
|
||||
"""Factory used for creating AnimeService with a SeriesApp instance."""
|
||||
return AnimeService(series_app)
|
||||
|
||||
|
||||
async def sync_legacy_series_to_db(
|
||||
anime_directory: str,
|
||||
log_instance=None # pylint: disable=unused-argument
|
||||
) -> int:
|
||||
"""
|
||||
One-time legacy sync: import any series from 'data' files
|
||||
not already in the database.
|
||||
|
||||
Deprecated: Series are now loaded directly from the database.
|
||||
This function remains for backwards compatibility with legacy
|
||||
file-based data during migration.
|
||||
|
||||
Args:
|
||||
anime_directory: Path to the anime directory with data files
|
||||
log_instance: Optional logger instance (unused, kept for API
|
||||
compatibility). This function always uses structlog internally.
|
||||
|
||||
Returns:
|
||||
Number of new series added to the database
|
||||
"""
|
||||
# Always use structlog for structured logging with keyword arguments
|
||||
log = structlog.get_logger(__name__)
|
||||
|
||||
log.warning(
|
||||
"sync_legacy_series_to_db is deprecated. "
|
||||
"Series are now loaded directly from database."
|
||||
)
|
||||
|
||||
try:
|
||||
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",
|
||||
directory=anime_directory
|
||||
)
|
||||
|
||||
# Get all series from data files using SeriesApp
|
||||
series_app = SeriesApp(anime_directory)
|
||||
all_series = await asyncio.to_thread(
|
||||
series_app.get_all_series_from_data_files
|
||||
)
|
||||
|
||||
if not all_series:
|
||||
log.info("No series found in data files to sync")
|
||||
return 0
|
||||
|
||||
log.info(
|
||||
"Found series in data files, syncing to database",
|
||||
count=len(all_series)
|
||||
)
|
||||
|
||||
async with get_db_session() as db:
|
||||
added_count = 0
|
||||
skipped_count = 0
|
||||
for serie in all_series:
|
||||
# Handle series with empty name - use folder as fallback
|
||||
if not serie.name or not serie.name.strip():
|
||||
if serie.folder and serie.folder.strip():
|
||||
serie.name = serie.folder.strip()
|
||||
log.debug(
|
||||
"Using folder as name fallback",
|
||||
key=serie.key,
|
||||
folder=serie.folder
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
"Skipping series with empty name and folder",
|
||||
key=serie.key
|
||||
)
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
# Check if series already exists in DB
|
||||
existing = await AnimeSeriesService.get_by_key(db, serie.key)
|
||||
if existing:
|
||||
log.debug(
|
||||
"Series already exists in database",
|
||||
name=serie.name,
|
||||
key=serie.key
|
||||
)
|
||||
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,
|
||||
year=serie.year if hasattr(serie, 'year') else None,
|
||||
)
|
||||
|
||||
# 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,
|
||||
name=serie.name,
|
||||
error=str(e)
|
||||
)
|
||||
skipped_count += 1
|
||||
|
||||
log.info(
|
||||
"Data file sync complete",
|
||||
added=added_count,
|
||||
skipped=len(all_series) - added_count
|
||||
)
|
||||
return added_count
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
log.warning(
|
||||
"Failed to sync series to database",
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -9,7 +9,6 @@ import structlog
|
||||
|
||||
from src.config.settings import settings
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
from src.server.services.anime_service import sync_legacy_series_to_db
|
||||
from src.server.services.setup_service import SetupService
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -237,14 +236,15 @@ async def _sync_anime_folders(progress_service=None) -> int:
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
|
||||
sync_count = await sync_legacy_series_to_db(settings.anime_directory)
|
||||
logger.info("Data file sync complete. Added %d series.", sync_count)
|
||||
# Legacy sync removed - series are loaded directly from database via _load_series_into_memory
|
||||
sync_count = 0
|
||||
logger.info("Data file sync skipped - series loaded directly from database")
|
||||
|
||||
if progress_service:
|
||||
await progress_service.update_progress(
|
||||
progress_id="series_sync",
|
||||
current=75,
|
||||
message=f"Synced {sync_count} series from data files",
|
||||
message=f"Series loaded directly from database",
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
|
||||
@@ -478,13 +478,8 @@ async def _execute_nfo_scan(progress_service=None) -> None:
|
||||
key=data.get('key'),
|
||||
folder=data.get('folder'),
|
||||
)
|
||||
elif event_data.get('type') == 'nfo_scan_completed':
|
||||
stats = event_data.get('statistics', {})
|
||||
if progress_service:
|
||||
await progress_service.complete_progress(
|
||||
progress_id="nfo_scan",
|
||||
message=f"NFO scan complete: {stats.get('created', 0)} created, {stats.get('updated', 0)} updated",
|
||||
)
|
||||
# Note: nfo_scan_completed event is NOT handled here because
|
||||
# perform_nfo_scan_phase handles completion after _execute_nfo_scan returns
|
||||
|
||||
nfo_service.subscribe_to_scan_events(nfo_event_handler)
|
||||
|
||||
@@ -546,16 +541,25 @@ async def perform_nfo_scan_if_needed(progress_service=None):
|
||||
|
||||
# Execute the NFO scan
|
||||
try:
|
||||
# Ensure any newly created series are loaded from DB into SeriesApp memory
|
||||
await _load_series_into_memory(progress_service=None)
|
||||
await _execute_nfo_scan(progress_service)
|
||||
await _mark_nfo_scan_completed()
|
||||
except Exception as e:
|
||||
logger.error("Failed to complete NFO scan: %s", e, exc_info=True)
|
||||
if progress_service:
|
||||
await progress_service.fail_progress(
|
||||
progress_id="nfo_scan",
|
||||
error_message=f"NFO scan failed: {str(e)}",
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
try:
|
||||
await progress_service.fail_progress(
|
||||
progress_id="nfo_scan",
|
||||
error_message=f"NFO scan failed: {str(e)}",
|
||||
metadata={"step_id": "nfo_scan"}
|
||||
)
|
||||
except Exception as fail_err:
|
||||
logger.warning(
|
||||
"Could not fail progress 'nfo_scan': %s",
|
||||
fail_err,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
async def perform_nfo_scan_phase(progress_service=None):
|
||||
@@ -613,6 +617,9 @@ async def perform_nfo_scan_phase(progress_service=None):
|
||||
|
||||
# Execute the NFO scan
|
||||
try:
|
||||
# Ensure any newly created series (e.g., from resolving unresolved folders)
|
||||
# are loaded from DB into SeriesApp memory before scanning
|
||||
await _load_series_into_memory(progress_service=None)
|
||||
await _execute_nfo_scan(progress_service)
|
||||
await _mark_nfo_scan_completed()
|
||||
|
||||
@@ -627,11 +634,18 @@ async def perform_nfo_scan_phase(progress_service=None):
|
||||
except Exception as e:
|
||||
logger.error("Failed to complete NFO scan phase: %s", e, exc_info=True)
|
||||
if progress_service:
|
||||
await progress_service.fail_progress(
|
||||
progress_id="nfo_scan",
|
||||
error_message=f"NFO scan failed: {str(e)}",
|
||||
metadata={"step_id": "nfo_scan", "phase": "nfo"}
|
||||
)
|
||||
try:
|
||||
await progress_service.fail_progress(
|
||||
progress_id="nfo_scan",
|
||||
error_message=f"NFO scan failed: {str(e)}",
|
||||
metadata={"step_id": "nfo_scan", "phase": "nfo"}
|
||||
)
|
||||
except Exception as fail_err:
|
||||
logger.warning(
|
||||
"Could not fail progress 'nfo_scan': %s",
|
||||
fail_err,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
|
||||
async def _check_media_scan_status() -> bool:
|
||||
|
||||
@@ -326,6 +326,22 @@ class NfoScanService:
|
||||
|
||||
nfo_exists = os.path.isfile(nfo_path)
|
||||
|
||||
# If tmdb_id is missing, try to look it up by series name
|
||||
if not series_data.get("tmdb_id"):
|
||||
logger.debug("No tmdb_id for %s — attempting TMDB lookup", key)
|
||||
name = series_data.get("name", "")
|
||||
found_tmdb_id = await self._lookup_tmdb_id_by_name(name)
|
||||
if found_tmdb_id:
|
||||
series_data["tmdb_id"] = found_tmdb_id
|
||||
await self._save_tmdb_id(key, found_tmdb_id)
|
||||
logger.info("Found and saved tmdb_id %s for %s", found_tmdb_id, key)
|
||||
else:
|
||||
logger.warning(
|
||||
"Could not resolve tmdb_id for %s (%s)",
|
||||
key,
|
||||
name,
|
||||
)
|
||||
|
||||
if not nfo_exists:
|
||||
# Create new NFO
|
||||
logger.info("Creating NFO for series: %s (%s)", key, folder)
|
||||
@@ -526,6 +542,53 @@ class NfoScanService:
|
||||
logger.info("Regenerated NFO for %s", key)
|
||||
return True
|
||||
|
||||
async def _save_tmdb_id(self, key: str, tmdb_id: int) -> None:
|
||||
"""Save tmdb_id to the database for a series.
|
||||
|
||||
Args:
|
||||
key: Series key (primary identifier)
|
||||
tmdb_id: TMDB series ID to save
|
||||
"""
|
||||
try:
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.database.service import AnimeSeriesService
|
||||
|
||||
async with get_db_session() as db:
|
||||
series = await AnimeSeriesService.get_by_key(db, key)
|
||||
if series:
|
||||
series.tmdb_id = tmdb_id
|
||||
await db.flush()
|
||||
logger.debug("Saved tmdb_id %s for series: %s", tmdb_id, key)
|
||||
else:
|
||||
logger.warning("Series not found for tmdb_id save: %s", key)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to save tmdb_id for %s: %s", key, exc)
|
||||
|
||||
async def _lookup_tmdb_id_by_name(self, name: str) -> Optional[int]:
|
||||
"""Look up a TMDB series ID by series name.
|
||||
|
||||
Args:
|
||||
name: Series name to search for
|
||||
|
||||
Returns:
|
||||
TMDB series ID or None if not found.
|
||||
"""
|
||||
if not name:
|
||||
return None
|
||||
|
||||
try:
|
||||
from src.server.nfo.tmdb_client import get_tmdb_client
|
||||
|
||||
client = get_tmdb_client()
|
||||
results = await client.search_tv_show(name)
|
||||
if results and results.get("results"):
|
||||
first_result = results["results"][0]
|
||||
return first_result.get("id")
|
||||
return None
|
||||
except Exception as exc:
|
||||
logger.warning("TMDB lookup failed for %s: %s", name, exc)
|
||||
return None
|
||||
|
||||
async def _fetch_tmdb_data(self, tmdb_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch series metadata from TMDB API.
|
||||
|
||||
@@ -539,7 +602,7 @@ class NfoScanService:
|
||||
from src.server.nfo.tmdb_client import get_tmdb_client
|
||||
|
||||
client = get_tmdb_client()
|
||||
data = await client.get_series_details(tmdb_id)
|
||||
data = await client.get_tv_show_details(tmdb_id)
|
||||
return data
|
||||
except Exception as exc:
|
||||
logger.warning("TMDB fetch failed for TMDB ID %s: %s", tmdb_id, exc)
|
||||
|
||||
@@ -110,81 +110,6 @@ class TestGetAllSeriesFromDataFiles:
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
class TestSyncSeriesToDatabase:
|
||||
"""Test sync_legacy_series_to_db function from anime_service."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_with_empty_directory(self):
|
||||
"""Test sync with empty anime directory."""
|
||||
from src.server.services.anime_service import sync_legacy_series_to_db
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
with patch('src.server.SeriesApp.Loaders'), \
|
||||
patch('src.server.SeriesApp.SerieScanner'):
|
||||
count = await sync_legacy_series_to_db(tmp_dir)
|
||||
|
||||
assert count == 0
|
||||
# Function should complete successfully with no series
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_adds_new_series_to_database(self):
|
||||
"""Test that sync adds new series to database.
|
||||
|
||||
This is a more realistic test that verifies series data is loaded
|
||||
from files and the sync function attempts to add them to the DB.
|
||||
The actual DB interaction is tested in test_add_to_db_creates_record.
|
||||
"""
|
||||
from src.server.services.anime_service import sync_legacy_series_to_db
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
# Create test data files
|
||||
_create_test_data_file(
|
||||
tmp_dir,
|
||||
folder="Sync Test Anime",
|
||||
key="sync-test-anime",
|
||||
name="Sync Test Anime",
|
||||
episodes={1: [1, 2]}
|
||||
)
|
||||
|
||||
# First verify that we can load the series from files
|
||||
with patch('src.server.SeriesApp.Loaders'), \
|
||||
patch('src.server.SeriesApp.SerieScanner'):
|
||||
app = SeriesApp(tmp_dir)
|
||||
series = app.get_all_series_from_data_files()
|
||||
assert len(series) == 1
|
||||
assert series[0].key == "sync-test-anime"
|
||||
|
||||
# Now test that the sync function loads series and handles DB
|
||||
# gracefully (even if DB operations fail, it should not crash)
|
||||
with patch('src.server.SeriesApp.Loaders'), \
|
||||
patch('src.server.SeriesApp.SerieScanner'):
|
||||
# The function should return 0 because DB isn't available
|
||||
# but should not crash
|
||||
count = await sync_legacy_series_to_db(tmp_dir)
|
||||
|
||||
# Since no real DB, it will fail gracefully
|
||||
# Function returns 0 when DB operations fail
|
||||
assert isinstance(count, int)
|
||||
assert count == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_handles_exceptions_gracefully(self):
|
||||
"""Test that sync handles exceptions without crashing."""
|
||||
from src.server.services.anime_service import sync_legacy_series_to_db
|
||||
|
||||
# Make SeriesApp raise an exception during initialization
|
||||
with patch('src.server.SeriesApp.Loaders'), \
|
||||
patch('src.server.SeriesApp.SerieScanner'), \
|
||||
patch(
|
||||
'src.server.SeriesApp.SerieList',
|
||||
side_effect=Exception("Test error")
|
||||
):
|
||||
count = await sync_legacy_series_to_db("/fake/path")
|
||||
|
||||
assert count == 0
|
||||
# Function should complete without crashing
|
||||
|
||||
|
||||
class TestEndToEndSync:
|
||||
"""End-to-end tests for the sync functionality."""
|
||||
|
||||
|
||||
@@ -13,11 +13,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.server.services.anime_service import (
|
||||
AnimeService,
|
||||
AnimeServiceError,
|
||||
sync_legacy_series_to_db,
|
||||
)
|
||||
from src.server.services.anime_service import AnimeService, AnimeServiceError
|
||||
from src.server.services.progress_service import ProgressService
|
||||
|
||||
|
||||
@@ -1302,142 +1298,3 @@ class TestGetNFOStatisticsSelfManaged:
|
||||
assert result["with_tmdb_id"] == 40
|
||||
|
||||
|
||||
class TestSyncSeriesFromDataFiles:
|
||||
"""Test module-level sync_legacy_series_to_db function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_adds_new_series(self, tmp_path):
|
||||
"""Should create series for data files not in DB."""
|
||||
mock_serie = MagicMock()
|
||||
mock_serie.key = "new-series"
|
||||
mock_serie.name = "New Series"
|
||||
mock_serie.site = "aniworld.to"
|
||||
mock_serie.folder = "New Series"
|
||||
mock_serie.episodeDict = {1: [1]}
|
||||
|
||||
mock_session = AsyncMock()
|
||||
mock_ctx = AsyncMock()
|
||||
mock_ctx.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_ctx.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch(
|
||||
"src.server.services.anime_service.SeriesApp"
|
||||
) as MockApp, patch(
|
||||
"src.server.database.connection.get_db_session",
|
||||
return_value=mock_ctx,
|
||||
), patch(
|
||||
"src.server.database.service.AnimeSeriesService.get_by_key",
|
||||
new_callable=AsyncMock,
|
||||
return_value=None,
|
||||
), patch(
|
||||
"src.server.database.service.AnimeSeriesService.create",
|
||||
new_callable=AsyncMock,
|
||||
return_value=MagicMock(id=1),
|
||||
) as mock_create, patch(
|
||||
"src.server.database.service.EpisodeService.create",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
mock_app_instance = MagicMock()
|
||||
mock_app_instance.get_all_series_from_data_files.return_value = [
|
||||
mock_serie
|
||||
]
|
||||
MockApp.return_value = mock_app_instance
|
||||
|
||||
count = await sync_legacy_series_to_db(str(tmp_path))
|
||||
|
||||
assert count == 1
|
||||
mock_create.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_skips_existing(self, tmp_path):
|
||||
"""Already-existing series should be skipped."""
|
||||
mock_serie = MagicMock()
|
||||
mock_serie.key = "exists"
|
||||
mock_serie.name = "Exists"
|
||||
mock_serie.site = "x"
|
||||
mock_serie.folder = "Exists"
|
||||
mock_serie.episodeDict = {}
|
||||
|
||||
mock_session = AsyncMock()
|
||||
mock_ctx = AsyncMock()
|
||||
mock_ctx.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_ctx.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch(
|
||||
"src.server.services.anime_service.SeriesApp"
|
||||
) as MockApp, patch(
|
||||
"src.server.database.connection.get_db_session",
|
||||
return_value=mock_ctx,
|
||||
), patch(
|
||||
"src.server.database.service.AnimeSeriesService.get_by_key",
|
||||
new_callable=AsyncMock,
|
||||
return_value=MagicMock(),
|
||||
), patch(
|
||||
"src.server.database.service.AnimeSeriesService.create",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_create:
|
||||
mock_app_instance = MagicMock()
|
||||
mock_app_instance.get_all_series_from_data_files.return_value = [
|
||||
mock_serie
|
||||
]
|
||||
MockApp.return_value = mock_app_instance
|
||||
|
||||
count = await sync_legacy_series_to_db(str(tmp_path))
|
||||
|
||||
assert count == 0
|
||||
mock_create.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_no_data_files(self, tmp_path):
|
||||
"""Empty directory should return 0."""
|
||||
with patch(
|
||||
"src.server.services.anime_service.SeriesApp"
|
||||
) as MockApp:
|
||||
mock_app_instance = MagicMock()
|
||||
mock_app_instance.get_all_series_from_data_files.return_value = []
|
||||
MockApp.return_value = mock_app_instance
|
||||
|
||||
count = await sync_legacy_series_to_db(str(tmp_path))
|
||||
|
||||
assert count == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_handles_empty_name(self, tmp_path):
|
||||
"""Series with empty name should use folder as fallback."""
|
||||
mock_serie = MagicMock()
|
||||
mock_serie.key = "no-name"
|
||||
mock_serie.name = ""
|
||||
mock_serie.site = "x"
|
||||
mock_serie.folder = "FallbackFolder"
|
||||
mock_serie.episodeDict = {}
|
||||
|
||||
mock_session = AsyncMock()
|
||||
mock_ctx = AsyncMock()
|
||||
mock_ctx.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_ctx.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch(
|
||||
"src.server.services.anime_service.SeriesApp"
|
||||
) as MockApp, patch(
|
||||
"src.server.database.connection.get_db_session",
|
||||
return_value=mock_ctx,
|
||||
), patch(
|
||||
"src.server.database.service.AnimeSeriesService.get_by_key",
|
||||
new_callable=AsyncMock,
|
||||
return_value=None,
|
||||
), patch(
|
||||
"src.server.database.service.AnimeSeriesService.create",
|
||||
new_callable=AsyncMock,
|
||||
return_value=MagicMock(id=1),
|
||||
) as mock_create:
|
||||
mock_app_instance = MagicMock()
|
||||
mock_app_instance.get_all_series_from_data_files.return_value = [
|
||||
mock_serie
|
||||
]
|
||||
MockApp.return_value = mock_app_instance
|
||||
|
||||
count = await sync_legacy_series_to_db(str(tmp_path))
|
||||
|
||||
assert count == 1
|
||||
# The name should have been set to folder
|
||||
assert mock_serie.name == "FallbackFolder"
|
||||
|
||||
@@ -161,14 +161,11 @@ class TestSyncAnimeFolders:
|
||||
async def test_sync_anime_folders_without_progress(self):
|
||||
"""Test syncing anime folders without progress service."""
|
||||
with patch('src.server.services.initialization_service.settings') as mock_settings, \
|
||||
patch('src.server.services.initialization_service.os.path.isdir', return_value=True), \
|
||||
patch('src.server.services.initialization_service.sync_legacy_series_to_db',
|
||||
new_callable=AsyncMock, return_value=42) as mock_sync:
|
||||
patch('src.server.services.initialization_service.os.path.isdir', return_value=True):
|
||||
mock_settings.anime_directory = "/path/to/anime"
|
||||
result = await _sync_anime_folders()
|
||||
|
||||
assert result == 42
|
||||
mock_sync.assert_called_once()
|
||||
assert result == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_anime_folders_with_progress(self):
|
||||
@@ -176,13 +173,11 @@ class TestSyncAnimeFolders:
|
||||
mock_progress = AsyncMock()
|
||||
|
||||
with patch('src.server.services.initialization_service.settings') as mock_settings, \
|
||||
patch('src.server.services.initialization_service.os.path.isdir', return_value=True), \
|
||||
patch('src.server.services.initialization_service.sync_legacy_series_to_db',
|
||||
new_callable=AsyncMock, return_value=10) as mock_sync:
|
||||
patch('src.server.services.initialization_service.os.path.isdir', return_value=True):
|
||||
mock_settings.anime_directory = "/path/to/anime"
|
||||
result = await _sync_anime_folders(progress_service=mock_progress)
|
||||
|
||||
assert result == 10
|
||||
assert result == 0
|
||||
# Verify progress updates were called
|
||||
assert mock_progress.update_progress.call_count == 2
|
||||
mock_progress.update_progress.assert_any_call(
|
||||
@@ -194,7 +189,7 @@ class TestSyncAnimeFolders:
|
||||
mock_progress.update_progress.assert_any_call(
|
||||
progress_id="series_sync",
|
||||
current=75,
|
||||
message="Synced 10 series from data files",
|
||||
message="Series loaded directly from database",
|
||||
metadata={"step_id": "series_sync"}
|
||||
)
|
||||
|
||||
@@ -537,6 +532,8 @@ class TestPerformNFOScan:
|
||||
new_callable=AsyncMock, return_value=False), \
|
||||
patch('src.server.services.initialization_service._is_nfo_scan_configured',
|
||||
new_callable=AsyncMock, return_value=True), \
|
||||
patch('src.server.services.initialization_service._load_series_into_memory',
|
||||
new_callable=AsyncMock), \
|
||||
patch('src.server.services.initialization_service._execute_nfo_scan',
|
||||
new_callable=AsyncMock), \
|
||||
patch('src.server.services.initialization_service._mark_nfo_scan_completed',
|
||||
@@ -554,6 +551,8 @@ class TestPerformNFOScan:
|
||||
new_callable=AsyncMock, return_value=False), \
|
||||
patch('src.server.services.initialization_service._is_nfo_scan_configured',
|
||||
new_callable=AsyncMock, return_value=True), \
|
||||
patch('src.server.services.initialization_service._load_series_into_memory',
|
||||
new_callable=AsyncMock), \
|
||||
patch('src.server.services.initialization_service._execute_nfo_scan',
|
||||
new_callable=AsyncMock), \
|
||||
patch('src.server.services.initialization_service._mark_nfo_scan_completed',
|
||||
|
||||
Reference in New Issue
Block a user