fix: isolate NFO repair sessions to prevent connector-closed errors
This commit is contained in:
@@ -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}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user