diff --git a/backend/app/api/import_admin.py b/backend/app/api/import_admin.py index 7ad3fa7..36575c3 100644 --- a/backend/app/api/import_admin.py +++ b/backend/app/api/import_admin.py @@ -159,9 +159,7 @@ def _refetch_task_sync(session, task_id: int) -> dict: return {"status": "not_found"} if task.status != "failed": return {"status": "not_failed"} - settings = session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - ).scalar_one() + settings = ImportSettings.load_sync(session) return attempt_refetch(session, task, Path(settings.import_scan_path)) diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index 145890e..fa97bda 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -31,9 +31,7 @@ _EDITABLE_FIELDS = ( @settings_bp.route("/settings/import", methods=["GET"]) async def get_import_settings(): async with get_session() as session: - row = ( - await session.execute(select(ImportSettings).where(ImportSettings.id == 1)) - ).scalar_one() + row = await ImportSettings.load(session) return jsonify({ "min_width": row.min_width, "min_height": row.min_height, @@ -99,9 +97,7 @@ async def update_import_settings(): return _bad_int("download_failure_warning_threshold", 1, 100) async with get_session() as session: - row = ( - await session.execute(select(ImportSettings).where(ImportSettings.id == 1)) - ).scalar_one() + row = await ImportSettings.load(session) for field in _EDITABLE_FIELDS: if field in body: setattr(row, field, body[field]) diff --git a/backend/app/api/system_backup.py b/backend/app/api/system_backup.py index fcf57c7..a0f9ef2 100644 --- a/backend/app/api/system_backup.py +++ b/backend/app/api/system_backup.py @@ -227,9 +227,7 @@ async def delete_run(run_id: int): @system_backup_bp.route("/settings", methods=["GET"]) async def get_settings(): async with get_session() as session: - row = (await session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - )).scalar_one() + row = await ImportSettings.load(session) return jsonify({ "backup_db_nightly_enabled": row.backup_db_nightly_enabled, "backup_db_nightly_hour_utc": row.backup_db_nightly_hour_utc, @@ -249,9 +247,7 @@ async def patch_settings(): return err async with get_session() as session: - row = (await session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - )).scalar_one() + row = await ImportSettings.load(session) for field in _BACKUP_SETTINGS_FIELDS: if field in body: setattr(row, field, body[field]) diff --git a/backend/app/models/import_settings.py b/backend/app/models/import_settings.py index 323fbf6..aa874ea 100644 --- a/backend/app/models/import_settings.py +++ b/backend/app/models/import_settings.py @@ -4,7 +4,7 @@ Enforced as a single row via a CHECK (id = 1) constraint. The application always SELECTs id=1 and never inserts/deletes after the initial migration. """ -from sqlalchemy import Boolean, CheckConstraint, Float, Integer, Text +from sqlalchemy import Boolean, CheckConstraint, Float, Integer, Text, select from sqlalchemy.orm import Mapped, mapped_column from .base import Base @@ -63,3 +63,13 @@ class ImportSettings(Base): backup_images_keep_last_n: Mapped[int] = mapped_column( Integer, nullable=False, default=3, ) + + @classmethod + async def load(cls, session) -> "ImportSettings": + """The singleton settings row (id=1), via an async session.""" + return (await session.execute(select(cls).where(cls.id == 1))).scalar_one() + + @classmethod + def load_sync(cls, session) -> "ImportSettings": + """The singleton settings row (id=1), via a sync session.""" + return session.execute(select(cls).where(cls.id == 1)).scalar_one() diff --git a/backend/app/services/scheduler_service.py b/backend/app/services/scheduler_service.py index 2008eba..e70c87f 100644 --- a/backend/app/services/scheduler_service.py +++ b/backend/app/services/scheduler_service.py @@ -58,9 +58,7 @@ async def select_due_sources(session: AsyncSession) -> list[Source]: .where(Artist.auto_check.is_(True)) )).scalars().all() - settings = (await session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - )).scalar_one() + settings = await ImportSettings.load(session) now = datetime.now(UTC) due: list[Source] = [] @@ -121,9 +119,7 @@ async def scheduler_status(session: AsyncSession) -> dict: .where(Source.enabled.is_(True)) .where(Artist.auto_check.is_(True)) )).scalars().all() - settings = (await session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - )).scalar_one() + settings = await ImportSettings.load(session) now = datetime.now(UTC) due_now = 0 diff --git a/backend/app/services/source_service.py b/backend/app/services/source_service.py index ec17bed..85a6564 100644 --- a/backend/app/services/source_service.py +++ b/backend/app/services/source_service.py @@ -120,9 +120,7 @@ class SourceService: return config async def _load_settings(self) -> ImportSettings: - return (await self.session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - )).scalar_one() + return await ImportSettings.load(self.session) def _build_record( self, source: Source, artist: Artist, settings: ImportSettings, diff --git a/backend/app/tasks/backup.py b/backend/app/tasks/backup.py index 9d2fefb..da7c841 100644 --- a/backend/app/tasks/backup.py +++ b/backend/app/tasks/backup.py @@ -246,9 +246,7 @@ def prune_backups() -> dict: SessionLocal = _sync_session_factory() counts = {"db_deleted": 0, "images_deleted": 0, "files_unlinked": 0} with SessionLocal() as session: - s = session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - ).scalar_one() + s = ImportSettings.load_sync(session) for kind, keep in ( ("db", s.backup_db_keep_last_n), ("images", s.backup_images_keep_last_n), @@ -286,9 +284,7 @@ def backup_db_nightly() -> dict: either {'skipped': ''} or {'dispatched': ''}.""" SessionLocal = _sync_session_factory() with SessionLocal() as session: - s = session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - ).scalar_one() + s = ImportSettings.load_sync(session) nightly_enabled = s.backup_db_nightly_enabled configured_hour = s.backup_db_nightly_hour_utc if not nightly_enabled: diff --git a/backend/app/tasks/download.py b/backend/app/tasks/download.py index fa82212..7cd7c13 100644 --- a/backend/app/tasks/download.py +++ b/backend/app/tasks/download.py @@ -3,7 +3,6 @@ import asyncio from pathlib import Path -from sqlalchemy import select from sqlalchemy.exc import DBAPIError, OperationalError from ..celery_app import celery @@ -41,9 +40,7 @@ def download_source(self, source_id: int) -> int: SyncFactory = _sync_session_factory() try: with SyncFactory() as sync_session: - settings = sync_session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - ).scalar_one() + settings = ImportSettings.load_sync(sync_session) rate_limit = settings.download_rate_limit_seconds validate_files = settings.download_validate_files @@ -57,9 +54,7 @@ def download_source(self, source_id: int) -> int: async with async_factory() as async_session: cred_service = CredentialService(async_session, crypto) with SyncFactory() as sync_session: - sync_settings = sync_session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - ).scalar_one() + sync_settings = ImportSettings.load_sync(sync_session) importer = Importer( session=sync_session, images_root=IMAGES_ROOT, diff --git a/backend/app/tasks/import_file.py b/backend/app/tasks/import_file.py index 4a124d1..49c798d 100644 --- a/backend/app/tasks/import_file.py +++ b/backend/app/tasks/import_file.py @@ -167,9 +167,7 @@ def enqueue_import(task_id: int, task_type: str) -> None: def _do_import(session, task, import_task_id: int) -> dict: """Actual work, called from inside the resilience wrapper.""" - settings = session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - ).scalar_one() + settings = ImportSettings.load_sync(session) import_root = Path(settings.import_scan_path) batch = session.get(ImportBatch, task.batch_id) deep = bool(batch and batch.scan_mode == "deep") diff --git a/backend/app/tasks/maintenance.py b/backend/app/tasks/maintenance.py index 4584d01..8bd321b 100644 --- a/backend/app/tasks/maintenance.py +++ b/backend/app/tasks/maintenance.py @@ -460,9 +460,7 @@ def cleanup_old_download_events() -> int: """ SessionLocal = _sync_session_factory() with SessionLocal() as session: - settings = session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - ).scalar_one() + settings = ImportSettings.load_sync(session) retention_days = settings.download_event_retention_days cutoff = datetime.now(UTC) - timedelta(days=retention_days) result = session.execute( diff --git a/backend/app/tasks/scan.py b/backend/app/tasks/scan.py index c5f3dff..90398fa 100644 --- a/backend/app/tasks/scan.py +++ b/backend/app/tasks/scan.py @@ -44,9 +44,7 @@ def scan_directory(self, triggered_by: str = "manual", batch id.""" SessionLocal = _sync_session_factory() with SessionLocal() as session: - settings = session.execute( - select(ImportSettings).where(ImportSettings.id == 1) - ).scalar_one() + settings = ImportSettings.load_sync(session) import_root = Path(settings.import_scan_path) batch = ImportBatch(