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/") 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//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//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//skip", methods=["POST"]) def skip(cid): s = _store() review.skip(s, cid) s.close() return jsonify({"ok": True, "status": "skipped"}) @bp.route("/cluster//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//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//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//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})