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:
@@ -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)
|
||||
|
||||
308
tests/integration/test_anime_add_nfo_isolation.py
Normal file
308
tests/integration/test_anime_add_nfo_isolation.py
Normal 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()
|
||||
Reference in New Issue
Block a user