refactor: centralize initialization logic in dedicated service

- Create initialization_service.py with shared initialization functions
- Extract setup logic from lifespan and setup endpoint into reusable functions
- Setup endpoint now calls perform_initial_setup() directly
- Lifespan startup calls the same shared functions
- Eliminates code duplication between setup and lifespan
- Ensures consistent initialization behavior regardless of entry point
This commit is contained in:
2026-01-23 14:37:07 +01:00
parent 8e8487b7b7
commit 50e0b21669
3 changed files with 265 additions and 206 deletions

View File

@@ -117,46 +117,26 @@ async def setup_auth(req: SetupRequest):
# Save the config with all updates
config_service.save_config(config, create_backup=False)
# Sync series from data files to database if anime directory is set
if anime_directory:
try:
import structlog
from src.server.database.connection import get_db_session
from src.server.database.system_settings_service import (
SystemSettingsService,
)
from src.server.services.anime_service import (
sync_series_from_data_files,
)
logger = structlog.get_logger(__name__)
sync_count = await sync_series_from_data_files(
anime_directory, logger
)
logger.info(
"Setup complete: synced series from data files",
count=sync_count
)
# Mark initial scan as completed
try:
async with get_db_session() as db:
await SystemSettingsService.mark_initial_scan_completed(
db
)
logger.info("Marked initial scan as completed")
except Exception as mark_error:
logger.warning(
"Failed to mark initial scan as completed",
error=str(mark_error)
)
except Exception as e:
# Log but don't fail setup if sync fails
import structlog
structlog.get_logger(__name__).warning(
"Failed to sync series after setup",
error=str(e)
)
# Perform initial setup (series sync, NFO scan, media scan)
# This ensures everything is initialized immediately after setup
# without requiring an application restart
# Reload settings to pick up the new configuration
from src.config.settings import settings
from src.server.services.initialization_service import (
perform_initial_setup,
perform_nfo_scan_if_needed,
)
settings.reload()
# Perform the initial series sync and mark as completed
await perform_initial_setup()
# Perform NFO scan if configured
await perform_nfo_scan_if_needed()
# Note: Media scan is skipped during setup as it requires
# background_loader service which is only available during
# application lifespan. It will run on first application startup.
return {"status": "ok"}

View File

@@ -186,35 +186,15 @@ async def lifespan(_application: FastAPI):
# Subscribe to progress events
progress_service.subscribe("progress_updated", progress_event_handler)
# Check if initial setup has been completed
try:
from src.server.database.connection import get_db_session
from src.server.database.system_settings_service import (
SystemSettingsService,
)
async with get_db_session() as db:
is_initial_scan_done = (
await SystemSettingsService.is_initial_scan_completed(db)
)
if is_initial_scan_done:
logger.info(
"Initial scan already completed, skipping data file sync"
)
else:
logger.info(
"Initial scan not completed, "
"performing first-time setup"
)
except Exception as e:
logger.warning(
"Failed to check system settings: %s, assuming first run", e
)
is_initial_scan_done = False
# Sync series from data files to database (only on first run)
# This must happen before SeriesApp initialization
# Perform initial setup (series sync and marking as completed)
# This is centralized in initialization_service and also called
# from the setup endpoint
from src.server.services.initialization_service import (
perform_initial_setup,
perform_media_scan_if_needed,
perform_nfo_scan_if_needed,
)
try:
logger.info(
"Checking anime_directory setting: '%s'",
@@ -222,103 +202,15 @@ async def lifespan(_application: FastAPI):
)
if settings.anime_directory:
# Only sync from data files on first run
if not is_initial_scan_done:
logger.info("Performing initial anime folder scan...")
sync_count = await sync_series_from_data_files(
settings.anime_directory
)
logger.info(
"Data file sync complete. Added %d series.", sync_count
)
# Mark initial scan as completed
try:
async with get_db_session() as db:
await (
SystemSettingsService
.mark_initial_scan_completed(db)
)
logger.info("Marked initial scan as completed")
except Exception as e:
logger.warning(
"Failed to mark initial scan as completed: %s", e
)
else:
logger.info(
"Skipping initial scan - "
"already completed on previous run"
)
# Perform initial setup if needed
await perform_initial_setup()
# Load series from database into SeriesApp's in-memory cache
# Get anime service for later use
from src.server.utils.dependencies import get_anime_service
anime_service = get_anime_service()
await anime_service._load_series_from_db()
logger.info("Series loaded from database into memory")
# Check if initial NFO scan has been completed
try:
async with get_db_session() as db:
is_nfo_scan_done = (
await SystemSettingsService
.is_initial_nfo_scan_completed(db)
)
except Exception as e:
logger.warning(
"Failed to check NFO scan status: %s, assuming not done",
e
)
is_nfo_scan_done = False
# Run NFO scan only on first run (if configured)
if settings.tmdb_api_key and (
settings.nfo_auto_create or settings.nfo_update_on_scan
):
if not is_nfo_scan_done:
logger.info("Performing initial NFO scan...")
try:
from src.core.services.series_manager_service import (
SeriesManagerService,
)
manager = SeriesManagerService.from_settings()
await manager.scan_and_process_nfo()
await manager.close()
logger.info("Initial NFO scan completed")
# Mark NFO scan as completed
try:
async with get_db_session() as db:
await (
SystemSettingsService
.mark_initial_nfo_scan_completed(db)
)
logger.info("Marked NFO scan as completed")
except Exception as e:
logger.warning(
"Failed to mark NFO scan as completed: %s",
e
)
except Exception as e:
logger.error(
"Failed to complete NFO scan: %s",
e,
exc_info=True
)
else:
logger.info(
"Skipping NFO scan - already completed on previous run"
)
else:
if not settings.tmdb_api_key:
logger.info(
"NFO scan skipped - TMDB API key not configured"
)
else:
logger.info(
"NFO scan skipped - auto_create and update_on_scan "
"both disabled"
)
await perform_nfo_scan_if_needed()
# Now initialize download service (will use data from database)
from src.server.utils.dependencies import get_download_service
@@ -341,52 +233,8 @@ async def lifespan(_application: FastAPI):
await background_loader.start()
logger.info("Background loader service started")
# Check if initial media scan has been completed
is_media_scan_done = False
try:
async with get_db_session() as db:
is_media_scan_done = (
await SystemSettingsService
.is_initial_media_scan_completed(db)
)
except Exception as e:
logger.warning(
"Failed to check media scan status: %s, assuming not done",
e
)
is_media_scan_done = False
# Run media scan only on first run
if not is_media_scan_done:
logger.info("Performing initial media scan...")
try:
# Check for incomplete series and queue background loading
await _check_incomplete_series_on_startup(background_loader)
logger.info("Initial media scan completed")
# Mark media scan as completed
try:
async with get_db_session() as db:
await (
SystemSettingsService
.mark_initial_media_scan_completed(db)
)
logger.info("Marked media scan as completed")
except Exception as e:
logger.warning(
"Failed to mark media scan as completed: %s",
e
)
except Exception as e:
logger.error(
"Failed to complete media scan: %s",
e,
exc_info=True
)
else:
logger.info(
"Skipping media scan - already completed on previous run"
)
await perform_media_scan_if_needed(background_loader)
else:
logger.info(
"Download service initialization skipped - "

View File

@@ -0,0 +1,231 @@
"""Centralized initialization service for application startup and setup."""
import structlog
from src.config.settings import settings
from src.server.services.anime_service import sync_series_from_data_files
logger = structlog.get_logger(__name__)
async def perform_initial_setup():
"""Perform initial setup including series sync and scan completion marking.
This function is called both during application lifespan startup
and when the setup endpoint is completed. It ensures that:
1. Series are synced from data files to database
2. Initial scan is marked as completed
3. Series are loaded into memory
4. NFO scan is performed if configured
5. Media scan is performed
Returns:
bool: True if initialization was performed, False if skipped
"""
from src.server.database.connection import get_db_session
from src.server.database.system_settings_service import SystemSettingsService
# Check if initial setup has been completed
try:
async with get_db_session() as db:
is_initial_scan_done = (
await SystemSettingsService.is_initial_scan_completed(db)
)
if is_initial_scan_done:
logger.info(
"Initial scan already completed, skipping data file sync"
)
return False
else:
logger.info(
"Initial scan not completed, "
"performing first-time setup"
)
except Exception as e:
logger.warning(
"Failed to check system settings: %s, assuming first run", e
)
is_initial_scan_done = False
# Sync series from data files to database (only on first run)
try:
logger.info(
"Checking anime_directory setting: '%s'",
settings.anime_directory
)
if not settings.anime_directory:
logger.info(
"Initialization skipped - anime directory not configured"
)
return False
# Only sync from data files on first run
if not is_initial_scan_done:
logger.info("Performing initial anime folder scan...")
sync_count = await sync_series_from_data_files(
settings.anime_directory
)
logger.info(
"Data file sync complete. Added %d series.", sync_count
)
# Mark initial scan as completed
try:
async with get_db_session() as db:
await (
SystemSettingsService
.mark_initial_scan_completed(db)
)
logger.info("Marked initial scan as completed")
except Exception as e:
logger.warning(
"Failed to mark initial scan as completed: %s", e
)
else:
logger.info(
"Skipping initial scan - "
"already completed on previous run"
)
# Load series from database into SeriesApp's in-memory cache
from src.server.utils.dependencies import get_anime_service
anime_service = get_anime_service()
await anime_service._load_series_from_db()
logger.info("Series loaded from database into memory")
return True
except (OSError, RuntimeError, ValueError) as e:
logger.warning("Failed to perform initial setup: %s", e)
return False
async def perform_nfo_scan_if_needed():
"""Perform initial NFO scan if not yet completed and configured."""
from src.server.database.connection import get_db_session
from src.server.database.system_settings_service import SystemSettingsService
# Check if initial NFO scan has been completed
try:
async with get_db_session() as db:
is_nfo_scan_done = (
await SystemSettingsService
.is_initial_nfo_scan_completed(db)
)
except Exception as e:
logger.warning(
"Failed to check NFO scan status: %s, assuming not done",
e
)
is_nfo_scan_done = False
# Run NFO scan only on first run (if configured)
if settings.tmdb_api_key and (
settings.nfo_auto_create or settings.nfo_update_on_scan
):
if not is_nfo_scan_done:
logger.info("Performing initial NFO scan...")
try:
from src.core.services.series_manager_service import (
SeriesManagerService,
)
manager = SeriesManagerService.from_settings()
await manager.scan_and_process_nfo()
await manager.close()
logger.info("Initial NFO scan completed")
# Mark NFO scan as completed
try:
async with get_db_session() as db:
await (
SystemSettingsService
.mark_initial_nfo_scan_completed(db)
)
logger.info("Marked NFO scan as completed")
except Exception as e:
logger.warning(
"Failed to mark NFO scan as completed: %s",
e
)
except Exception as e:
logger.error(
"Failed to complete NFO scan: %s",
e,
exc_info=True
)
else:
logger.info(
"Skipping NFO scan - already completed on previous run"
)
else:
if not settings.tmdb_api_key:
logger.info(
"NFO scan skipped - TMDB API key not configured"
)
else:
logger.info(
"NFO scan skipped - auto_create and update_on_scan "
"both disabled"
)
async def perform_media_scan_if_needed(background_loader):
"""Perform initial media scan if not yet completed.
Args:
background_loader: The background loader service instance
"""
from src.server.database.connection import get_db_session
from src.server.database.system_settings_service import SystemSettingsService
# Check if initial media scan has been completed
is_media_scan_done = False
try:
async with get_db_session() as db:
is_media_scan_done = (
await SystemSettingsService
.is_initial_media_scan_completed(db)
)
except Exception as e:
logger.warning(
"Failed to check media scan status: %s, assuming not done",
e
)
is_media_scan_done = False
# Run media scan only on first run
if not is_media_scan_done:
logger.info("Performing initial media scan...")
try:
# Import the helper function from fastapi_app
from src.server.fastapi_app import _check_incomplete_series_on_startup
# Check for incomplete series and queue background loading
await _check_incomplete_series_on_startup(background_loader)
logger.info("Initial media scan completed")
# Mark media scan as completed
try:
async with get_db_session() as db:
await (
SystemSettingsService
.mark_initial_media_scan_completed(db)
)
logger.info("Marked media scan as completed")
except Exception as e:
logger.warning(
"Failed to mark media scan as completed: %s",
e
)
except Exception as e:
logger.error(
"Failed to complete media scan: %s",
e,
exc_info=True
)
else:
logger.info(
"Skipping media scan - already completed on previous run"
)