Add sync_single_series_after_scan with NFO metadata and WebSocket updates

- Implement sync_single_series_after_scan to persist scanned series to database
- Enhanced _broadcast_series_updated to include full NFO metadata (nfo_created_at, nfo_updated_at, tmdb_id, tvdb_id)
- Add immediate episode scanning in add_series endpoint when background loader isn't running
- Implement updateSingleSeries in frontend to handle series_updated WebSocket events
- Add SERIES_UPDATED event constant to WebSocket event definitions
- Update background loader to use sync_single_series_after_scan method
- Simplified background loader initialization in FastAPI app
- Add comprehensive tests for series update WebSocket payload and episode counting logic
- Import reorganization: move get_background_loader_service to dependencies module
This commit is contained in:
2026-02-06 18:36:39 +01:00
parent d74c181556
commit d72b8cb1ab
9 changed files with 1078 additions and 21 deletions

View File

@@ -658,6 +658,57 @@ class AnimeService:
logger.exception("rescan failed")
raise AnimeServiceError("Rescan failed") from exc
async def sync_single_series_after_scan(self, series_key: str) -> None:
"""Persist a single scanned series and refresh cached state.
Reuses the same save/reload/cache invalidation flow as `rescan`
to keep the database, in-memory list, and UI in sync.
Args:
series_key: Series key to persist and refresh.
"""
# Get serie from scanner's keyDict, not series_app.list.keyDict
# scan_single_series updates serie_scanner.keyDict with episodeDict
if not hasattr(self._app, "serie_scanner") or not hasattr(self._app.serie_scanner, "keyDict"):
logger.warning(
"Serie scanner not available for single-series sync: %s",
series_key,
)
return
serie = self._app.serie_scanner.keyDict.get(series_key)
if not serie:
logger.warning(
"Series not found in scanner keyDict for single-series sync: %s",
series_key,
)
return
total_episodes = sum(len(eps) for eps in (serie.episodeDict or {}).values())
logger.info(
"Syncing series %s with %d missing episodes. episodeDict: %s",
series_key,
total_episodes,
serie.episodeDict
)
await self._save_scan_results_to_db([serie])
await self._load_series_from_db()
try:
self._cached_list_missing.cache_clear()
except Exception: # pylint: disable=broad-except
pass
try:
await self._broadcast_series_updated(series_key)
except Exception as exc: # pylint: disable=broad-except
logger.warning(
"Failed to broadcast series update for %s: %s",
series_key,
exc,
)
async def _save_scan_results_to_db(self, series_list: list) -> int:
"""
Save scan results to the database.
@@ -684,13 +735,27 @@ class AnimeService:
db, serie.key
)
total_episodes = sum(len(eps) for eps in (serie.episodeDict or {}).values())
if existing:
# Update existing series
logger.info(
"Updating existing series %s with %d episodes. episodeDict: %s",
serie.key,
total_episodes,
serie.episodeDict
)
await self._update_series_in_db(
serie, existing, db
)
else:
# Create new series
logger.info(
"Creating new series %s with %d episodes. episodeDict: %s",
serie.key,
total_episodes,
serie.episodeDict
)
await self._create_series_in_db(serie, db)
saved_count += 1
@@ -936,17 +1001,79 @@ class AnimeService:
return episodes_added
async def _broadcast_series_updated(self, series_key: str) -> None:
"""Broadcast series update event to WebSocket clients."""
"""Broadcast series update event to WebSocket clients with full data."""
if not self._websocket_service:
return
# Get updated series data to send to frontend
series_data = None
if hasattr(self._app, 'list') and hasattr(self._app.list, 'keyDict'):
serie = self._app.list.keyDict.get(series_key)
if serie:
# Convert episode dict keys to strings for JSON
missing_episodes = {str(k): v for k, v in (serie.episodeDict or {}).items()}
total_missing = sum(len(eps) for eps in missing_episodes.values())
# Fetch NFO metadata from database
has_nfo = False
nfo_created_at = None
nfo_updated_at = None
tmdb_id = None
tvdb_id = None
try:
from src.server.database.connection import get_db_session
from src.server.database.service import AnimeSeriesService
async with get_db_session() as db:
db_series = await AnimeSeriesService.get_by_key(db, series_key)
if db_series:
has_nfo = db_series.has_nfo or False
nfo_created_at = (
db_series.nfo_created_at.isoformat()
if db_series.nfo_created_at else None
)
nfo_updated_at = (
db_series.nfo_updated_at.isoformat()
if db_series.nfo_updated_at else None
)
tmdb_id = db_series.tmdb_id
tvdb_id = db_series.tvdb_id
except Exception as e:
logger.warning(
"Could not fetch NFO data for %s: %s",
series_key,
str(e)
)
series_data = {
"key": serie.key,
"name": serie.name,
"folder": serie.folder,
"site": serie.site,
"missing_episodes": missing_episodes,
"has_missing": total_missing > 0,
"has_nfo": has_nfo,
"nfo_created_at": nfo_created_at,
"nfo_updated_at": nfo_updated_at,
"tmdb_id": tmdb_id,
"tvdb_id": tvdb_id,
}
payload = {
"type": "series_updated",
"key": series_key,
"message": "Episodes updated",
"data": series_data,
"message": "Series episodes updated",
"timestamp": datetime.now(timezone.utc).isoformat()
}
logger.info(
"Broadcasting series update for %s with %d missing episodes",
series_key,
sum(len(eps) for eps in (series_data.get("missing_episodes", {}).values())) if series_data else 0
)
await self._websocket_service.broadcast(payload)
async def add_series_to_db(