Compare commits

...

11 Commits

Author SHA1 Message Date
de330dc146 chore: bump version 2026-06-11 08:45:36 +02:00
4731fd644a fix(tests): resolve 13 failing unit tests
- Use dynamic APP_VERSION instead of hardcoded v1.3.6 in:
  test_template_helpers, test_health, test_page_controller
- Add unresolved_folders to EXPECTED_TABLES in database/init.py
- Fix shallow copy bug in test_serie_scanner.py episodeDict comparison
- Update test_schema_constants to expect 6 tables instead of 5
2026-06-11 08:36:41 +02:00
9d52ff0c45 fix: use async context manager for TMDBClient to prevent resource leak
The TMDBClient was being instantiated but never closed, causing 'Unclosed
client session' errors in the logs. Fixed by using 'async with' context
manager which properly calls close() on exit.

Changes:
- _lookup_tmdb_id_by_name: wrapped client in async with
- _fetch_tmdb_data: wrapped client in async with
2026-06-11 08:03:03 +02:00
ee5d719f37 fix(scheduler): add to_dict to AnimeSeries for auto-download
AnimeSeries objects returned by SerieList.GetMissingEpisode() lacked
to_dict(), causing AttributeError when _run_auto_download() called
series.get("episodeDict").
2026-06-11 08:02:27 +02:00
cbc44491e7 chore: bump version 2026-06-10 20:14:41 +02:00
e319cfecb8 fix: add episodeDict setter to AnimeSeries model
SerieScanner attempted to assign serie.episodeDict = missing_episodes
but the property had no setter, causing AttributeError during scan.

Added setter that stores value in _episode_dict_cache, which the getter
already checks. This allows SerieScanner to update episodeDict directly.
2026-06-10 20:14:15 +02:00
4f61ded92a chore: bump version 2026-06-10 19:17:39 +02:00
d6082b5cf6 fix: ensure series loaded from DB before NFO scan
- Call _load_series_into_memory() before NFO scan phases to sync DB
  to SeriesApp memory, fixing missing NFO for recently resolved folders
- Add TMDB lookup for series without cached tmdb_id during NFO creation
- Add get_tmdb_client() factory and get_tmdb_image_base_url() helpers
- Fix: use get_tv_show_details instead of deprecated get_series_details
- Fix tests: mock _load_series_into_memory in NFO scan tests
2026-06-10 18:49:53 +02:00
e76cd3a708 test: remove sync_legacy_series_to_db tests
- Removed TestSyncSeriesFromDataFiles class from test_anime_service.py
- Updated TestSyncAnimeFolders tests to expect sync_count=0
- Removed TestSyncSeriesToDatabase class from test_data_file_db_sync.py
2026-06-10 18:26:09 +02:00
08f7f7453c refactor: remove legacy data file sync functionality
Series now loaded directly from database. Removed:
- sync_legacy_series_to_db() from anime_service.py
- Corresponding sync call after directory update in config.py
- Safety nets in initialization_service.py for missing progress IDs
2026-06-10 18:23:01 +02:00
023ddd182f fix(initialization): remove duplicate nfo_scan progress completion
The nfo_scan_completed event handler was calling complete_progress()
which removed the progress before _execute_nfo_scan returned. This caused
perform_nfo_scan_phase to fail with 'Progress with id nfo_scan not found'
when it tried to complete the same progress.

Completion is now only handled by perform_nfo_scan_phase after
_execute_nfo_scan returns, as intended.
2026-06-10 18:20:04 +02:00
18 changed files with 189 additions and 420 deletions

View File

@@ -1 +1 @@
v1.4.14
v1.4.17

View File

@@ -4,4 +4,5 @@ API key : 299ae8f630a31bda814263c551361448
/setup
SeriesApp initialized for directory:
SeriesApp initialized for directory:
to remove:

View File

@@ -1,6 +1,6 @@
{
"name": "aniworld-web",
"version": "1.4.14",
"version": "1.4.17",
"description": "Aniworld Anime Download Manager - Web Frontend",
"type": "module",
"scripts": {

View File

@@ -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

View File

@@ -37,6 +37,7 @@ EXPECTED_TABLES = {
"download_queue",
"user_sessions",
"system_settings",
"unresolved_folders",
}
# Expected indexes for performance

View File

@@ -13,7 +13,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from typing import List, Optional
from typing import Any, Dict, List, Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
@@ -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.
@@ -238,6 +247,21 @@ class AnimeSeries(Base, TimestampMixin):
except ValueError:
return sanitize_folder_name(self.key)
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary for cache serialization.
Returns:
Dictionary with series data including episodeDict for
auto-download functionality.
"""
return {
"key": self.key,
"name": self.name,
"site": self.site,
"folder": self.folder,
"episodeDict": self.episodeDict,
}
class Episode(Base, TimestampMixin):
"""SQLAlchemy model for anime episodes.

View File

@@ -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/"

View File

@@ -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

View File

@@ -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:

View File

@@ -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
async with get_tmdb_client() as 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.
@@ -538,8 +601,8 @@ class NfoScanService:
try:
from src.server.nfo.tmdb_client import get_tmdb_client
client = get_tmdb_client()
data = await client.get_series_details(tmdb_id)
async with get_tmdb_client() as client:
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)

View File

@@ -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."""

View File

@@ -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"

View File

@@ -473,12 +473,13 @@ async def test_validate_schema_with_inspection_error():
def test_schema_constants():
"""Test that schema constants are properly defined."""
assert CURRENT_SCHEMA_VERSION == "1.0.1"
assert len(EXPECTED_TABLES) == 5
assert len(EXPECTED_TABLES) == 6
assert "anime_series" in EXPECTED_TABLES
assert "episodes" in EXPECTED_TABLES
assert "download_queue" in EXPECTED_TABLES
assert "user_sessions" in EXPECTED_TABLES
assert "system_settings" in EXPECTED_TABLES
assert "unresolved_folders" in EXPECTED_TABLES
if __name__ == "__main__":

View File

@@ -14,6 +14,7 @@ from src.server.api.health import (
get_system_metrics,
ready_check,
)
from src.server.utils.version import APP_VERSION
@pytest.mark.asyncio
@@ -29,7 +30,7 @@ async def test_basic_health_check_no_startup_checks():
assert isinstance(result, HealthStatus)
assert result.status == "healthy"
assert result.version == "v1.3.6"
assert result.version == APP_VERSION
assert result.service == "aniworld-api"
assert result.timestamp is not None
assert result.series_app_initialized is False

View File

@@ -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',

View File

@@ -180,6 +180,7 @@ class TestTemplateHelpers:
def test_get_base_context(self):
"""Test getting base context."""
from src.server.utils.template_helpers import get_base_context
from src.server.utils.version import APP_VERSION
mock_request = MagicMock(spec=Request)
context = get_base_context(mock_request, "Test Title")
@@ -187,7 +188,7 @@ class TestTemplateHelpers:
assert context["request"] == mock_request
assert context["title"] == "Test Title"
assert context["app_name"] == "Aniworld Download Manager"
assert context["version"] == "v1.3.6"
assert context["version"] == APP_VERSION
def test_get_base_context_default_title(self):
"""Test getting base context with default title."""

View File

@@ -199,7 +199,9 @@ class TestSerieScannerSingleSeries:
# Pre-populate keyDict
scanner.keyDict[sample_serie.key] = sample_serie
old_episode_dict = sample_serie.episodeDict.copy()
# Use deepcopy because episodeDict is modified in-place
import copy
old_episode_dict = copy.deepcopy(sample_serie.episodeDict)
with patch.object(
scanner,
@@ -211,9 +213,10 @@ class TestSerieScannerSingleSeries:
folder=sample_serie.folder
)
# Verify existing entry was updated
# Verify existing entry was updated - episodeDict is merged (not replaced)
# Old episodes [2, 3, 4] + new episodes [10, 11, 12] = merged result
assert scanner.keyDict[sample_serie.key].episodeDict != old_episode_dict
assert scanner.keyDict[sample_serie.key].episodeDict == {1: [10, 11, 12]}
assert scanner.keyDict[sample_serie.key].episodeDict == {1: [2, 3, 4, 10, 11, 12]}
def test_scan_single_series_empty_key_raises_error(
self, temp_directory, mock_loader

View File

@@ -16,6 +16,7 @@ from src.server.utils.template_helpers import (
prepare_series_context,
validate_template_exists,
)
from src.server.utils.version import APP_VERSION
class TestTemplateHelpers:
@@ -30,7 +31,7 @@ class TestTemplateHelpers:
assert context["request"] == request
assert context["title"] == "Test Title"
assert context["app_name"] == "Aniworld Download Manager"
assert context["version"] == "v1.3.6"
assert context["version"] == APP_VERSION
def test_get_base_context_default_title(self):
"""Test that default title is used."""