fix: always repair NFO via update_tvshow_nfo so plot is written
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
"""Centralized initialization service for application startup and setup."""
|
"""Centralized initialization service for application startup and setup."""
|
||||||
|
import asyncio
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
@@ -381,13 +382,16 @@ async def perform_nfo_repair_scan(background_loader=None) -> None:
|
|||||||
|
|
||||||
Runs on every application startup (not guarded by a run-once DB flag).
|
Runs on every application startup (not guarded by a run-once DB flag).
|
||||||
Checks each subfolder of ``settings.anime_directory`` for a ``tvshow.nfo``
|
Checks each subfolder of ``settings.anime_directory`` for a ``tvshow.nfo``
|
||||||
and queues a repair via *background_loader* when required tags are absent
|
and calls ``NfoRepairService.repair_series`` for every file with absent or
|
||||||
or empty, or runs repairs inline when no loader is provided.
|
empty required tags. Repairs are fired as independent asyncio tasks so
|
||||||
|
they do not block the startup sequence.
|
||||||
|
|
||||||
|
The ``background_loader`` parameter is accepted for backwards-compatibility
|
||||||
|
but is no longer used — repairs always go through ``update_tvshow_nfo`` so
|
||||||
|
that the TMDB overview (plot) and all other required tags are written.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
background_loader: Optional BackgroundLoaderService. When provided,
|
background_loader: Unused. Kept to avoid breaking call-sites.
|
||||||
deficient series are queued non-blocking. When None, repairs
|
|
||||||
execute inline (useful in tests).
|
|
||||||
"""
|
"""
|
||||||
from src.core.services.nfo_factory import NFOServiceFactory
|
from src.core.services.nfo_factory import NFOServiceFactory
|
||||||
from src.core.services.nfo_repair_service import NfoRepairService, nfo_needs_repair
|
from src.core.services.nfo_repair_service import NfoRepairService, nfo_needs_repair
|
||||||
@@ -420,14 +424,13 @@ async def perform_nfo_repair_scan(background_loader=None) -> None:
|
|||||||
series_name = series_dir.name
|
series_name = series_dir.name
|
||||||
if nfo_needs_repair(nfo_path):
|
if nfo_needs_repair(nfo_path):
|
||||||
queued += 1
|
queued += 1
|
||||||
if background_loader is not None:
|
# Always repair via update_tvshow_nfo so that missing fields such
|
||||||
await background_loader.add_series_loading_task(
|
# as `plot` are fetched from TMDB. Fire as an asyncio task to
|
||||||
key=series_name,
|
# avoid blocking the startup loop.
|
||||||
folder=series_name,
|
asyncio.create_task(
|
||||||
name=series_name,
|
repair_service.repair_series(series_dir, series_name),
|
||||||
)
|
name=f"nfo_repair:{series_name}",
|
||||||
else:
|
)
|
||||||
await repair_service.repair_series(series_dir, series_name)
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"NFO repair scan complete: %d of %d series queued for repair",
|
"NFO repair scan complete: %d of %d series queued for repair",
|
||||||
|
|||||||
@@ -48,11 +48,10 @@ class TestNfoRepairScanIntegrationWithBackgroundLoader:
|
|||||||
"""Integration test: incomplete NFO series are queued via background_loader."""
|
"""Integration test: incomplete NFO series are queued via background_loader."""
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_incomplete_nfo_series_queued(self, tmp_path):
|
async def test_incomplete_nfo_series_scheduled_for_repair(self, tmp_path):
|
||||||
"""Series whose tvshow.nfo is missing required tags get queued."""
|
"""Series whose tvshow.nfo is missing required tags are scheduled via asyncio.create_task."""
|
||||||
from src.server.services.initialization_service import perform_nfo_repair_scan
|
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 = tmp_path / "IncompleteAnime"
|
||||||
series_dir.mkdir()
|
series_dir.mkdir()
|
||||||
(series_dir / "tvshow.nfo").write_text(
|
(series_dir / "tvshow.nfo").write_text(
|
||||||
@@ -63,8 +62,8 @@ class TestNfoRepairScanIntegrationWithBackgroundLoader:
|
|||||||
mock_settings.tmdb_api_key = "test-key"
|
mock_settings.tmdb_api_key = "test-key"
|
||||||
mock_settings.anime_directory = str(tmp_path)
|
mock_settings.anime_directory = str(tmp_path)
|
||||||
|
|
||||||
mock_loader = AsyncMock()
|
mock_repair_service = AsyncMock()
|
||||||
mock_loader.add_series_loading_task = AsyncMock()
|
mock_repair_service.repair_series = AsyncMock(return_value=True)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"src.server.services.initialization_service.settings", mock_settings
|
"src.server.services.initialization_service.settings", mock_settings
|
||||||
@@ -73,19 +72,20 @@ class TestNfoRepairScanIntegrationWithBackgroundLoader:
|
|||||||
return_value=True,
|
return_value=True,
|
||||||
), patch(
|
), patch(
|
||||||
"src.core.services.nfo_factory.NFOServiceFactory"
|
"src.core.services.nfo_factory.NFOServiceFactory"
|
||||||
) as mock_factory:
|
) as mock_factory, patch(
|
||||||
|
"src.core.services.nfo_repair_service.NfoRepairService",
|
||||||
|
return_value=mock_repair_service,
|
||||||
|
), patch(
|
||||||
|
"asyncio.create_task"
|
||||||
|
) as mock_create_task:
|
||||||
mock_factory.return_value.create.return_value = MagicMock()
|
mock_factory.return_value.create.return_value = MagicMock()
|
||||||
await perform_nfo_repair_scan(background_loader=mock_loader)
|
await perform_nfo_repair_scan(background_loader=AsyncMock())
|
||||||
|
|
||||||
mock_loader.add_series_loading_task.assert_called_once_with(
|
mock_create_task.assert_called_once()
|
||||||
key="IncompleteAnime",
|
|
||||||
folder="IncompleteAnime",
|
|
||||||
name="IncompleteAnime",
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_complete_nfo_series_not_queued(self, tmp_path):
|
async def test_complete_nfo_series_not_scheduled(self, tmp_path):
|
||||||
"""Series whose tvshow.nfo has all required tags are not queued."""
|
"""Series whose tvshow.nfo has all required tags are not scheduled for repair."""
|
||||||
from src.server.services.initialization_service import perform_nfo_repair_scan
|
from src.server.services.initialization_service import perform_nfo_repair_scan
|
||||||
|
|
||||||
series_dir = tmp_path / "CompleteAnime"
|
series_dir = tmp_path / "CompleteAnime"
|
||||||
@@ -98,9 +98,6 @@ class TestNfoRepairScanIntegrationWithBackgroundLoader:
|
|||||||
mock_settings.tmdb_api_key = "test-key"
|
mock_settings.tmdb_api_key = "test-key"
|
||||||
mock_settings.anime_directory = str(tmp_path)
|
mock_settings.anime_directory = str(tmp_path)
|
||||||
|
|
||||||
mock_loader = AsyncMock()
|
|
||||||
mock_loader.add_series_loading_task = AsyncMock()
|
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"src.server.services.initialization_service.settings", mock_settings
|
"src.server.services.initialization_service.settings", mock_settings
|
||||||
), patch(
|
), patch(
|
||||||
@@ -108,8 +105,10 @@ class TestNfoRepairScanIntegrationWithBackgroundLoader:
|
|||||||
return_value=False,
|
return_value=False,
|
||||||
), patch(
|
), patch(
|
||||||
"src.core.services.nfo_factory.NFOServiceFactory"
|
"src.core.services.nfo_factory.NFOServiceFactory"
|
||||||
) as mock_factory:
|
) as mock_factory, patch(
|
||||||
|
"asyncio.create_task"
|
||||||
|
) as mock_create_task:
|
||||||
mock_factory.return_value.create.return_value = MagicMock()
|
mock_factory.return_value.create.return_value = MagicMock()
|
||||||
await perform_nfo_repair_scan(background_loader=mock_loader)
|
await perform_nfo_repair_scan(background_loader=AsyncMock())
|
||||||
|
|
||||||
mock_loader.add_series_loading_task.assert_not_called()
|
mock_create_task.assert_not_called()
|
||||||
|
|||||||
@@ -790,9 +790,8 @@ class TestPerformNfoRepairScan:
|
|||||||
await perform_nfo_repair_scan()
|
await perform_nfo_repair_scan()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_queues_deficient_series_via_background_loader(self, tmp_path):
|
async def test_queues_deficient_series_as_asyncio_task(self, tmp_path):
|
||||||
"""Series with incomplete NFO should be queued via background_loader."""
|
"""Series with incomplete NFO should be scheduled via asyncio.create_task."""
|
||||||
# Create a fake series directory with a tvshow.nfo file
|
|
||||||
series_dir = tmp_path / "MyAnime"
|
series_dir = tmp_path / "MyAnime"
|
||||||
series_dir.mkdir()
|
series_dir.mkdir()
|
||||||
nfo_file = series_dir / "tvshow.nfo"
|
nfo_file = series_dir / "tvshow.nfo"
|
||||||
@@ -802,8 +801,8 @@ class TestPerformNfoRepairScan:
|
|||||||
mock_settings.tmdb_api_key = "test-key"
|
mock_settings.tmdb_api_key = "test-key"
|
||||||
mock_settings.anime_directory = str(tmp_path)
|
mock_settings.anime_directory = str(tmp_path)
|
||||||
|
|
||||||
mock_background_loader = AsyncMock()
|
mock_repair_service = AsyncMock()
|
||||||
mock_background_loader.add_series_loading_task = AsyncMock()
|
mock_repair_service.repair_series = AsyncMock(return_value=True)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"src.server.services.initialization_service.settings", mock_settings
|
"src.server.services.initialization_service.settings", mock_settings
|
||||||
@@ -812,17 +811,20 @@ class TestPerformNfoRepairScan:
|
|||||||
return_value=True,
|
return_value=True,
|
||||||
), patch(
|
), patch(
|
||||||
"src.core.services.nfo_factory.NFOServiceFactory"
|
"src.core.services.nfo_factory.NFOServiceFactory"
|
||||||
) as mock_factory_cls:
|
) as mock_factory_cls, patch(
|
||||||
|
"src.core.services.nfo_repair_service.NfoRepairService",
|
||||||
|
return_value=mock_repair_service,
|
||||||
|
), patch(
|
||||||
|
"asyncio.create_task"
|
||||||
|
) as mock_create_task:
|
||||||
mock_factory_cls.return_value.create.return_value = MagicMock()
|
mock_factory_cls.return_value.create.return_value = MagicMock()
|
||||||
await perform_nfo_repair_scan(background_loader=mock_background_loader)
|
await perform_nfo_repair_scan(background_loader=AsyncMock())
|
||||||
|
|
||||||
mock_background_loader.add_series_loading_task.assert_called_once_with(
|
mock_create_task.assert_called_once()
|
||||||
key="MyAnime", folder="MyAnime", name="MyAnime"
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_skips_complete_series(self, tmp_path):
|
async def test_skips_complete_series(self, tmp_path):
|
||||||
"""Series with complete NFO should not be queued or repaired."""
|
"""Series with complete NFO should not be scheduled for repair."""
|
||||||
series_dir = tmp_path / "CompleteAnime"
|
series_dir = tmp_path / "CompleteAnime"
|
||||||
series_dir.mkdir()
|
series_dir.mkdir()
|
||||||
nfo_file = series_dir / "tvshow.nfo"
|
nfo_file = series_dir / "tvshow.nfo"
|
||||||
@@ -832,9 +834,6 @@ class TestPerformNfoRepairScan:
|
|||||||
mock_settings.tmdb_api_key = "test-key"
|
mock_settings.tmdb_api_key = "test-key"
|
||||||
mock_settings.anime_directory = str(tmp_path)
|
mock_settings.anime_directory = str(tmp_path)
|
||||||
|
|
||||||
mock_background_loader = AsyncMock()
|
|
||||||
mock_background_loader.add_series_loading_task = AsyncMock()
|
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"src.server.services.initialization_service.settings", mock_settings
|
"src.server.services.initialization_service.settings", mock_settings
|
||||||
), patch(
|
), patch(
|
||||||
@@ -842,15 +841,17 @@ class TestPerformNfoRepairScan:
|
|||||||
return_value=False,
|
return_value=False,
|
||||||
), patch(
|
), patch(
|
||||||
"src.core.services.nfo_factory.NFOServiceFactory"
|
"src.core.services.nfo_factory.NFOServiceFactory"
|
||||||
) as mock_factory_cls:
|
) as mock_factory_cls, patch(
|
||||||
|
"asyncio.create_task"
|
||||||
|
) as mock_create_task:
|
||||||
mock_factory_cls.return_value.create.return_value = MagicMock()
|
mock_factory_cls.return_value.create.return_value = MagicMock()
|
||||||
await perform_nfo_repair_scan(background_loader=mock_background_loader)
|
await perform_nfo_repair_scan(background_loader=AsyncMock())
|
||||||
|
|
||||||
mock_background_loader.add_series_loading_task.assert_not_called()
|
mock_create_task.assert_not_called()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_repairs_directly_without_background_loader(self, tmp_path):
|
async def test_repairs_via_asyncio_task_without_background_loader(self, tmp_path):
|
||||||
"""When no background_loader provided, repair_series is called directly."""
|
"""When no background_loader provided, repair is still scheduled via asyncio.create_task."""
|
||||||
series_dir = tmp_path / "NeedsRepair"
|
series_dir = tmp_path / "NeedsRepair"
|
||||||
series_dir.mkdir()
|
series_dir.mkdir()
|
||||||
nfo_file = series_dir / "tvshow.nfo"
|
nfo_file = series_dir / "tvshow.nfo"
|
||||||
@@ -873,8 +874,10 @@ class TestPerformNfoRepairScan:
|
|||||||
) as mock_factory_cls, patch(
|
) as mock_factory_cls, patch(
|
||||||
"src.core.services.nfo_repair_service.NfoRepairService",
|
"src.core.services.nfo_repair_service.NfoRepairService",
|
||||||
return_value=mock_repair_service,
|
return_value=mock_repair_service,
|
||||||
):
|
), patch(
|
||||||
|
"asyncio.create_task"
|
||||||
|
) as mock_create_task:
|
||||||
mock_factory_cls.return_value.create.return_value = MagicMock()
|
mock_factory_cls.return_value.create.return_value = MagicMock()
|
||||||
await perform_nfo_repair_scan(background_loader=None)
|
await perform_nfo_repair_scan(background_loader=None)
|
||||||
|
|
||||||
mock_repair_service.repair_series.assert_called_once()
|
mock_create_task.assert_called_once()
|
||||||
|
|||||||
Reference in New Issue
Block a user