#!/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") 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 data = _api_get(LIVE_API_URL, f"/api/day-detail/{date_str}") if not data: return jsonify({"success": False, "error": "Live API unreachable"}), 200 for b in data.get("batches", []): if b.get("batch_number") == batch_num: 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"), }) return jsonify({"success": False, "error": f"Batch #{batch_num} not found on {date_str}"}), 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)