183 lines
6.2 KiB
Python
183 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Recounting dashboard — consumes live counter + recount APIs,
|
|
lists OUTPUT_DIR MP4 files, streams via go2rtc for preview.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import requests
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from flask import Flask, render_template, jsonify, request, Response
|
|
from werkzeug.serving import WSGIRequestHandler
|
|
|
|
from dotenv import load_dotenv
|
|
load_dotenv()
|
|
|
|
app = Flask(__name__, template_folder="templates")
|
|
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-me-in-production")
|
|
|
|
OUTPUT_DIR = os.getenv("OUTPUT_DIR", os.getenv("OUTPUT_DIR", "/opt/bytetrack-counter"))
|
|
LIVE_API_URL = os.getenv("LIVE_API_URL", "http://localhost:5000")
|
|
RECOUNT_API_URL = os.getenv("RECOUNT_API_URL", "http://localhost:5001")
|
|
GO2RTC_API_URL = os.getenv("GO2RTC_API_URL", "http://localhost:1984")
|
|
GO2RTC_STREAM_NAME = os.getenv("GO2RTC_STREAM_NAME", "recount")
|
|
|
|
SITE_NAME = os.getenv("SITE_NAME", "RECOUNT")
|
|
|
|
DASHBOARD_PORT = int(os.getenv("RECOUNTING_DASHBOARD_PORT", "5002"))
|
|
DASHBOARD_HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0")
|
|
FLASK_DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true"
|
|
|
|
_http_session = requests.Session()
|
|
_http_session.timeout = 3
|
|
|
|
_FILENAME_RE = re.compile(r"batch[_-](\d+)[_-](\d{8})[_-]\d{6}\.mp4", re.IGNORECASE)
|
|
|
|
|
|
def _parse_filename(filename):
|
|
m = _FILENAME_RE.match(filename)
|
|
if m:
|
|
batch_num = int(m.group(1))
|
|
date_str = m.group(2)
|
|
date_formatted = f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}"
|
|
return batch_num, date_formatted
|
|
return None, None
|
|
|
|
|
|
def _api_get(base_url, path, default=None):
|
|
try:
|
|
resp = _http_session.get(f"{base_url}{path}")
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
except Exception:
|
|
pass
|
|
return default
|
|
|
|
|
|
@app.route("/")
|
|
def index():
|
|
return render_template("recounting.html", site_name=SITE_NAME, output_dir=OUTPUT_DIR)
|
|
|
|
|
|
@app.route("/api/live-progress")
|
|
def api_live_progress():
|
|
data = _api_get(LIVE_API_URL, "/api/current-batch")
|
|
if data and data.get("success"):
|
|
return jsonify(data)
|
|
return jsonify({"success": False, "count": 0, "batch_number": None, "error": "Live unreachable"}), 200
|
|
|
|
|
|
@app.route("/api/recount-progress")
|
|
def api_recount_progress():
|
|
data = _api_get(RECOUNT_API_URL, "/api/current-batch")
|
|
if data and data.get("success"):
|
|
return jsonify(data)
|
|
return jsonify({"success": False, "count": 0, "batch_number": None, "error": "Recount unreachable"}), 200
|
|
|
|
|
|
@app.route("/api/mp4-files")
|
|
def api_mp4_files():
|
|
files = []
|
|
output = Path(OUTPUT_DIR)
|
|
if output.exists():
|
|
for f in sorted(output.rglob("*.mp4"), key=lambda p: p.stat().st_mtime, reverse=True):
|
|
st = f.stat()
|
|
files.append({
|
|
"name": f.name,
|
|
"path": str(f),
|
|
"size": st.st_size,
|
|
"mtime": datetime.fromtimestamp(st.st_mtime).isoformat(),
|
|
})
|
|
return jsonify(files)
|
|
|
|
|
|
@app.route("/api/start-recount", methods=["POST"])
|
|
def start_recount():
|
|
data = request.get_json(force=True) or {}
|
|
mp4_path = data.get("path", "")
|
|
if not mp4_path:
|
|
return jsonify({"success": False, "error": "Missing 'path'"}), 400
|
|
if not os.path.isfile(mp4_path):
|
|
return jsonify({"success": False, "error": f"File not found: {mp4_path}"}), 404
|
|
|
|
src = f"ffmpeg:{mp4_path}#video=h264#hardware"
|
|
try:
|
|
resp = requests.put(
|
|
f"{GO2RTC_API_URL}/api/streams",
|
|
params={"name": GO2RTC_STREAM_NAME, "src": src},
|
|
timeout=5,
|
|
)
|
|
if resp.status_code not in (200, 201):
|
|
return jsonify({"success": False, "error": f"go2rtc returned {resp.status_code}: {resp.text}"}), 502
|
|
except Exception as e:
|
|
return jsonify({"success": False, "error": f"go2rtc unreachable: {e}"}), 502
|
|
|
|
stream_url = f"{GO2RTC_API_URL}/api/stream.mjpeg?src={GO2RTC_STREAM_NAME}"
|
|
return jsonify({"success": True, "stream_url": stream_url, "file": Path(mp4_path).name})
|
|
|
|
|
|
@app.route("/api/batch-result")
|
|
def api_batch_result():
|
|
fpath = request.args.get("file", "")
|
|
filename = Path(fpath).name
|
|
batch_num, date_str = _parse_filename(filename)
|
|
if batch_num is None:
|
|
return jsonify({"success": False, "error": "Cannot parse batch number from filename"}), 200
|
|
|
|
def _match_in_batches(batches):
|
|
for b in batches:
|
|
if b.get("batch_number") == batch_num:
|
|
return b
|
|
return None
|
|
|
|
data = _api_get(LIVE_API_URL, f"/api/day-detail/{date_str}")
|
|
if data:
|
|
b = _match_in_batches(data.get("batches", []))
|
|
if b:
|
|
return jsonify({
|
|
"success": True,
|
|
"batch_number": batch_num,
|
|
"date": date_str,
|
|
"count": b["count"],
|
|
"start_time": b.get("start_time"),
|
|
"end_time": b.get("end_time"),
|
|
})
|
|
|
|
recent = _api_get(LIVE_API_URL, "/api/recent-batches?limit=100")
|
|
if recent:
|
|
for entry in recent if isinstance(recent, list) else recent.get("batches", []):
|
|
if isinstance(entry, dict) and entry.get("batch_number") == batch_num:
|
|
return jsonify({
|
|
"success": True,
|
|
"batch_number": batch_num,
|
|
"date": entry.get("date", date_str),
|
|
"count": entry["count"],
|
|
"start_time": entry.get("start_time"),
|
|
"end_time": entry.get("end_time"),
|
|
})
|
|
|
|
return jsonify({"success": False, "error": f"Batch #{batch_num} not found"}), 200
|
|
|
|
|
|
@app.route("/api/stop-recount", methods=["POST"])
|
|
def stop_recount():
|
|
try:
|
|
requests.delete(f"{GO2RTC_API_URL}/api/streams", params={"name": GO2RTC_STREAM_NAME}, timeout=5)
|
|
except Exception:
|
|
pass
|
|
return jsonify({"success": True})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
WSGIRequestHandler.protocol_version = "HTTP/1.1"
|
|
print(f"Recounting dashboard at http://{DASHBOARD_HOST}:{DASHBOARD_PORT}")
|
|
print(f"Live API: {LIVE_API_URL}")
|
|
print(f"Recount API: {RECOUNT_API_URL}")
|
|
print(f"go2rtc API: {GO2RTC_API_URL}")
|
|
print(f"OUTPUT_DIR: {OUTPUT_DIR}")
|
|
app.run(host=DASHBOARD_HOST, port=DASHBOARD_PORT, debug=FLASK_DEBUG)
|