diff --git a/README.md b/README.md index 4aa45f7..c2fd34f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ dated video folders into a simple daily view that is easy to search and play. - Shows recordings on a full 24-hour timeline. - Uses the time in each filename to place clips at the correct time of day. - Shows recorded minutes in light blue and empty periods in dark blue. +- Shows recorder-detected system events in dark yellow when `fileindex.dat` is present. - Lets you choose the recording date. - Plays browser-friendly MP4 files immediately. - Converts an AVI clip automatically when no MP4 copy exists. diff --git a/index.html b/index.html index 3e77eb5..58abb46 100644 --- a/index.html +++ b/index.html @@ -66,6 +66,7 @@ .legend .rec::before { background: var(--accent); } .legend .gap::before { background: #2a2a2a; border: 1px solid #444; } .legend .highlight::before { background: #ffd43b; } + .legend .system::before { background: #a87900; } .highlights { margin-top: 18px; background: var(--surface); border-radius: 8px; padding: 14px 16px; } .highlights h2 { font-size: 14px; font-weight: 500; margin-bottom: 10px; } .highlight-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } @@ -160,7 +161,7 @@
-
RecordedHighlightNo recording
+
RecordedHighlightSystem highlightNo recording
@@ -198,6 +199,7 @@ let zoom = 1; let focus = 43200; let highlights = []; + let systemHighlights = []; let highlightStart = null; let highlightEnd = null; let encodingWasRunning = false; @@ -316,6 +318,11 @@ list.append(row); }); } + async function loadSystemHighlights() { + const response = await fetch(`/api/system-highlights?date=${dateSelect.value}`); + systemHighlights = await response.json(); + drawTimeline(); + } document.querySelector("#mark-start").onclick = () => setMark("start"); document.querySelector("#mark-end").onclick = () => setMark("end"); document.querySelector("#save-highlight").onclick = async () => { @@ -398,6 +405,12 @@ const r = (item.end - winStart) / winSize * w; if (r > 0 && l < w) ctx.fillRect(Math.max(0, l), 0, Math.min(w, r) - Math.max(0, l), 56); }); + ctx.fillStyle = "#a87900"; + systemHighlights.forEach(item => { + const l = (item.start - winStart) / winSize * w; + const r = (item.end - winStart) / winSize * w; + if (r > 0 && l < w) ctx.fillRect(Math.max(0, l), 0, Math.min(w, r) - Math.max(0, l), 56); + }); // hour lines ctx.strokeStyle = "rgba(255,255,255,.08)"; for (let h = Math.ceil(winStart / 3600); h < winEnd / 3600; h++) { @@ -449,6 +462,7 @@ scrubFill.style.width = "0%"; drawTimeline(); loadHighlights(); + loadSystemHighlights(); if (!date && d.dates[0]) loadDate(d.dates[0]); } diff --git a/server.py b/server.py index ca4c20f..3118738 100644 --- a/server.py +++ b/server.py @@ -4,7 +4,9 @@ import concurrent.futures import json import re import subprocess +import struct import threading +from datetime import datetime from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import parse_qs, urlparse @@ -62,6 +64,24 @@ def normalize_date(value): return date +def scan_system_highlights(root): + try: + data = (root / "fileindex.dat").read_bytes() + except OSError: + return [] + highlights = [] + for offset in range(0, len(data) - 15, 16): + timestamp, packed, _, _ = struct.unpack_from("<4I", data, offset) + duration = packed & 0xffff + event = (packed >> 16) & 1 + if not event or not duration: + continue + moment = datetime.fromtimestamp(timestamp) + start = moment.hour * 3600 + moment.minute * 60 + moment.second + highlights.append({"date": moment.strftime("%Y%m%d"), "start": start, "end": min(86400, start + duration)}) + return highlights + + class PlayerHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" @@ -81,6 +101,9 @@ class PlayerHandler(BaseHTTPRequestHandler): return self.send_json(self.server.encode_status) if request.path == "/api/highlights": return self.send_json(self.server.highlights) + if request.path == "/api/system-highlights": + date = parse_qs(request.query).get("date", [""])[0] + return self.send_json([item for item in self.server.system_highlights if item["date"] == date]) if request.path == "/video": return self.send_video(request.query) self.send_error(404) @@ -274,6 +297,7 @@ def main(): server.output_root = args.root.expanduser().resolve() server.highlights_path = server.output_root / "highlights.json" server.highlights = load_highlights(server.highlights_path) + server.system_highlights = scan_system_highlights(server.source_root) server.encode_status = {"running": False, "total": 0, "completed": 0, "skipped": 0, "failed": 0} count = sum(map(len, recordings.values())) print(f"Indexed {count} recordings across {len(recordings)} days")