#!/usr/bin/env python3 """ Recounting dashboard — consumes live counter + recount APIs, lists OUTPUT_DIR MP4 files, streams MP4 via ffmpeg RTSP → go2rtc for preview. """ import json import os import re import subprocess import threading import requests from datetime import datetime from pathlib import Path from flask import Flask, render_template, jsonify, request, Response, send_file 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", "/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_URL = os.getenv("GO2RTC_URL", "http://localhost:1984") RTSP_PORT = os.getenv("RTSP_PORT", "8554") STREAM_NAME = os.getenv("RECOUNT_STREAM_NAME", "stream_1") 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) _streaming_mp4 = None _streaming_lock = threading.Lock() _streaming_proc = None 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 def _stop_ffmpeg_locked(): global _streaming_proc, _streaming_mp4 if _streaming_proc: try: _streaming_proc.kill() _streaming_proc.wait(timeout=3) except Exception: pass _streaming_proc = None _streaming_mp4 = None @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(): global _streaming_mp4, _streaming_proc 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 with _streaming_lock: _stop_ffmpeg_locked() cmd = [ "ffmpeg", "-re", "-i", mp4_path, "-vf", "scale=640:480", "-c:v", "libx264", "-preset", "veryfast", "-c:a", "aac", "-f", "rtsp", f"rtsp://localhost:{RTSP_PORT}/{STREAM_NAME}", ] _streaming_proc = subprocess.Popen(cmd, stderr=subprocess.DEVNULL) _streaming_mp4 = mp4_path return jsonify({ "success": True, "stream_url": f"{GO2RTC_URL}/api/stream.mjpeg?src={STREAM_NAME}", "file": Path(mp4_path).name, }) @app.route("/api/stop-recount", methods=["POST"]) def stop_recount(): with _streaming_lock: _stop_ffmpeg_locked() return jsonify({"success": True}) @app.route("/api/download-mp4") def api_download_mp4(): fpath = request.args.get("file", "") if not fpath or not os.path.isfile(fpath): return jsonify({"success": False, "error": "File not found"}), 404 return send_file(fpath, as_attachment=True, download_name=Path(fpath).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 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: {GO2RTC_URL}") print(f"RTSP: rtsp://localhost:{RTSP_PORT}/{STREAM_NAME}") print(f"OUTPUT_DIR: {OUTPUT_DIR}") app.run(host=DASHBOARD_HOST, port=DASHBOARD_PORT, debug=FLASK_DEBUG)