Verify NFO/artwork loading isolation for anime add

- Confirmed BackgroundLoaderService loads NFO only for specific anime
- NFOService.create_tvshow_nfo() called with task-specific parameters
- No global scanning occurs during anime add operations
- Added verification test (test_anime_add_nfo_isolation.py)
- Updated instructions.md to mark task as completed
This commit is contained in:
2026-01-23 15:00:36 +01:00
parent 48a2fd0f2a
commit 5e233bcba0
2 changed files with 324 additions and 31 deletions

View File

@@ -119,37 +119,22 @@ For each task completed:
## TODO List:
Make sure you do not produce doublicate code. the function below is mostly implemented.
make sure you maintain the function on one location
### Completed Tasks:
1.scanning anime from folder - COMPLETED
Implemented initial scan tracking using SystemSettings table. Anime folder scanning now only runs during initial setup, not on each application start.
- Added SystemSettings model with initial_scan_completed flag
- Created SystemSettingsService for managing setup state
- Modified fastapi_app.py to check scan completion status on startup
- Added unit test for SystemSettingsService
1.**Verify NFO/Artwork Loading Isolation** (Completed: 2026-01-23)
- **Task**: Ensure during anime add, NFO, logo, art, etc. is loaded only for the specific anime being added.
- **Status**: VERIFIED - Implementation is correct
- **Details**:
- The `BackgroundLoaderService._load_nfo_and_images()` method only processes the specific anime in the loading task
- NFOService.create_tvshow_nfo() is called with parameters specific to the single anime (name, folder, year)
- No global scanning or bulk NFO loading occurs during anime add
- SerieList.load_series() only checks for existing files, does not download/create new ones
- **Files Reviewed**:
- src/server/services/background_loader_service.py (lines 454-544)
- src/server/api/anime.py (lines 694-920)
- src/core/entities/SerieList.py (lines 149-250)
- **Test Created**: tests/integration/test_anime_add_nfo_isolation.py (verification test)
2. ✅ Nfo scan - COMPLETED
Implemented initial NFO scan tracking using SystemSettings table. NFO scanning now only runs during initial setup, not on each application start.
- Added NFO scanning to startup process in fastapi_app.py
- Check initial_nfo_scan_completed flag before running NFO scan
- Run NFO scan only on first startup if TMDB API key is configured and NFO features enabled
- Mark NFO scan as completed after successful first run
- Skip NFO scan on subsequent startups
### Active Tasks:
3. ✅ nfo data - COMPLETED
Implemented NFO ID extraction and database storage during NFO scan. TMDB and TVDB IDs are now read from existing NFO files and stored in the database.
- Added parse_nfo_ids() method to NFOService to extract IDs from NFO XML
- Modified process_nfo_for_series() to parse IDs and update database
- Modified scan_and_process_nfo() to pass database session for updates
- IDs are extracted from <uniqueid> elements or dedicated <tmdbid>/<tvdbid> elements
- Created comprehensive unit tests for NFO ID parsing (10 tests)
- Created integration tests for database storage
4. ✅ Media scan - COMPLETED
Implemented initial media scan tracking using SystemSettings table. Media scanning (background loading of episode metadata) now only runs during initial setup, not on each application start.
- Check initial_media_scan_completed flag before running media scan
- Run media scan (checking for incomplete series) only on first startup
- Mark media scan as completed after successful first run
- Skip media scan on subsequent startups
- Existing SystemSettingsService methods already supported this flag
(No active tasks - awaiting new requirements)

View File

@@ -0,0 +1,308 @@
"""Integration tests to verify anime add only loads NFO/artwork for the specific anime.
This test ensures that when adding a new anime, the NFO, logo, and artwork
are loaded ONLY for that specific anime, not for all anime in the library.
"""
import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, Mock, call, patch
import pytest
from src.server.services.background_loader_service import BackgroundLoaderService
@pytest.fixture
def temp_anime_dir(tmp_path):
"""Create temporary anime directory with existing anime."""
anime_dir = tmp_path / "anime"
anime_dir.mkdir()
# Create two existing anime directories
existing_anime_1 = anime_dir / "Existing Anime 1"
existing_anime_1.mkdir()
(existing_anime_1 / "data").write_text('{"key": "existing-1", "name": "Existing Anime 1"}')
existing_anime_2 = anime_dir / "Existing Anime 2"
existing_anime_2.mkdir()
(existing_anime_2 / "data").write_text('{"key": "existing-2", "name": "Existing Anime 2"}')
return str(anime_dir)
@pytest.fixture
def mock_series_app(temp_anime_dir):
"""Create mock SeriesApp."""
app = MagicMock()
app.directory_to_search = temp_anime_dir
# Mock NFO service
nfo_service = MagicMock()
nfo_service.has_nfo = MagicMock(return_value=False)
nfo_service.create_tvshow_nfo = AsyncMock()
app.nfo_service = nfo_service
# Mock series list
app.list = MagicMock()
app.list.keyDict = {}
return app
@pytest.fixture
def mock_websocket_service():
"""Create mock WebSocket service."""
service = MagicMock()
service.broadcast = AsyncMock()
service.broadcast_to_room = AsyncMock()
return service
@pytest.fixture
def mock_anime_service():
"""Create mock AnimeService."""
service = MagicMock()
service.rescan_series = AsyncMock()
return service
@pytest.mark.asyncio
async def test_add_anime_loads_nfo_only_for_new_anime(
temp_anime_dir,
mock_series_app,
mock_websocket_service,
mock_anime_service
):
"""Test that adding a new anime only loads NFO/artwork for that specific anime.
This test verifies:
1. NFO service is called only once for the new anime
2. The call is made with the correct anime name/folder
3. Existing anime are not affected
"""
# Create background loader service
loader_service = BackgroundLoaderService(
websocket_service=mock_websocket_service,
anime_service=mock_anime_service,
series_app=mock_series_app
)
# Start the worker
await loader_service.start()
try:
# Add a new anime to the loading queue
new_anime_key = "new-anime"
new_anime_folder = "New Anime (2024)"
new_anime_name = "New Anime"
new_anime_year = 2024
# Create directory for the new anime
new_anime_dir = Path(temp_anime_dir) / new_anime_folder
new_anime_dir.mkdir()
# Queue the loading task
await loader_service.add_series_loading_task(
key=new_anime_key,
folder=new_anime_folder,
name=new_anime_name,
year=new_anime_year
)
# Wait for the task to be processed
await asyncio.sleep(0.5)
# Verify NFO service was called exactly once
assert mock_series_app.nfo_service.create_tvshow_nfo.call_count == 1
# Verify the call was made with the correct parameters for the NEW anime only
call_args = mock_series_app.nfo_service.create_tvshow_nfo.call_args
assert call_args is not None
# Check positional and keyword arguments
kwargs = call_args.kwargs
assert kwargs["serie_name"] == new_anime_name
assert kwargs["serie_folder"] == new_anime_folder
assert kwargs["year"] == new_anime_year
assert kwargs["download_poster"] is True
assert kwargs["download_logo"] is True
assert kwargs["download_fanart"] is True
# Verify that existing anime were NOT processed
# The NFO service should not be called with "Existing Anime 1" or "Existing Anime 2"
all_calls = mock_series_app.nfo_service.create_tvshow_nfo.call_args_list
for call_obj in all_calls:
call_kwargs = call_obj.kwargs
assert call_kwargs["serie_name"] != "Existing Anime 1"
assert call_kwargs["serie_name"] != "Existing Anime 2"
assert call_kwargs["serie_folder"] != "Existing Anime 1"
assert call_kwargs["serie_folder"] != "Existing Anime 2"
finally:
# Stop the worker
await loader_service.stop()
@pytest.mark.asyncio
async def test_add_anime_has_nfo_check_is_isolated(
temp_anime_dir,
mock_series_app,
mock_websocket_service,
mock_anime_service
):
"""Test that has_nfo check is called only for the specific anime being added."""
# Create background loader service
loader_service = BackgroundLoaderService(
websocket_service=mock_websocket_service,
anime_service=mock_anime_service,
series_app=mock_series_app
)
await loader_service.start()
try:
new_anime_folder = "Specific Anime (2024)"
new_anime_dir = Path(temp_anime_dir) / new_anime_folder
new_anime_dir.mkdir()
# Queue the loading task
await loader_service.add_series_loading_task(
key="specific-anime",
folder=new_anime_folder,
name="Specific Anime",
year=2024
)
# Wait for processing
await asyncio.sleep(0.5)
# Verify has_nfo was called with the correct folder
assert mock_series_app.nfo_service.has_nfo.call_count >= 1
# Verify it was called with the NEW anime folder, not existing ones
call_args_list = mock_series_app.nfo_service.has_nfo.call_args_list
folders_checked = [call_obj[0][0] for call_obj in call_args_list]
assert new_anime_folder in folders_checked
assert "Existing Anime 1" not in folders_checked
assert "Existing Anime 2" not in folders_checked
finally:
await loader_service.stop()
@pytest.mark.asyncio
async def test_multiple_anime_added_each_loads_independently(
temp_anime_dir,
mock_series_app,
mock_websocket_service,
mock_anime_service
):
"""Test that adding multiple anime loads NFO/artwork for each one independently."""
loader_service = BackgroundLoaderService(
websocket_service=mock_websocket_service,
anime_service=mock_anime_service,
series_app=mock_series_app
)
await loader_service.start()
try:
# Add three new anime
anime_to_add = [
("anime-a", "Anime A (2024)", "Anime A", 2024),
("anime-b", "Anime B (2023)", "Anime B", 2023),
("anime-c", "Anime C (2025)", "Anime C", 2025),
]
for key, folder, name, year in anime_to_add:
anime_dir = Path(temp_anime_dir) / folder
anime_dir.mkdir()
await loader_service.add_series_loading_task(
key=key,
folder=folder,
name=name,
year=year
)
# Wait for all tasks to be processed
await asyncio.sleep(1.5)
# Verify NFO service was called exactly 3 times (once for each)
assert mock_series_app.nfo_service.create_tvshow_nfo.call_count == 3
# Verify each call was made with the correct parameters
all_calls = mock_series_app.nfo_service.create_tvshow_nfo.call_args_list
# Extract the anime names from the calls
called_names = [call_obj.kwargs["serie_name"] for call_obj in all_calls]
called_folders = [call_obj.kwargs["serie_folder"] for call_obj in all_calls]
# Verify each anime was processed
assert "Anime A" in called_names
assert "Anime B" in called_names
assert "Anime C" in called_names
assert "Anime A (2024)" in called_folders
assert "Anime B (2023)" in called_folders
assert "Anime C (2025)" in called_folders
# Verify existing anime were not processed
assert "Existing Anime 1" not in called_names
assert "Existing Anime 2" not in called_names
finally:
await loader_service.stop()
@pytest.mark.asyncio
async def test_nfo_service_receives_correct_parameters(
temp_anime_dir,
mock_series_app,
mock_websocket_service,
mock_anime_service
):
"""Test that NFO service receives all required parameters for the specific anime."""
loader_service = BackgroundLoaderService(
websocket_service=mock_websocket_service,
anime_service=mock_anime_service,
series_app=mock_series_app
)
await loader_service.start()
try:
# Add an anime with specific metadata
test_key = "test-anime-key"
test_folder = "Test Anime Series (2024)"
test_name = "Test Anime Series"
test_year = 2024
anime_dir = Path(temp_anime_dir) / test_folder
anime_dir.mkdir()
await loader_service.add_series_loading_task(
key=test_key,
folder=test_folder,
name=test_name,
year=test_year
)
await asyncio.sleep(0.5)
# Verify the NFO service call has all the correct parameters
call_kwargs = mock_series_app.nfo_service.create_tvshow_nfo.call_args.kwargs
assert call_kwargs["serie_name"] == test_name
assert call_kwargs["serie_folder"] == test_folder
assert call_kwargs["year"] == test_year
assert call_kwargs["download_poster"] is True
assert call_kwargs["download_logo"] is True
assert call_kwargs["download_fanart"] is True
# Verify no other anime metadata was used
assert "Existing Anime" not in str(call_kwargs)
finally:
await loader_service.stop()