feat: add ImageLoadingService for downloading series artwork
- Create ImageLoadingService that downloads poster.jpg, fanart.jpg, and logo.png from TMDB when anime is added or during scheduler rescan - Integrate into BackgroundLoaderService._load_nfo_and_images() to trigger image downloads when new anime is added - Add image_scan_after_rescan config option to scheduler (default: true) - Add _run_image_scan() to scheduler rescan flow, processing series in batches of 10 to respect TMDB rate limits - Fix SearchResult model missing folder, snippet, and score fields - Update background_loader tests to match new image loading behavior
This commit is contained in:
@@ -11,11 +11,12 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from src.config.settings import settings
|
||||
from src.server.models.config import SchedulerConfig
|
||||
from src.server.services.config_service import ConfigServiceError, get_config_service
|
||||
|
||||
@@ -264,6 +265,9 @@ class SchedulerService:
|
||||
"nfo_scan_after_rescan": (
|
||||
self._config.nfo_scan_after_rescan if self._config else True
|
||||
),
|
||||
"image_scan_after_rescan": (
|
||||
self._config.image_scan_after_rescan if self._config else True
|
||||
),
|
||||
"last_run": (
|
||||
self._last_scan_time.isoformat()
|
||||
if self._last_scan_time
|
||||
@@ -409,6 +413,21 @@ class SchedulerService:
|
||||
logger.error("Auto-download failed: %s", exc, exc_info=True)
|
||||
await self._broadcast("auto_download_error", {"error": str(exc)})
|
||||
|
||||
# 4. Image scan (if enabled)
|
||||
if self._config and self._config.image_scan_after_rescan:
|
||||
try:
|
||||
image_result = await self._run_image_scan()
|
||||
await self._broadcast("image_scan_completed", {
|
||||
"total": image_result.get("total", 0),
|
||||
"success": image_result.get("success", 0),
|
||||
"poster": image_result.get("poster", 0),
|
||||
"fanart": image_result.get("fanart", 0),
|
||||
"logo": image_result.get("logo", 0),
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.error("Image scan failed: %s", exc, exc_info=True)
|
||||
await self._broadcast("image_scan_error", {"error": str(exc)})
|
||||
|
||||
self._last_scan_time = datetime.now(timezone.utc)
|
||||
duration = (self._last_scan_time - scan_start).total_seconds()
|
||||
|
||||
@@ -462,6 +481,61 @@ class SchedulerService:
|
||||
)
|
||||
return result
|
||||
|
||||
async def _run_image_scan(self) -> Dict[str, Any]:
|
||||
"""Download missing images for all series from TMDB."""
|
||||
from src.server.database.connection import get_db_session
|
||||
from src.server.nfo.tmdb_client import get_tmdb_client
|
||||
from src.server.services.image_loading_service import init_image_loading_service
|
||||
from src.server.utils.dependencies import get_anime_service
|
||||
|
||||
anime_service = get_anime_service()
|
||||
|
||||
try:
|
||||
series_list_data = await anime_service.list_series_with_filters()
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to get series list for image scan: %s", exc)
|
||||
return {"total": 0, "success": 0, "partial": 0, "failed": 0}
|
||||
|
||||
if not series_list_data:
|
||||
logger.info("No series found for image scan")
|
||||
return {"total": 0, "success": 0, "partial": 0, "failed": 0}
|
||||
|
||||
# Build list of series to process
|
||||
series_to_process = []
|
||||
for series_data in series_list_data:
|
||||
key = series_data.get("key")
|
||||
folder = series_data.get("folder")
|
||||
if not key or not folder:
|
||||
continue
|
||||
series_to_process.append({"key": key, "folder": folder})
|
||||
|
||||
if not series_to_process:
|
||||
logger.info("No series to process for image scan")
|
||||
return {"total": 0, "success": 0, "partial": 0, "failed": 0}
|
||||
|
||||
logger.info("Starting image scan for %d series...", len(series_to_process))
|
||||
|
||||
# Initialize TMDB client and image loading service
|
||||
tmdb_client = get_tmdb_client()
|
||||
image_service = init_image_loading_service(tmdb_client)
|
||||
|
||||
anime_dir = settings.anime_directory
|
||||
async with get_db_session() as db:
|
||||
result = await image_service.load_series_images_batch(
|
||||
series_list=series_to_process,
|
||||
anime_directory=anime_dir,
|
||||
db=db,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Image scan completed: total=%d success=%d partial=%d failed=%d",
|
||||
result.get("total", 0),
|
||||
result.get("success", 0),
|
||||
result.get("partial", 0),
|
||||
result.get("failed", 0),
|
||||
)
|
||||
return result
|
||||
|
||||
async def _run_auto_download(self) -> int:
|
||||
"""Queue and start downloads for all series with missing episodes."""
|
||||
from src.server.models.download import EpisodeIdentifier
|
||||
|
||||
Reference in New Issue
Block a user