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:
2026-01-21 19:22:50 +01:00
parent 35c82e68b7
commit bf3cfa00d5
8 changed files with 383 additions and 45 deletions

View File

@@ -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",
]

View File

@@ -36,6 +36,7 @@ EXPECTED_TABLES = {
"episodes",
"download_queue",
"user_sessions",
"system_settings",
}
# Expected indexes for performance

View File

@@ -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})>"
)

View 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")