- write-back: call upsert_tag inside _apply_tag's try so a tag-create failure is caught per-asset and apply_all no longer aborts mid-batch (was P1) - write-back: surface _pipeline/processed write failures in the result instead of discarding them (was reported as success) - ui: add title to the high confidence badge so approve-high-confidence's pre-action count is non-zero; confirm() before single-cluster apply - core: Store context manager; close DB connection even if a route raises; guard split_cluster against a first-member boundary (empty cluster); add writeback_log lookup index - ingest: split thumbnail download/write error handling and clean up the .tmp file on a write failure - tests: upsert/processed write-failure regression tests + split-guard test
128 lines
3.6 KiB
Python
128 lines
3.6 KiB
Python
from flask import Blueprint, current_app, render_template, request, jsonify, abort
|
|
from photoflow.core import Store
|
|
from app import review
|
|
|
|
bp = Blueprint("review", __name__)
|
|
|
|
|
|
def _store():
|
|
cfg = current_app.config["APP_CONFIG"]
|
|
return Store(cfg.db_path).connect()
|
|
|
|
|
|
def _client():
|
|
factory = current_app.config.get("IMMICH_FACTORY")
|
|
if factory:
|
|
return factory()
|
|
from photoflow.immich import ImmichClient
|
|
cfg = current_app.config["APP_CONFIG"]
|
|
return ImmichClient(cfg.immich_url, cfg.immich_api_key)
|
|
|
|
|
|
def _body():
|
|
return request.get_json(silent=True) or {}
|
|
|
|
|
|
@bp.route("/cluster/<int:cid>")
|
|
def detail(cid):
|
|
s = _store()
|
|
c = s.get_cluster(cid)
|
|
if c is None:
|
|
s.close()
|
|
abort(404)
|
|
members = s.cluster_members(cid)
|
|
prev_id, next_id = s.chronological_neighbors(cid)
|
|
s.close()
|
|
return render_template("_detail.html", c=c, members=members,
|
|
prev_id=prev_id, next_id=next_id)
|
|
|
|
|
|
@bp.route("/cluster/<int:cid>/approve", methods=["POST"])
|
|
def approve(cid):
|
|
s = _store()
|
|
review.approve(s, cid, _body().get("name"))
|
|
c = s.get_cluster(cid)
|
|
s.close()
|
|
return jsonify({"ok": True, "status": c.status, "name": c.decided_name})
|
|
|
|
|
|
@bp.route("/cluster/<int:cid>/non-trip", methods=["POST"])
|
|
def non_trip(cid):
|
|
s = _store()
|
|
review.mark_non_trip(s, cid)
|
|
s.close()
|
|
return jsonify({"ok": True, "status": "non_trip"})
|
|
|
|
|
|
@bp.route("/cluster/<int:cid>/skip", methods=["POST"])
|
|
def skip(cid):
|
|
s = _store()
|
|
review.skip(s, cid)
|
|
s.close()
|
|
return jsonify({"ok": True, "status": "skipped"})
|
|
|
|
|
|
@bp.route("/cluster/<int:cid>/split", methods=["POST"])
|
|
def split(cid):
|
|
s = _store()
|
|
boundary = _body().get("boundary_asset_id")
|
|
if not boundary:
|
|
s.close()
|
|
return jsonify({"error": "boundary_asset_id required"}), 400
|
|
try:
|
|
id1, id2 = review.split(s, cid, boundary)
|
|
except ValueError as e:
|
|
s.close()
|
|
return jsonify({"error": str(e)}), 400
|
|
s.close()
|
|
return jsonify({"ok": True, "ids": [id1, id2]})
|
|
|
|
|
|
@bp.route("/cluster/<int:cid>/merge", methods=["POST"])
|
|
def merge(cid):
|
|
s = _store()
|
|
other = _body().get("other_id")
|
|
if other is None:
|
|
s.close()
|
|
return jsonify({"error": "other_id required"}), 400
|
|
new_id = review.merge(s, cid, int(other))
|
|
s.close()
|
|
return jsonify({"ok": True, "id": new_id})
|
|
|
|
|
|
@bp.route("/cluster/<int:cid>/member", methods=["POST"])
|
|
def member(cid):
|
|
s = _store()
|
|
body = _body()
|
|
review.set_member(s, cid, body["asset_id"], bool(body.get("included")))
|
|
s.close()
|
|
return jsonify({"ok": True})
|
|
|
|
|
|
@bp.route("/approve-high-confidence", methods=["POST"])
|
|
def approve_high_confidence():
|
|
s = _store()
|
|
threshold = float(_body().get("threshold", 0.75))
|
|
n = review.approve_high_confidence(s, threshold)
|
|
s.close()
|
|
return jsonify({"approved": n})
|
|
|
|
|
|
@bp.route("/cluster/<int:cid>/apply", methods=["POST"])
|
|
def apply_one(cid):
|
|
# Lazy import: app.writeback is owned by a later task and may be absent
|
|
# at app-startup; importing here keeps the blueprint importable regardless.
|
|
from app.writeback import apply_cluster
|
|
with _store() as s: # close even if the Immich write-back raises
|
|
res = apply_cluster(_client(), s, cid)
|
|
return jsonify(res)
|
|
|
|
|
|
@bp.route("/apply-all", methods=["POST"])
|
|
def apply_everything():
|
|
# Lazy import: see apply_one above.
|
|
from app.writeback import apply_all
|
|
with _store() as s: # close even if the Immich write-back raises
|
|
results = apply_all(_client(), s)
|
|
return jsonify({"results": results})
|