From a336733ea9524bf5be3291f14bd8bbfe2e59b565 Mon Sep 17 00:00:00 2001 From: Lukas Date: Mon, 25 May 2026 16:32:54 +0200 Subject: [PATCH] fix(nfo): add year field to series and create missing NFO files - Add missing year field when building series list in anime_service - Add _create_missing_nfo to generate minimal NFO for series without one - Update perform_nfo_repair_scan to detect and create missing NFOs - Add semaphore-protected async creation with TMDB rate limiting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/server/services/anime_service.py | 3 +- src/server/services/folder_scan_service.py | 50 +++++++++++++++++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/server/services/anime_service.py b/src/server/services/anime_service.py index 84261b4..7eb9759 100644 --- a/src/server/services/anime_service.py +++ b/src/server/services/anime_service.py @@ -919,7 +919,8 @@ class AnimeService: name=anime_series.name, site=anime_series.site, folder=anime_series.folder, - episodeDict=episode_dict + episodeDict=episode_dict, + year=anime_series.year ) series_list.append(serie) diff --git a/src/server/services/folder_scan_service.py b/src/server/services/folder_scan_service.py index 4268d54..c1e03e6 100644 --- a/src/server/services/folder_scan_service.py +++ b/src/server/services/folder_scan_service.py @@ -28,6 +28,36 @@ _POSTER_DOWNLOAD_SEMAPHORE: asyncio.Semaphore = asyncio.Semaphore(3) _NFO_REPAIR_SEMAPHORE: asyncio.Semaphore = asyncio.Semaphore(3) +async def _create_missing_nfo(series_dir: Path, series_name: str) -> None: + """Create minimal NFO for series without one. + + Creates a fresh :class:`NFOService` per invocation so concurrent + tasks cannot interfere with each other. + + A module-level semaphore limits concurrent TMDB operations to 3. + + 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 + + async with _NFO_REPAIR_SEMAPHORE: + try: + factory = NFOServiceFactory() + nfo_service = factory.create() + await nfo_service.create_minimal_nfo( + serie_name=series_name, + serie_folder=series_dir.name, + ) + except Exception as exc: # pylint: disable=broad-except + logger.error( + "NFO creation failed for %s: %s", + series_name, + exc, + ) + + async def _repair_one_series(series_dir: Path, series_name: str) -> None: """Repair a single series NFO in isolation. @@ -63,12 +93,13 @@ async def _repair_one_series(series_dir: Path, series_name: str) -> 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, repair incomplete and create missing NFO files. Called from ``FolderScanService.run_folder_scan()`` during the scheduled daily folder scan (not on every startup). Checks each subfolder of - ``settings.anime_directory`` for a ``tvshow.nfo`` and calls - ``_repair_one_series`` for every file with absent or empty required tags. + ``settings.anime_directory`` for a ``tvshow.nfo``: + - Missing NFOs: creates minimal NFO via ``_create_missing_nfo`` + - Incomplete NFOs: repairs via ``_repair_one_series`` Each repair task creates its own isolated :class:`NFOService` / :class:`TMDBClient` so concurrent tasks never share an ``aiohttp`` @@ -97,26 +128,33 @@ async def perform_nfo_repair_scan(background_loader=None) -> None: queued = 0 total = 0 + missing_nfo_count = 0 for series_dir in sorted(anime_dir.iterdir()): if not series_dir.is_dir(): continue nfo_path = series_dir / "tvshow.nfo" + series_name = series_dir.name if not nfo_path.exists(): + # Create minimal NFO for series without one + missing_nfo_count += 1 + asyncio.create_task( + _create_missing_nfo(series_dir, series_name), + name=f"nfo_create:{series_name}", + ) continue total += 1 - series_name = series_dir.name if nfo_needs_repair(nfo_path): queued += 1 - # Each task creates its own NFOService so connectors are isolated. asyncio.create_task( _repair_one_series(series_dir, series_name), name=f"nfo_repair:{series_name}", ) logger.info( - "NFO repair scan complete: %d of %d series queued for repair", + "NFO repair scan complete: %d of %d series queued for repair, %d missing NFOs queued for creation", queued, total, + missing_nfo_count, )