From 800790fc8f7939e30abca7fdebbd8d446978c15b Mon Sep 17 00:00:00 2001 From: Lukas Date: Fri, 23 Jan 2026 18:26:36 +0100 Subject: [PATCH] Remove redundant episode loading step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merged _load_episodes() functionality into _scan_missing_episodes() - _scan_missing_episodes() already queries provider and compares with filesystem - Eliminates duplicate filesystem scanning during series add - Simplifies background loading flow: NFO → Episode Discovery --- docs/instructions.md | 1 + src/server/services/anime_service.py | 93 +++++++++++++++++++ .../services/background_loader_service.py | 86 +++++++++++++++-- 3 files changed, 171 insertions(+), 9 deletions(-) diff --git a/docs/instructions.md b/docs/instructions.md index c8ef653..1df7ce1 100644 --- a/docs/instructions.md +++ b/docs/instructions.md @@ -120,3 +120,4 @@ For each task completed: ## TODO List: ✅ **COMPLETED:** anime not showing issue - Fixed by loading series from database on every startup +✅ **COMPLETED:** Load missing episodes for newly added series - Implemented episode scanning and database sync after NFO creation diff --git a/src/server/services/anime_service.py b/src/server/services/anime_service.py index aef6ad5..f579c98 100644 --- a/src/server/services/anime_service.py +++ b/src/server/services/anime_service.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import time +from datetime import datetime, timezone from functools import lru_cache from typing import Optional @@ -733,6 +734,98 @@ class AnimeService: # Load into SeriesApp self._app.load_series_from_list(series_list) + async def sync_episodes_to_db(self, series_key: str) -> int: + """ + Sync episodes from in-memory SeriesApp to database for a specific series. + + This method reads the episodeDict from the in-memory series (populated + by scanner) and syncs it to the database. Called after scanning for + missing episodes. + + Args: + series_key: The series key to sync episodes for + + Returns: + Number of episodes synced to database + """ + from src.server.database.connection import get_db_session + from src.server.database.service import AnimeSeriesService, EpisodeService + + # Get the serie from in-memory cache + if not hasattr(self._app, 'list') or not hasattr(self._app.list, 'keyDict'): + logger.warning(f"Series list not available for episode sync: {series_key}") + return 0 + + serie = self._app.list.keyDict.get(series_key) + if not serie: + logger.warning(f"Series not found in memory for episode sync: {series_key}") + return 0 + + episodes_added = 0 + + async with get_db_session() as db: + # Get series from database + series_db = await AnimeSeriesService.get_by_key(db, series_key) + if not series_db: + logger.warning(f"Series not found in database: {series_key}") + return 0 + + # Get existing episodes from database + existing_episodes = await EpisodeService.get_by_series(db, series_db.id) + + # Build dict of existing episodes: {season: {ep_num: episode_id}} + existing_dict: dict[int, dict[int, int]] = {} + for ep in existing_episodes: + if ep.season not in existing_dict: + existing_dict[ep.season] = {} + existing_dict[ep.season][ep.episode_number] = ep.id + + # Get new missing episodes from in-memory serie + new_dict = serie.episodeDict or {} + + # Add new missing episodes that are not in the database + for season, episode_numbers in new_dict.items(): + existing_season_eps = existing_dict.get(season, {}) + for ep_num in episode_numbers: + if ep_num not in existing_season_eps: + await EpisodeService.create( + db=db, + series_id=series_db.id, + season=season, + episode_number=ep_num, + ) + episodes_added += 1 + logger.debug( + f"Added missing episode to database: {series_key} S{season:02d}E{ep_num:02d}" + ) + + if episodes_added > 0: + logger.info( + f"Synced {episodes_added} missing episodes to database for {series_key}" + ) + + # Broadcast update to frontend to refresh series list + try: + await self._broadcast_series_updated(series_key) + except Exception as e: + logger.warning(f"Failed to broadcast series update: {e}") + + return episodes_added + + async def _broadcast_series_updated(self, series_key: str) -> None: + """Broadcast series update event to WebSocket clients.""" + if not self._websocket_service: + return + + payload = { + "type": "series_updated", + "key": series_key, + "message": "Episodes updated", + "timestamp": datetime.now(timezone.utc).isoformat() + } + + await self._websocket_service.broadcast(payload) + async def add_series_to_db( self, serie, diff --git a/src/server/services/background_loader_service.py b/src/server/services/background_loader_service.py index 5376891..70f617b 100644 --- a/src/server/services/background_loader_service.py +++ b/src/server/services/background_loader_service.py @@ -289,12 +289,6 @@ class BackgroundLoaderService: db ) - # Load episodes if missing - if missing["episodes"]: - await self._load_episodes(task, db) - else: - task.progress["episodes"] = True - # Load NFO and images if missing if missing["nfo"] or missing["logo"] or missing["images"]: await self._load_nfo_and_images(task, db) @@ -303,6 +297,11 @@ class BackgroundLoaderService: task.progress["logo"] = True task.progress["images"] = True + # Scan for missing episodes + # This discovers seasons/episodes from provider and compares with filesystem + # to populate episodeDict with episodes available for download + await self._scan_missing_episodes(task, db) + # Mark as completed task.status = LoadingStatus.COMPLETED task.completed_at = datetime.now(timezone.utc) @@ -451,12 +450,15 @@ class BackgroundLoaderService: logger.exception(f"Failed to load episodes for {task.key}: {e}") raise - async def _load_nfo_and_images(self, task: SeriesLoadingTask, db: Any) -> None: + async def _load_nfo_and_images(self, task: SeriesLoadingTask, db: Any) -> bool: """Load NFO file and images for a series by reusing NFOService. Args: task: The loading task db: Database session + + Returns: + bool: True if NFO was created, False if it already existed or failed """ task.status = LoadingStatus.LOADING_NFO await self._broadcast_status(task, "Checking NFO file...") @@ -470,7 +472,7 @@ class BackgroundLoaderService: task.progress["nfo"] = False task.progress["logo"] = False task.progress["images"] = False - return + return False # Check if NFO already exists if self.series_app.nfo_service.has_nfo(task.folder): @@ -497,7 +499,7 @@ class BackgroundLoaderService: await db.commit() logger.info(f"Existing NFO found and database updated for series: {task.key}") - return + return False # NFO doesn't exist, create it await self._broadcast_status(task, "Generating NFO file...") @@ -531,6 +533,7 @@ class BackgroundLoaderService: await db.commit() logger.info(f"NFO and images created and loaded for series: {task.key}") + return True except Exception as e: logger.exception(f"Failed to load NFO/images for {task.key}: {e}") @@ -538,6 +541,69 @@ class BackgroundLoaderService: task.progress["nfo"] = False task.progress["logo"] = False task.progress["images"] = False + return False + + async def _scan_missing_episodes(self, task: SeriesLoadingTask, db: Any) -> None: + """Scan for missing episodes after NFO creation. + + This method calls SerieScanner.scan_single_series() to populate + the episodeDict with available episodes that can be downloaded. + + Args: + task: The loading task + db: Database session + """ + task.status = LoadingStatus.LOADING_EPISODES + await self._broadcast_status(task, "Scanning for missing episodes...") + + try: + # Get scanner from SeriesApp + if not hasattr(self.series_app, 'serie_scanner'): + logger.warning( + f"Scanner not available, skipping episode scan for {task.key}" + ) + return + + # Scan for missing episodes using the targeted scan method + # This populates the episodeDict without triggering a full rescan + logger.info(f"Scanning missing episodes for {task.key}") + missing_episodes = self.series_app.serie_scanner.scan_single_series( + key=task.key, + folder=task.folder + ) + + # Log the results + total_missing = sum(len(eps) for eps in missing_episodes.values()) + if total_missing > 0: + logger.info( + f"Found {total_missing} missing episodes across " + f"{len(missing_episodes)} seasons for {task.key}" + ) + + # Notify anime_service to sync episodes to database + if self.anime_service: + logger.debug(f"Calling anime_service.sync_episodes_to_db for {task.key}") + await self.anime_service.sync_episodes_to_db(task.key) + else: + logger.warning(f"anime_service not available, episodes will not be synced to DB for {task.key}") + else: + logger.info(f"No missing episodes found for {task.key}") + + # Update series status in database + from src.server.database.service import AnimeSeriesService + series_db = await AnimeSeriesService.get_by_key(db, task.key) + if series_db: + series_db.episodes_loaded = True + series_db.loading_status = "loading_episodes" + await db.commit() + + # Mark progress as complete + task.progress["episodes"] = True + task.progress["episodes"] = True + + except Exception as e: + logger.exception(f"Failed to scan missing episodes for {task.key}: {e}") + task.progress["episodes"] = False async def _broadcast_status( self, @@ -567,7 +633,9 @@ class BackgroundLoaderService: payload = { "type": "series_loading_update", "key": task.key, + "series_key": task.key, # For frontend compatibility "folder": task.folder, + "status": task.status.value, # For frontend compatibility "loading_status": task.status.value, "progress": task.progress, "message": message,