Implement initial scan tracking for one-time setup
- Add SystemSettings model to track setup completion status - Create SystemSettingsService for managing setup flags - Modify fastapi_app startup to check and set initial_scan_completed flag - Anime folder scanning now only runs on first startup - Update DATABASE.md with new system_settings table documentation - Add unit test for SystemSettingsService functionality This ensures expensive one-time operations like scanning the entire anime directory only occur during initial setup, not on every application restart.
This commit is contained in:
@@ -39,6 +39,7 @@ from src.server.database.models import (
|
||||
AnimeSeries,
|
||||
DownloadQueueItem,
|
||||
Episode,
|
||||
SystemSettings,
|
||||
UserSession,
|
||||
)
|
||||
from src.server.database.service import (
|
||||
@@ -47,6 +48,7 @@ from src.server.database.service import (
|
||||
EpisodeService,
|
||||
UserSessionService,
|
||||
)
|
||||
from src.server.database.system_settings_service import SystemSettingsService
|
||||
|
||||
__all__ = [
|
||||
# Base and connection
|
||||
@@ -69,10 +71,12 @@ __all__ = [
|
||||
"AnimeSeries",
|
||||
"Episode",
|
||||
"DownloadQueueItem",
|
||||
"SystemSettings",
|
||||
"UserSession",
|
||||
# Services
|
||||
"AnimeSeriesService",
|
||||
"EpisodeService",
|
||||
"DownloadQueueService",
|
||||
"SystemSettingsService",
|
||||
"UserSessionService",
|
||||
]
|
||||
|
||||
@@ -36,6 +36,7 @@ EXPECTED_TABLES = {
|
||||
"episodes",
|
||||
"download_queue",
|
||||
"user_sessions",
|
||||
"system_settings",
|
||||
}
|
||||
|
||||
# Expected indexes for performance
|
||||
|
||||
@@ -543,3 +543,52 @@ class UserSession(Base, TimestampMixin):
|
||||
def revoke(self) -> None:
|
||||
"""Revoke this session."""
|
||||
self.is_active = False
|
||||
|
||||
|
||||
class SystemSettings(Base, TimestampMixin):
|
||||
"""SQLAlchemy model for system-wide settings and state.
|
||||
|
||||
Stores application-level configuration and state flags that persist
|
||||
across restarts. Used to track initialization status and setup completion.
|
||||
|
||||
Attributes:
|
||||
id: Primary key (single row expected)
|
||||
initial_scan_completed: Whether the initial anime folder scan has been completed
|
||||
initial_nfo_scan_completed: Whether the initial NFO scan has been completed
|
||||
initial_media_scan_completed: Whether the initial media scan has been completed
|
||||
last_scan_timestamp: Timestamp of the last completed scan
|
||||
created_at: Creation timestamp (from TimestampMixin)
|
||||
updated_at: Last update timestamp (from TimestampMixin)
|
||||
"""
|
||||
__tablename__ = "system_settings"
|
||||
|
||||
# Primary key (only one row should exist)
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True
|
||||
)
|
||||
|
||||
# Setup/initialization tracking
|
||||
initial_scan_completed: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=False, server_default="0",
|
||||
doc="Whether the initial anime folder scan has been completed"
|
||||
)
|
||||
initial_nfo_scan_completed: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=False, server_default="0",
|
||||
doc="Whether the initial NFO scan has been completed"
|
||||
)
|
||||
initial_media_scan_completed: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=False, server_default="0",
|
||||
doc="Whether the initial media scan has been completed"
|
||||
)
|
||||
last_scan_timestamp: Mapped[Optional[datetime]] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True,
|
||||
doc="Timestamp of the last completed scan"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<SystemSettings(id={self.id}, "
|
||||
f"initial_scan_completed={self.initial_scan_completed}, "
|
||||
f"initial_nfo_scan_completed={self.initial_nfo_scan_completed}, "
|
||||
f"initial_media_scan_completed={self.initial_media_scan_completed})>"
|
||||
)
|
||||
|
||||
159
src/server/database/system_settings_service.py
Normal file
159
src/server/database/system_settings_service.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""System settings service for managing application-level configuration.
|
||||
|
||||
This module provides services for managing system-wide settings and state,
|
||||
including tracking initial setup completion status.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
import structlog
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.server.database.models import SystemSettings
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class SystemSettingsService:
|
||||
"""Service for managing system settings."""
|
||||
|
||||
@staticmethod
|
||||
async def get_or_create(db: AsyncSession) -> SystemSettings:
|
||||
"""Get the system settings record, creating it if it doesn't exist.
|
||||
|
||||
Only one system settings record should exist in the database.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
SystemSettings instance
|
||||
"""
|
||||
# Try to get existing settings
|
||||
stmt = select(SystemSettings).limit(1)
|
||||
result = await db.execute(stmt)
|
||||
settings = result.scalar_one_or_none()
|
||||
|
||||
if settings is None:
|
||||
# Create new settings with defaults
|
||||
settings = SystemSettings(
|
||||
initial_scan_completed=False,
|
||||
initial_nfo_scan_completed=False,
|
||||
initial_media_scan_completed=False,
|
||||
)
|
||||
db.add(settings)
|
||||
await db.commit()
|
||||
await db.refresh(settings)
|
||||
logger.info("Created new system settings record")
|
||||
|
||||
return settings
|
||||
|
||||
@staticmethod
|
||||
async def is_initial_scan_completed(db: AsyncSession) -> bool:
|
||||
"""Check if the initial anime folder scan has been completed.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
True if initial scan is completed, False otherwise
|
||||
"""
|
||||
settings = await SystemSettingsService.get_or_create(db)
|
||||
return settings.initial_scan_completed
|
||||
|
||||
@staticmethod
|
||||
async def mark_initial_scan_completed(
|
||||
db: AsyncSession,
|
||||
timestamp: Optional[datetime] = None
|
||||
) -> None:
|
||||
"""Mark the initial anime folder scan as completed.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
timestamp: Optional timestamp to set, defaults to current time
|
||||
"""
|
||||
settings = await SystemSettingsService.get_or_create(db)
|
||||
settings.initial_scan_completed = True
|
||||
settings.last_scan_timestamp = timestamp or datetime.now(timezone.utc)
|
||||
await db.commit()
|
||||
logger.info("Marked initial scan as completed")
|
||||
|
||||
@staticmethod
|
||||
async def is_initial_nfo_scan_completed(db: AsyncSession) -> bool:
|
||||
"""Check if the initial NFO scan has been completed.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
True if initial NFO scan is completed, False otherwise
|
||||
"""
|
||||
settings = await SystemSettingsService.get_or_create(db)
|
||||
return settings.initial_nfo_scan_completed
|
||||
|
||||
@staticmethod
|
||||
async def mark_initial_nfo_scan_completed(
|
||||
db: AsyncSession,
|
||||
timestamp: Optional[datetime] = None
|
||||
) -> None:
|
||||
"""Mark the initial NFO scan as completed.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
timestamp: Optional timestamp to set, defaults to current time
|
||||
"""
|
||||
settings = await SystemSettingsService.get_or_create(db)
|
||||
settings.initial_nfo_scan_completed = True
|
||||
if timestamp:
|
||||
settings.last_scan_timestamp = timestamp
|
||||
await db.commit()
|
||||
logger.info("Marked initial NFO scan as completed")
|
||||
|
||||
@staticmethod
|
||||
async def is_initial_media_scan_completed(db: AsyncSession) -> bool:
|
||||
"""Check if the initial media scan has been completed.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
True if initial media scan is completed, False otherwise
|
||||
"""
|
||||
settings = await SystemSettingsService.get_or_create(db)
|
||||
return settings.initial_media_scan_completed
|
||||
|
||||
@staticmethod
|
||||
async def mark_initial_media_scan_completed(
|
||||
db: AsyncSession,
|
||||
timestamp: Optional[datetime] = None
|
||||
) -> None:
|
||||
"""Mark the initial media scan as completed.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
timestamp: Optional timestamp to set, defaults to current time
|
||||
"""
|
||||
settings = await SystemSettingsService.get_or_create(db)
|
||||
settings.initial_media_scan_completed = True
|
||||
if timestamp:
|
||||
settings.last_scan_timestamp = timestamp
|
||||
await db.commit()
|
||||
logger.info("Marked initial media scan as completed")
|
||||
|
||||
@staticmethod
|
||||
async def reset_all_scans(db: AsyncSession) -> None:
|
||||
"""Reset all scan completion flags (for testing or re-setup).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
"""
|
||||
settings = await SystemSettingsService.get_or_create(db)
|
||||
settings.initial_scan_completed = False
|
||||
settings.initial_nfo_scan_completed = False
|
||||
settings.initial_media_scan_completed = False
|
||||
settings.last_scan_timestamp = None
|
||||
await db.commit()
|
||||
logger.info("Reset all scan completion flags")
|
||||
Reference in New Issue
Block a user