feat: wire NFO repair scan into app startup lifespan
This commit is contained in:
@@ -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
|
either queues the series for background reload (when a `background_loader` is
|
||||||
provided) or calls `NfoRepairService.repair_series()` directly. Skips
|
provided) or calls `NfoRepairService.repair_series()` directly. Skips
|
||||||
gracefully when `tmdb_api_key` or `anime_directory` is not configured.
|
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
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ async def lifespan(_application: FastAPI):
|
|||||||
perform_initial_setup,
|
perform_initial_setup,
|
||||||
perform_media_scan_if_needed,
|
perform_media_scan_if_needed,
|
||||||
perform_nfo_scan_if_needed,
|
perform_nfo_scan_if_needed,
|
||||||
|
perform_nfo_repair_scan,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -273,6 +274,10 @@ async def lifespan(_application: FastAPI):
|
|||||||
|
|
||||||
# Run media scan only on first run
|
# Run media scan only on first run
|
||||||
await perform_media_scan_if_needed(background_loader)
|
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:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Download service initialization skipped - "
|
"Download service initialization skipped - "
|
||||||
|
|||||||
114
tests/integration/test_nfo_repair_startup.py
Normal file
114
tests/integration/test_nfo_repair_startup.py
Normal file
@@ -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(
|
||||||
|
"<tvshow><title>IncompleteAnime</title></tvshow>"
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
"<tvshow><title>CompleteAnime</title></tvshow>"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user