diff --git a/src/server/api/auth.py b/src/server/api/auth.py index 12a67d3..8f84cb5 100644 --- a/src/server/api/auth.py +++ b/src/server/api/auth.py @@ -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"} diff --git a/src/server/fastapi_app.py b/src/server/fastapi_app.py index 82d295f..6da9536 100644 --- a/src/server/fastapi_app.py +++ b/src/server/fastapi_app.py @@ -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 - " diff --git a/src/server/services/initialization_service.py b/src/server/services/initialization_service.py new file mode 100644 index 0000000..52d44dc --- /dev/null +++ b/src/server/services/initialization_service.py @@ -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" + )