fix: isolate NFO repair sessions to prevent connector-closed errors

This commit is contained in:
2026-02-22 17:06:21 +01:00
parent ddcac5a96d
commit 1712dfd776

View File

@@ -377,24 +377,65 @@ async def perform_nfo_scan_if_needed(progress_service=None):
) )
_NFO_REPAIR_SEMAPHORE: asyncio.Semaphore = asyncio.Semaphore(3)
async def _repair_one_series(series_dir: Path, series_name: str) -> None:
"""Repair a single series NFO in isolation.
Creates a fresh :class:`NFOService` and :class:`NfoRepairService` per
invocation so that each repair owns its own ``aiohttp`` session/connector
and concurrent tasks cannot interfere with each other.
A module-level semaphore (``_NFO_REPAIR_SEMAPHORE``) limits the number of
simultaneous TMDB requests to avoid rate-limiting.
Any exception is caught and logged so the asyncio task never silently
drops an unhandled error.
Args:
series_dir: Absolute path to the series folder.
series_name: Human-readable series name for log messages.
"""
from src.core.services.nfo_factory import NFOServiceFactory
from src.core.services.nfo_repair_service import NfoRepairService
async with _NFO_REPAIR_SEMAPHORE:
try:
factory = NFOServiceFactory()
nfo_service = factory.create()
repair_service = NfoRepairService(nfo_service)
await repair_service.repair_series(series_dir, series_name)
except Exception as exc: # pylint: disable=broad-except
logger.error(
"NFO repair failed for %s: %s",
series_name,
exc,
)
async def perform_nfo_repair_scan(background_loader=None) -> None: async def perform_nfo_repair_scan(background_loader=None) -> None:
"""Scan all series folders and repair incomplete tvshow.nfo files. """Scan all series folders and repair incomplete tvshow.nfo files.
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 calls ``NfoRepairService.repair_series`` for every file with absent or and calls ``_repair_one_series`` for every file with absent or empty
empty required tags. Repairs are fired as independent asyncio tasks so required tags.
they do not block the startup sequence.
Each repair task creates its own isolated :class:`NFOService` /
:class:`TMDBClient` so concurrent tasks never share an ``aiohttp``
session — this prevents "Connector is closed" errors when many repairs
run in parallel. A semaphore caps TMDB concurrency at 3 to stay within
rate limits.
The ``background_loader`` parameter is accepted for backwards-compatibility The ``background_loader`` parameter is accepted for backwards-compatibility
but is no longer used — repairs always go through ``update_tvshow_nfo`` so but is no longer used.
that the TMDB overview (plot) and all other required tags are written.
Args: Args:
background_loader: Unused. Kept to avoid breaking call-sites. background_loader: Unused. Kept to avoid breaking call-sites.
""" """
from src.core.services.nfo_factory import NFOServiceFactory from src.core.services.nfo_repair_service import nfo_needs_repair
from src.core.services.nfo_repair_service import NfoRepairService, nfo_needs_repair
if not settings.tmdb_api_key: if not settings.tmdb_api_key:
logger.warning("NFO repair scan skipped — TMDB API key not configured") logger.warning("NFO repair scan skipped — TMDB API key not configured")
return return
@@ -405,13 +446,7 @@ async def perform_nfo_repair_scan(background_loader=None) -> None:
if not anime_dir.is_dir(): if not anime_dir.is_dir():
logger.warning("NFO repair scan skipped — anime directory not found: %s", anime_dir) logger.warning("NFO repair scan skipped — anime directory not found: %s", anime_dir)
return return
try:
factory = NFOServiceFactory()
nfo_service = factory.create()
except ValueError as exc:
logger.warning("NFO repair scan skipped — cannot create NFOService: %s", exc)
return
repair_service = NfoRepairService(nfo_service)
queued = 0 queued = 0
total = 0 total = 0
for series_dir in sorted(anime_dir.iterdir()): for series_dir in sorted(anime_dir.iterdir()):
@@ -424,11 +459,9 @@ 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
# Always repair via update_tvshow_nfo so that missing fields such # Each task creates its own NFOService so connectors are isolated.
# as `plot` are fetched from TMDB. Fire as an asyncio task to
# avoid blocking the startup loop.
asyncio.create_task( asyncio.create_task(
repair_service.repair_series(series_dir, series_name), _repair_one_series(series_dir, series_name),
name=f"nfo_repair:{series_name}", name=f"nfo_repair:{series_name}",
) )