diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index d9b1b74..70df01a 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -62,6 +62,10 @@ This changelog follows [Keep a Changelog](https://keepachangelog.com/) principle
either queues the series for background reload (when a `background_loader` is
provided) or calls `NfoRepairService.repair_series()` directly. Skips
gracefully when `tmdb_api_key` or `anime_directory` is not configured.
+- **NFO repair wired into startup lifespan (`src/server/fastapi_app.py`)**:
+ `perform_nfo_repair_scan(background_loader)` is called at the end of the
+ FastAPI lifespan startup, after `perform_media_scan_if_needed`, ensuring
+ every existing series NFO is checked and repaired on each server start.
### Changed
diff --git a/src/server/fastapi_app.py b/src/server/fastapi_app.py
index 2422051..411fc85 100644
--- a/src/server/fastapi_app.py
+++ b/src/server/fastapi_app.py
@@ -220,6 +220,7 @@ async def lifespan(_application: FastAPI):
perform_initial_setup,
perform_media_scan_if_needed,
perform_nfo_scan_if_needed,
+ perform_nfo_repair_scan,
)
try:
@@ -273,6 +274,10 @@ async def lifespan(_application: FastAPI):
# Run media scan only on first run
await perform_media_scan_if_needed(background_loader)
+
+ # Scan every series NFO on startup and repair any that are
+ # missing required tags by queuing them for background reload
+ await perform_nfo_repair_scan(background_loader)
else:
logger.info(
"Download service initialization skipped - "
diff --git a/tests/integration/test_nfo_repair_startup.py b/tests/integration/test_nfo_repair_startup.py
new file mode 100644
index 0000000..c7c0f0b
--- /dev/null
+++ b/tests/integration/test_nfo_repair_startup.py
@@ -0,0 +1,114 @@
+"""Integration tests verifying perform_nfo_repair_scan is wired into app startup.
+
+These tests confirm that:
+1. The lifespan calls perform_nfo_repair_scan after perform_media_scan_if_needed.
+2. Series with incomplete NFO files are queued via the background_loader.
+"""
+from unittest.mock import AsyncMock, MagicMock, patch, call
+
+import pytest
+
+
+class TestNfoRepairScanCalledOnStartup:
+ """Verify perform_nfo_repair_scan is invoked during the FastAPI lifespan."""
+
+ def test_perform_nfo_repair_scan_imported_in_lifespan(self):
+ """fastapi_app.py lifespan imports perform_nfo_repair_scan."""
+ import importlib
+ import src.server.fastapi_app as app_module
+
+ source = importlib.util.find_spec("src.server.fastapi_app").origin
+ with open(source, "r", encoding="utf-8") as fh:
+ content = fh.read()
+
+ assert "perform_nfo_repair_scan" in content, (
+ "perform_nfo_repair_scan must be imported and called in fastapi_app.py"
+ )
+
+ def test_perform_nfo_repair_scan_called_after_media_scan(self):
+ """perform_nfo_repair_scan must appear after perform_media_scan_if_needed."""
+ import importlib
+
+ source = importlib.util.find_spec("src.server.fastapi_app").origin
+ with open(source, "r", encoding="utf-8") as fh:
+ content = fh.read()
+
+ media_scan_pos = content.find("perform_media_scan_if_needed(background_loader)")
+ repair_scan_pos = content.find("perform_nfo_repair_scan(background_loader)")
+
+ assert media_scan_pos != -1, "perform_media_scan_if_needed call not found"
+ assert repair_scan_pos != -1, "perform_nfo_repair_scan call not found"
+ assert repair_scan_pos > media_scan_pos, (
+ "perform_nfo_repair_scan must be called AFTER perform_media_scan_if_needed"
+ )
+
+
+class TestNfoRepairScanIntegrationWithBackgroundLoader:
+ """Integration test: incomplete NFO series are queued via background_loader."""
+
+ @pytest.mark.asyncio
+ async def test_incomplete_nfo_series_queued(self, tmp_path):
+ """Series whose tvshow.nfo is missing required tags get queued."""
+ from src.server.services.initialization_service import perform_nfo_repair_scan
+
+ # Create a series directory with a minimal (incomplete) NFO
+ series_dir = tmp_path / "IncompleteAnime"
+ series_dir.mkdir()
+ (series_dir / "tvshow.nfo").write_text(
+ "IncompleteAnime"
+ )
+
+ mock_settings = MagicMock()
+ mock_settings.tmdb_api_key = "test-key"
+ mock_settings.anime_directory = str(tmp_path)
+
+ mock_loader = AsyncMock()
+ mock_loader.add_series_loading_task = AsyncMock()
+
+ with patch(
+ "src.server.services.initialization_service.settings", mock_settings
+ ), patch(
+ "src.core.services.nfo_repair_service.nfo_needs_repair",
+ return_value=True,
+ ), patch(
+ "src.core.services.nfo_factory.NFOServiceFactory"
+ ) as mock_factory:
+ mock_factory.return_value.create.return_value = MagicMock()
+ await perform_nfo_repair_scan(background_loader=mock_loader)
+
+ mock_loader.add_series_loading_task.assert_called_once_with(
+ key="IncompleteAnime",
+ folder="IncompleteAnime",
+ name="IncompleteAnime",
+ )
+
+ @pytest.mark.asyncio
+ async def test_complete_nfo_series_not_queued(self, tmp_path):
+ """Series whose tvshow.nfo has all required tags are not queued."""
+ from src.server.services.initialization_service import perform_nfo_repair_scan
+
+ series_dir = tmp_path / "CompleteAnime"
+ series_dir.mkdir()
+ (series_dir / "tvshow.nfo").write_text(
+ "CompleteAnime"
+ )
+
+ mock_settings = MagicMock()
+ mock_settings.tmdb_api_key = "test-key"
+ mock_settings.anime_directory = str(tmp_path)
+
+ mock_loader = AsyncMock()
+ mock_loader.add_series_loading_task = AsyncMock()
+
+ with patch(
+ "src.server.services.initialization_service.settings", mock_settings
+ ), patch(
+ "src.core.services.nfo_repair_service.nfo_needs_repair",
+ return_value=False,
+ ), patch(
+ "src.core.services.nfo_factory.NFOServiceFactory"
+ ) as mock_factory:
+ mock_factory.return_value.create.return_value = MagicMock()
+ await perform_nfo_repair_scan(background_loader=mock_loader)
+
+ mock_loader.add_series_loading_task.assert_not_called()