Remove redundant episode loading step

- 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
This commit is contained in:
2026-01-23 18:26:36 +01:00
parent 0e58a49cdd
commit 800790fc8f
3 changed files with 171 additions and 9 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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,