fix: ensure series loaded from DB before NFO scan

- Call _load_series_into_memory() before NFO scan phases to sync DB
  to SeriesApp memory, fixing missing NFO for recently resolved folders
- Add TMDB lookup for series without cached tmdb_id during NFO creation
- Add get_tmdb_client() factory and get_tmdb_image_base_url() helpers
- Fix: use get_tv_show_details instead of deprecated get_series_details
- Fix tests: mock _load_series_into_memory in NFO scan tests
This commit is contained in:
2026-06-10 18:48:45 +02:00
parent e76cd3a708
commit d6082b5cf6
4 changed files with 102 additions and 1 deletions

View File

@@ -422,3 +422,32 @@ class TMDBClient:
if expired_keys: if expired_keys:
logger.debug("Removed %d expired negative cache entries", len(expired_keys)) logger.debug("Removed %d expired negative cache entries", len(expired_keys))
return len(expired_keys) return len(expired_keys)
def get_tmdb_client() -> TMDBClient:
"""Factory function to create a TMDBClient with settings configuration.
Returns:
TMDBClient instance configured with settings.tmdb_api_key
Raises:
ValueError: If TMDB API key is not configured
"""
from src.config.settings import settings
if not settings.tmdb_api_key:
raise ValueError("TMDB API key is not configured")
return TMDBClient(api_key=settings.tmdb_api_key)
def get_tmdb_image_base_url(tmdb_id: int) -> str:
"""Get the base URL for TMDB images.
Args:
tmdb_id: TMDB show ID (used for account-specific URLs)
Returns:
Base URL string for TMDB images
"""
return "https://image.tmdb.org/t/p/"

View File

@@ -541,6 +541,8 @@ async def perform_nfo_scan_if_needed(progress_service=None):
# Execute the NFO scan # Execute the NFO scan
try: try:
# Ensure any newly created series are loaded from DB into SeriesApp memory
await _load_series_into_memory(progress_service=None)
await _execute_nfo_scan(progress_service) await _execute_nfo_scan(progress_service)
await _mark_nfo_scan_completed() await _mark_nfo_scan_completed()
except Exception as e: except Exception as e:
@@ -615,6 +617,9 @@ async def perform_nfo_scan_phase(progress_service=None):
# Execute the NFO scan # Execute the NFO scan
try: try:
# Ensure any newly created series (e.g., from resolving unresolved folders)
# are loaded from DB into SeriesApp memory before scanning
await _load_series_into_memory(progress_service=None)
await _execute_nfo_scan(progress_service) await _execute_nfo_scan(progress_service)
await _mark_nfo_scan_completed() await _mark_nfo_scan_completed()

View File

@@ -326,6 +326,22 @@ class NfoScanService:
nfo_exists = os.path.isfile(nfo_path) nfo_exists = os.path.isfile(nfo_path)
# If tmdb_id is missing, try to look it up by series name
if not series_data.get("tmdb_id"):
logger.debug("No tmdb_id for %s — attempting TMDB lookup", key)
name = series_data.get("name", "")
found_tmdb_id = await self._lookup_tmdb_id_by_name(name)
if found_tmdb_id:
series_data["tmdb_id"] = found_tmdb_id
await self._save_tmdb_id(key, found_tmdb_id)
logger.info("Found and saved tmdb_id %s for %s", found_tmdb_id, key)
else:
logger.warning(
"Could not resolve tmdb_id for %s (%s)",
key,
name,
)
if not nfo_exists: if not nfo_exists:
# Create new NFO # Create new NFO
logger.info("Creating NFO for series: %s (%s)", key, folder) logger.info("Creating NFO for series: %s (%s)", key, folder)
@@ -526,6 +542,53 @@ class NfoScanService:
logger.info("Regenerated NFO for %s", key) logger.info("Regenerated NFO for %s", key)
return True return True
async def _save_tmdb_id(self, key: str, tmdb_id: int) -> None:
"""Save tmdb_id to the database for a series.
Args:
key: Series key (primary identifier)
tmdb_id: TMDB series ID to save
"""
try:
from src.server.database.connection import get_db_session
from src.server.database.service import AnimeSeriesService
async with get_db_session() as db:
series = await AnimeSeriesService.get_by_key(db, key)
if series:
series.tmdb_id = tmdb_id
await db.flush()
logger.debug("Saved tmdb_id %s for series: %s", tmdb_id, key)
else:
logger.warning("Series not found for tmdb_id save: %s", key)
except Exception as exc:
logger.warning("Failed to save tmdb_id for %s: %s", key, exc)
async def _lookup_tmdb_id_by_name(self, name: str) -> Optional[int]:
"""Look up a TMDB series ID by series name.
Args:
name: Series name to search for
Returns:
TMDB series ID or None if not found.
"""
if not name:
return None
try:
from src.server.nfo.tmdb_client import get_tmdb_client
client = get_tmdb_client()
results = await client.search_tv_show(name)
if results and results.get("results"):
first_result = results["results"][0]
return first_result.get("id")
return None
except Exception as exc:
logger.warning("TMDB lookup failed for %s: %s", name, exc)
return None
async def _fetch_tmdb_data(self, tmdb_id: int) -> Optional[Dict[str, Any]]: async def _fetch_tmdb_data(self, tmdb_id: int) -> Optional[Dict[str, Any]]:
"""Fetch series metadata from TMDB API. """Fetch series metadata from TMDB API.
@@ -539,7 +602,7 @@ class NfoScanService:
from src.server.nfo.tmdb_client import get_tmdb_client from src.server.nfo.tmdb_client import get_tmdb_client
client = get_tmdb_client() client = get_tmdb_client()
data = await client.get_series_details(tmdb_id) data = await client.get_tv_show_details(tmdb_id)
return data return data
except Exception as exc: except Exception as exc:
logger.warning("TMDB fetch failed for TMDB ID %s: %s", tmdb_id, exc) logger.warning("TMDB fetch failed for TMDB ID %s: %s", tmdb_id, exc)

View File

@@ -532,6 +532,8 @@ class TestPerformNFOScan:
new_callable=AsyncMock, return_value=False), \ new_callable=AsyncMock, return_value=False), \
patch('src.server.services.initialization_service._is_nfo_scan_configured', patch('src.server.services.initialization_service._is_nfo_scan_configured',
new_callable=AsyncMock, return_value=True), \ new_callable=AsyncMock, return_value=True), \
patch('src.server.services.initialization_service._load_series_into_memory',
new_callable=AsyncMock), \
patch('src.server.services.initialization_service._execute_nfo_scan', patch('src.server.services.initialization_service._execute_nfo_scan',
new_callable=AsyncMock), \ new_callable=AsyncMock), \
patch('src.server.services.initialization_service._mark_nfo_scan_completed', patch('src.server.services.initialization_service._mark_nfo_scan_completed',
@@ -549,6 +551,8 @@ class TestPerformNFOScan:
new_callable=AsyncMock, return_value=False), \ new_callable=AsyncMock, return_value=False), \
patch('src.server.services.initialization_service._is_nfo_scan_configured', patch('src.server.services.initialization_service._is_nfo_scan_configured',
new_callable=AsyncMock, return_value=True), \ new_callable=AsyncMock, return_value=True), \
patch('src.server.services.initialization_service._load_series_into_memory',
new_callable=AsyncMock), \
patch('src.server.services.initialization_service._execute_nfo_scan', patch('src.server.services.initialization_service._execute_nfo_scan',
new_callable=AsyncMock), \ new_callable=AsyncMock), \
patch('src.server.services.initialization_service._mark_nfo_scan_completed', patch('src.server.services.initialization_service._mark_nfo_scan_completed',