Fix Issue 5: Create NFOServiceFactory for centralized initialization
- Created NFOServiceFactory in src/core/services/nfo_factory.py - Enforces configuration precedence: explicit params > ENV > config.json - Provides create() and create_optional() methods - Singleton factory instance via get_nfo_factory() - Updated 4 files to use factory (nfo.py, SeriesApp.py, series_manager_service.py, nfo_cli.py) - Fixed test mocks: added ensure_folder_with_year(), corrected dependency test - Tests: 17/18 NFO passing, 15/16 anime passing - Resolves Code Duplication 2 (NFO initialization)
This commit is contained in:
@@ -175,14 +175,11 @@ class SeriesApp:
|
||||
self.nfo_service: Optional[NFOService] = None
|
||||
if settings.tmdb_api_key:
|
||||
try:
|
||||
self.nfo_service = NFOService(
|
||||
tmdb_api_key=settings.tmdb_api_key,
|
||||
anime_directory=directory_to_search,
|
||||
image_size=settings.nfo_image_size,
|
||||
auto_create=settings.nfo_auto_create
|
||||
)
|
||||
from src.core.services.nfo_factory import get_nfo_factory
|
||||
factory = get_nfo_factory()
|
||||
self.nfo_service = factory.create()
|
||||
logger.info("NFO service initialized successfully")
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
except (ValueError, Exception) as e: # pylint: disable=broad-except
|
||||
logger.warning(
|
||||
"Failed to initialize NFO service: %s", str(e)
|
||||
)
|
||||
|
||||
237
src/core/services/nfo_factory.py
Normal file
237
src/core/services/nfo_factory.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""NFO Service Factory Module.
|
||||
|
||||
This module provides a centralized factory for creating NFOService instances
|
||||
with consistent configuration and initialization logic.
|
||||
|
||||
The factory supports both direct instantiation and FastAPI dependency injection,
|
||||
while remaining testable through optional dependency overrides.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from src.config.settings import settings
|
||||
from src.core.services.nfo_service import NFOService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NFOServiceFactory:
|
||||
"""Factory for creating NFOService instances with consistent configuration.
|
||||
|
||||
This factory centralizes NFO service initialization logic that was previously
|
||||
duplicated across multiple modules (SeriesApp, SeriesManagerService, API endpoints).
|
||||
|
||||
The factory follows these precedence rules for configuration:
|
||||
1. Explicit parameters (highest priority)
|
||||
2. Environment variables via settings
|
||||
3. config.json via ConfigService (fallback)
|
||||
4. Raise error if TMDB API key unavailable
|
||||
|
||||
Example:
|
||||
>>> factory = NFOServiceFactory()
|
||||
>>> nfo_service = factory.create()
|
||||
>>> # Or with custom settings:
|
||||
>>> nfo_service = factory.create(tmdb_api_key="custom_key")
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the NFO service factory."""
|
||||
self._config_service = None
|
||||
|
||||
def create(
|
||||
self,
|
||||
tmdb_api_key: Optional[str] = None,
|
||||
anime_directory: Optional[str] = None,
|
||||
image_size: Optional[str] = None,
|
||||
auto_create: Optional[bool] = None
|
||||
) -> NFOService:
|
||||
"""Create an NFOService instance with proper configuration.
|
||||
|
||||
This method implements the configuration precedence:
|
||||
1. Use explicit parameters if provided
|
||||
2. Fall back to settings (from ENV vars)
|
||||
3. Fall back to config.json (only if ENV not set)
|
||||
4. Raise ValueError if TMDB API key still unavailable
|
||||
|
||||
Args:
|
||||
tmdb_api_key: TMDB API key (optional, falls back to settings/config)
|
||||
anime_directory: Anime directory path (optional, defaults to settings)
|
||||
image_size: Image size for downloads (optional, defaults to settings)
|
||||
auto_create: Whether to auto-create NFO files (optional, defaults to settings)
|
||||
|
||||
Returns:
|
||||
NFOService: Configured NFO service instance
|
||||
|
||||
Raises:
|
||||
ValueError: If TMDB API key cannot be determined from any source
|
||||
|
||||
Example:
|
||||
>>> factory = NFOServiceFactory()
|
||||
>>> # Use all defaults from settings
|
||||
>>> service = factory.create()
|
||||
>>> # Override specific settings
|
||||
>>> service = factory.create(auto_create=False)
|
||||
"""
|
||||
# Step 1: Determine TMDB API key with fallback logic
|
||||
api_key = tmdb_api_key or settings.tmdb_api_key
|
||||
|
||||
# Step 2: If no API key in settings, try config.json as fallback
|
||||
if not api_key:
|
||||
api_key = self._get_api_key_from_config()
|
||||
|
||||
# Step 3: Validate API key is available
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
"TMDB API key not configured. Set TMDB_API_KEY environment "
|
||||
"variable or configure in config.json (nfo.tmdb_api_key)."
|
||||
)
|
||||
|
||||
# Step 4: Use provided values or fall back to settings
|
||||
directory = anime_directory or settings.anime_directory
|
||||
size = image_size or settings.nfo_image_size
|
||||
auto = auto_create if auto_create is not None else settings.nfo_auto_create
|
||||
|
||||
# Step 5: Create and return the service
|
||||
logger.debug(
|
||||
"Creating NFOService: directory=%s, size=%s, auto_create=%s",
|
||||
directory, size, auto
|
||||
)
|
||||
|
||||
return NFOService(
|
||||
tmdb_api_key=api_key,
|
||||
anime_directory=directory,
|
||||
image_size=size,
|
||||
auto_create=auto
|
||||
)
|
||||
|
||||
def create_optional(
|
||||
self,
|
||||
tmdb_api_key: Optional[str] = None,
|
||||
anime_directory: Optional[str] = None,
|
||||
image_size: Optional[str] = None,
|
||||
auto_create: Optional[bool] = None
|
||||
) -> Optional[NFOService]:
|
||||
"""Create an NFOService instance, returning None if configuration unavailable.
|
||||
|
||||
This is a convenience method for cases where NFO service is optional.
|
||||
Unlike create(), this returns None instead of raising ValueError when
|
||||
the TMDB API key is not configured.
|
||||
|
||||
Args:
|
||||
tmdb_api_key: TMDB API key (optional)
|
||||
anime_directory: Anime directory path (optional)
|
||||
image_size: Image size for downloads (optional)
|
||||
auto_create: Whether to auto-create NFO files (optional)
|
||||
|
||||
Returns:
|
||||
Optional[NFOService]: Configured service or None if key unavailable
|
||||
|
||||
Example:
|
||||
>>> factory = NFOServiceFactory()
|
||||
>>> service = factory.create_optional()
|
||||
>>> if service:
|
||||
... service.create_tvshow_nfo(...)
|
||||
"""
|
||||
try:
|
||||
return self.create(
|
||||
tmdb_api_key=tmdb_api_key,
|
||||
anime_directory=anime_directory,
|
||||
image_size=image_size,
|
||||
auto_create=auto_create
|
||||
)
|
||||
except ValueError as e:
|
||||
logger.debug("NFO service not available: %s", e)
|
||||
return None
|
||||
|
||||
def _get_api_key_from_config(self) -> Optional[str]:
|
||||
"""Get TMDB API key from config.json as fallback.
|
||||
|
||||
This method is only called when the API key is not in settings
|
||||
(i.e., not set via environment variable). It provides backward
|
||||
compatibility with config.json configuration.
|
||||
|
||||
Returns:
|
||||
Optional[str]: API key from config.json, or None if unavailable
|
||||
"""
|
||||
try:
|
||||
# Lazy import to avoid circular dependencies
|
||||
from src.server.services.config_service import get_config_service
|
||||
|
||||
if self._config_service is None:
|
||||
self._config_service = get_config_service()
|
||||
|
||||
config = self._config_service.load_config()
|
||||
|
||||
if config.nfo and config.nfo.tmdb_api_key:
|
||||
logger.debug("Using TMDB API key from config.json")
|
||||
return config.nfo.tmdb_api_key
|
||||
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
logger.debug("Could not load API key from config.json: %s", e)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Global factory instance for convenience
|
||||
_factory_instance: Optional[NFOServiceFactory] = None
|
||||
|
||||
|
||||
def get_nfo_factory() -> NFOServiceFactory:
|
||||
"""Get the global NFO service factory instance.
|
||||
|
||||
This function provides a singleton factory instance for the application.
|
||||
The singleton pattern here is for the factory itself (which is stateless),
|
||||
not for the NFO service instances it creates.
|
||||
|
||||
Returns:
|
||||
NFOServiceFactory: The global factory instance
|
||||
|
||||
Example:
|
||||
>>> factory = get_nfo_factory()
|
||||
>>> service = factory.create()
|
||||
"""
|
||||
global _factory_instance
|
||||
|
||||
if _factory_instance is None:
|
||||
_factory_instance = NFOServiceFactory()
|
||||
|
||||
return _factory_instance
|
||||
|
||||
|
||||
def create_nfo_service(
|
||||
tmdb_api_key: Optional[str] = None,
|
||||
anime_directory: Optional[str] = None,
|
||||
image_size: Optional[str] = None,
|
||||
auto_create: Optional[bool] = None
|
||||
) -> NFOService:
|
||||
"""Convenience function to create an NFOService instance.
|
||||
|
||||
This is a shorthand for get_nfo_factory().create() that can be used
|
||||
when you need a quick NFO service instance without interacting with
|
||||
the factory directly.
|
||||
|
||||
Args:
|
||||
tmdb_api_key: TMDB API key (optional)
|
||||
anime_directory: Anime directory path (optional)
|
||||
image_size: Image size for downloads (optional)
|
||||
auto_create: Whether to auto-create NFO files (optional)
|
||||
|
||||
Returns:
|
||||
NFOService: Configured NFO service instance
|
||||
|
||||
Raises:
|
||||
ValueError: If TMDB API key cannot be determined
|
||||
|
||||
Example:
|
||||
>>> service = create_nfo_service()
|
||||
>>> # Or with custom settings:
|
||||
>>> service = create_nfo_service(auto_create=False)
|
||||
"""
|
||||
factory = get_nfo_factory()
|
||||
return factory.create(
|
||||
tmdb_api_key=tmdb_api_key,
|
||||
anime_directory=anime_directory,
|
||||
image_size=image_size,
|
||||
auto_create=auto_create
|
||||
)
|
||||
@@ -71,14 +71,22 @@ class SeriesManagerService:
|
||||
# Initialize NFO service if API key provided and NFO features enabled
|
||||
self.nfo_service: Optional[NFOService] = None
|
||||
if tmdb_api_key and (auto_create_nfo or update_on_scan):
|
||||
self.nfo_service = NFOService(
|
||||
tmdb_api_key=tmdb_api_key,
|
||||
anime_directory=anime_directory,
|
||||
image_size=image_size,
|
||||
auto_create=auto_create_nfo
|
||||
)
|
||||
logger.info("NFO service initialized (auto_create=%s, update=%s)",
|
||||
auto_create_nfo, update_on_scan)
|
||||
try:
|
||||
from src.core.services.nfo_factory import get_nfo_factory
|
||||
factory = get_nfo_factory()
|
||||
self.nfo_service = factory.create(
|
||||
tmdb_api_key=tmdb_api_key,
|
||||
anime_directory=anime_directory,
|
||||
image_size=image_size,
|
||||
auto_create=auto_create_nfo
|
||||
)
|
||||
logger.info("NFO service initialized (auto_create=%s, update=%s)",
|
||||
auto_create_nfo, update_on_scan)
|
||||
except (ValueError, Exception) as e: # pylint: disable=broad-except
|
||||
logger.warning(
|
||||
"Failed to initialize NFO service: %s", str(e)
|
||||
)
|
||||
self.nfo_service = None
|
||||
elif auto_create_nfo or update_on_scan:
|
||||
logger.warning(
|
||||
"NFO features requested but TMDB_API_KEY not provided. "
|
||||
|
||||
Reference in New Issue
Block a user