Files
FabledCurator/backend/app/api/settings.py
T
2026-05-19 17:53:12 -04:00

140 lines
4.8 KiB
Python

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