Add Apprise notifications to quality-scan (self-hosted apprise server); dashboard ready
This commit is contained in:
42
apprise_helper.py
Normal file
42
apprise_helper.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""photo-pipeline: Apprise notification helper.
|
||||||
|
|
||||||
|
Posts to the self-hosted Apprise API server (fans out to all configured targets).
|
||||||
|
Usage: notify("Batch 3 done: 212 photos, 79 near-dups, 3 candidates")
|
||||||
|
|
||||||
|
The Apprise server holds the target config; we just POST title/body.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
APPRISE_URL = os.environ.get("APPRISE_URL", "https://apprise.lab.audasmedia.com.au/notify")
|
||||||
|
|
||||||
|
|
||||||
|
def notify(title: str, body: str, tag: str = None) -> bool:
|
||||||
|
"""Send a notification via the Apprise API server. Returns success."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
payload = {"title": title, "body": body}
|
||||||
|
if tag:
|
||||||
|
payload["tag"] = tag
|
||||||
|
|
||||||
|
r = subprocess.run(
|
||||||
|
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
||||||
|
"-X", "POST", APPRISE_URL,
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-d", json.dumps(payload)],
|
||||||
|
capture_output=True, text=True, timeout=30,
|
||||||
|
)
|
||||||
|
ok = r.stdout.strip() in ("204", "200")
|
||||||
|
print(f"[apprise] {title}: HTTP {r.stdout.strip()}")
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# CLI test: python apprise.py "title" "body"
|
||||||
|
import sys
|
||||||
|
|
||||||
|
t = sys.argv[1] if len(sys.argv) > 1 else "photo-pipeline test"
|
||||||
|
b = sys.argv[2] if len(sys.argv) > 2 else "Notification test from photo-pipeline"
|
||||||
|
ok = notify(t, b)
|
||||||
|
print("sent" if ok else "FAILED")
|
||||||
15
prefect.yaml
15
prefect.yaml
@@ -72,3 +72,18 @@ deployments:
|
|||||||
name: photo-pool
|
name: photo-pool
|
||||||
work_queue_name: null
|
work_queue_name: null
|
||||||
job_variables: {}
|
job_variables: {}
|
||||||
|
|
||||||
|
- name: import
|
||||||
|
version: null
|
||||||
|
tags: [photo-pipeline]
|
||||||
|
description: "Import staged photos into Immich via server-bundled CLI (checksum dedup)"
|
||||||
|
schedule: null
|
||||||
|
flow_name: null
|
||||||
|
entrypoint: immich_import.py:immich_import
|
||||||
|
parameters:
|
||||||
|
src_dir: /mnt/data/01_keep
|
||||||
|
dry_run: true
|
||||||
|
work_pool:
|
||||||
|
name: photo-pool
|
||||||
|
work_queue_name: null
|
||||||
|
job_variables: {}
|
||||||
|
|||||||
@@ -87,13 +87,15 @@ def _move(p: Path, dest_root: Path, src_root: Path):
|
|||||||
|
|
||||||
|
|
||||||
@flow(name="photo-quality-scan")
|
@flow(name="photo-quality-scan")
|
||||||
def quality_scan(base_dir: str, move: bool = False):
|
def quality_scan(base_dir: str, move: bool = False, notify: bool = True):
|
||||||
"""Audit image quality with CleanVision; classify into keep/review/delete."""
|
"""Audit image quality with CleanVision; classify into keep/review/delete."""
|
||||||
audit = audit_folder(base_dir)
|
audit = audit_folder(base_dir)
|
||||||
print(f"Issue summary: {audit['summary']}")
|
print(f"Issue summary: {audit['summary']}")
|
||||||
result = classify_and_sort(base_dir, audit["per_image"], move=move)
|
result = classify_and_sort(base_dir, audit["per_image"], move=move)
|
||||||
print(f"Verdicts: {result['counts']}")
|
print(f"Verdicts: {result['counts']}")
|
||||||
return {"audit": audit["summary"], **result}
|
return {"audit": audit["summary"], **result}
|
||||||
|
if notify:
|
||||||
|
notify_result(result["counts"], audit["summary"], base_dir)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -101,3 +103,23 @@ if __name__ == "__main__":
|
|||||||
d = sys.argv[1] if len(sys.argv) > 1 else "/tmp/cvtest"
|
d = sys.argv[1] if len(sys.argv) > 1 else "/tmp/cvtest"
|
||||||
move = "--move" in sys.argv
|
move = "--move" in sys.argv
|
||||||
quality_scan(d, move=move)
|
quality_scan(d, move=move)
|
||||||
|
|
||||||
|
|
||||||
|
@task
|
||||||
|
def notify_result(counts: dict, summary: list, base_dir: str):
|
||||||
|
"""Send batch summary via Apprise."""
|
||||||
|
import apprise_helper
|
||||||
|
|
||||||
|
hard = counts.get("delete_candidate", 0)
|
||||||
|
soft = counts.get("review", 0)
|
||||||
|
keep = counts.get("keep", 0)
|
||||||
|
flagged = [s for s in summary if s["num_images"] > 0]
|
||||||
|
lines = "; ".join(f"{s[issue_type]}: {s[num_images]}" for s in flagged) or "none"
|
||||||
|
body = (
|
||||||
|
f"Scanned: {base_dir}\n"
|
||||||
|
f"Keep: {keep} | Review: {soft} | Delete-candidates: {hard}\n"
|
||||||
|
f"Issues: {lines}\n"
|
||||||
|
f"Review: http://192.168.20.13:8092/review"
|
||||||
|
)
|
||||||
|
apprise_helper.notify("📸 photo-pipeline batch complete", body)
|
||||||
|
return True
|
||||||
|
|||||||
Reference in New Issue
Block a user