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:
@@ -117,46 +117,26 @@ async def setup_auth(req: SetupRequest):
|
|||||||
# Save the config with all updates
|
# Save the config with all updates
|
||||||
config_service.save_config(config, create_backup=False)
|
config_service.save_config(config, create_backup=False)
|
||||||
|
|
||||||
# Sync series from data files to database if anime directory is set
|
# Perform initial setup (series sync, NFO scan, media scan)
|
||||||
if anime_directory:
|
# This ensures everything is initialized immediately after setup
|
||||||
try:
|
# without requiring an application restart
|
||||||
import structlog
|
# Reload settings to pick up the new configuration
|
||||||
|
from src.config.settings import settings
|
||||||
from src.server.database.connection import get_db_session
|
from src.server.services.initialization_service import (
|
||||||
from src.server.database.system_settings_service import (
|
perform_initial_setup,
|
||||||
SystemSettingsService,
|
perform_nfo_scan_if_needed,
|
||||||
)
|
)
|
||||||
from src.server.services.anime_service import (
|
settings.reload()
|
||||||
sync_series_from_data_files,
|
|
||||||
)
|
# Perform the initial series sync and mark as completed
|
||||||
logger = structlog.get_logger(__name__)
|
await perform_initial_setup()
|
||||||
sync_count = await sync_series_from_data_files(
|
|
||||||
anime_directory, logger
|
# Perform NFO scan if configured
|
||||||
)
|
await perform_nfo_scan_if_needed()
|
||||||
logger.info(
|
|
||||||
"Setup complete: synced series from data files",
|
# Note: Media scan is skipped during setup as it requires
|
||||||
count=sync_count
|
# background_loader service which is only available during
|
||||||
)
|
# application lifespan. It will run on first application startup.
|
||||||
|
|
||||||
# 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|||||||
@@ -186,35 +186,15 @@ async def lifespan(_application: FastAPI):
|
|||||||
# Subscribe to progress events
|
# Subscribe to progress events
|
||||||
progress_service.subscribe("progress_updated", progress_event_handler)
|
progress_service.subscribe("progress_updated", progress_event_handler)
|
||||||
|
|
||||||
# Check if initial setup has been completed
|
# Perform initial setup (series sync and marking as completed)
|
||||||
try:
|
# This is centralized in initialization_service and also called
|
||||||
from src.server.database.connection import get_db_session
|
# from the setup endpoint
|
||||||
from src.server.database.system_settings_service import (
|
from src.server.services.initialization_service import (
|
||||||
SystemSettingsService,
|
perform_initial_setup,
|
||||||
)
|
perform_media_scan_if_needed,
|
||||||
|
perform_nfo_scan_if_needed,
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Checking anime_directory setting: '%s'",
|
"Checking anime_directory setting: '%s'",
|
||||||
@@ -222,103 +202,15 @@ async def lifespan(_application: FastAPI):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if settings.anime_directory:
|
if settings.anime_directory:
|
||||||
# Only sync from data files on first run
|
# Perform initial setup if needed
|
||||||
if not is_initial_scan_done:
|
await perform_initial_setup()
|
||||||
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
|
# Get anime service for later use
|
||||||
from src.server.utils.dependencies import get_anime_service
|
from src.server.utils.dependencies import get_anime_service
|
||||||
anime_service = 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)
|
# Run NFO scan only on first run (if configured)
|
||||||
if settings.tmdb_api_key and (
|
await perform_nfo_scan_if_needed()
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Now initialize download service (will use data from database)
|
# Now initialize download service (will use data from database)
|
||||||
from src.server.utils.dependencies import get_download_service
|
from src.server.utils.dependencies import get_download_service
|
||||||
@@ -341,52 +233,8 @@ async def lifespan(_application: FastAPI):
|
|||||||
await background_loader.start()
|
await background_loader.start()
|
||||||
logger.info("Background loader service started")
|
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
|
# Run media scan only on first run
|
||||||
if not is_media_scan_done:
|
await perform_media_scan_if_needed(background_loader)
|
||||||
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"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Download service initialization skipped - "
|
"Download service initialization skipped - "
|
||||||
|
|||||||
231
src/server/services/initialization_service.py
Normal file
231
src/server/services/initialization_service.py
Normal 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"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user