diff --git a/alembic/versions/0006_fc2d_phash_threshold.py b/alembic/versions/0006_fc2d_phash_threshold.py new file mode 100644 index 0000000..895ed46 --- /dev/null +++ b/alembic/versions/0006_fc2d_phash_threshold.py @@ -0,0 +1,30 @@ +"""fc2d: import_settings.phash_threshold + +Revision ID: 0006 +Revises: 0005 +Create Date: 2026-05-17 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0006" +down_revision: Union[str, None] = "0005" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "import_settings", + sa.Column( + "phash_threshold", sa.Integer(), + nullable=False, server_default="10", + ), + ) + + +def downgrade() -> None: + op.drop_column("import_settings", "phash_threshold") diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index f5f5354..a91bd60 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -17,6 +17,7 @@ _EDITABLE_FIELDS = ( "skip_single_color", "single_color_threshold", "single_color_tolerance", + "phash_threshold", ) @@ -34,6 +35,7 @@ async def get_import_settings(): "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, }) @@ -43,6 +45,15 @@ async def update_import_settings(): 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)) diff --git a/backend/app/models/import_settings.py b/backend/app/models/import_settings.py index 9af4ed0..be65d26 100644 --- a/backend/app/models/import_settings.py +++ b/backend/app/models/import_settings.py @@ -28,3 +28,5 @@ class ImportSettings(Base): skip_single_color: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) single_color_threshold: Mapped[float] = mapped_column(Float, nullable=False, default=0.95) single_color_tolerance: Mapped[int] = mapped_column(Integer, nullable=False, default=30) + + phash_threshold: Mapped[int] = mapped_column(Integer, nullable=False, default=10) diff --git a/tests/test_api_settings.py b/tests/test_api_settings.py index e3f214a..a7f30b1 100644 --- a/tests/test_api_settings.py +++ b/tests/test_api_settings.py @@ -47,6 +47,30 @@ async def test_patch_rejects_non_object(client): assert resp.status_code == 400 +@pytest.mark.asyncio +async def test_import_settings_phash_threshold_default(client): + resp = await client.get("/api/settings/import") + assert resp.status_code == 200 + assert (await resp.get_json())["phash_threshold"] == 10 + + +@pytest.mark.asyncio +async def test_patch_phash_threshold(client): + resp = await client.patch( + "/api/settings/import", json={"phash_threshold": 4} + ) + assert resp.status_code == 200 + assert (await resp.get_json())["phash_threshold"] == 4 + + +@pytest.mark.asyncio +async def test_patch_phash_threshold_rejects_negative(client): + resp = await client.patch( + "/api/settings/import", json={"phash_threshold": -1} + ) + assert resp.status_code == 400 + + @pytest.mark.asyncio async def test_system_stats_shape(client): resp = await client.get("/api/system/stats")