"""Settings API: import filters, system stats.""" from quart import Blueprint, jsonify, request from sqlalchemy import func, select from ..extensions import get_session from ..models import ImageRecord, ImportBatch, ImportSettings, ImportTask, Tag settings_bp = Blueprint("settings", __name__, url_prefix="/api") _EDITABLE_FIELDS = ( "min_width", "min_height", "skip_transparent", "transparency_threshold", "skip_single_color", "single_color_threshold", "single_color_tolerance", "phash_threshold", ) @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() return jsonify({ "min_width": row.min_width, "min_height": row.min_height, "skip_transparent": row.skip_transparent, "transparency_threshold": row.transparency_threshold, "skip_single_color": row.skip_single_color, "single_color_threshold": row.single_color_threshold, "single_color_tolerance": row.single_color_tolerance, "phash_threshold": row.phash_threshold, }) @settings_bp.route("/settings/import", methods=["PATCH"]) async def update_import_settings(): body = await request.get_json() if not isinstance(body, dict): return jsonify({"error": "body must be a JSON object"}), 400 if "phash_threshold" in body and ( not isinstance(body["phash_threshold"], int) or isinstance(body["phash_threshold"], bool) or body["phash_threshold"] < 0 ): return jsonify( {"error": "phash_threshold must be a non-negative integer"} ), 400 async with get_session() as session: row = ( await session.execute(select(ImportSettings).where(ImportSettings.id == 1)) ).scalar_one() for field in _EDITABLE_FIELDS: if field in body: setattr(row, field, body[field]) await session.commit() return await get_import_settings() @settings_bp.route("/system/stats", methods=["GET"]) async def system_stats(): async with get_session() as session: total_images = (await session.execute(select(func.count(ImageRecord.id)))).scalar_one() total_tags = (await session.execute(select(func.count(Tag.id)))).scalar_one() storage_bytes = ( (await session.execute(select(func.coalesce(func.sum(ImageRecord.size_bytes), 0)))).scalar_one() ) # Task counts grouped by status status_rows = ( await session.execute( select(ImportTask.status, func.count(ImportTask.id)).group_by(ImportTask.status) ) ).all() status_counts = {row[0]: row[1] for row in status_rows} # Integrity counts — FC-2e. integrity_rows = ( await session.execute( select( ImageRecord.integrity_status, func.count(ImageRecord.id), ).group_by(ImageRecord.integrity_status) ) ).all() integrity_counts = {row[0]: row[1] for row in integrity_rows} # Active batch (most recent running) active_batch_row = ( await session.execute( select(ImportBatch) .where(ImportBatch.status == "running") .order_by(ImportBatch.started_at.desc()) .limit(1) ) ).scalar_one_or_none() active_batch = None if active_batch_row: active_batch = { "id": active_batch_row.id, "source_path": active_batch_row.source_path, "started_at": active_batch_row.started_at.isoformat(), "total_files": active_batch_row.total_files, "imported": active_batch_row.imported, "skipped": active_batch_row.skipped, "failed": active_batch_row.failed, } return jsonify({ "total_images": total_images, "total_tags": total_tags, "storage_bytes": storage_bytes, "tasks": { "pending": status_counts.get("pending", 0), "queued": status_counts.get("queued", 0), "processing": status_counts.get("processing", 0), "complete": status_counts.get("complete", 0), "skipped": status_counts.get("skipped", 0), "failed": status_counts.get("failed", 0), }, "integrity": { "unknown": int(integrity_counts.get("unknown", 0)), "ok": int(integrity_counts.get("ok", 0)), "corrupt": int(integrity_counts.get("corrupt", 0)), "failed_verification": int( integrity_counts.get("failed_verification", 0) ), }, "active_batch": active_batch, })