Files
N22/python_scripts/generate_indkorsel_gallery.py
T

173 lines
7.3 KiB
Python

#!/usr/bin/env python3
"""Generate HTML gallery for indkorsel person-detection snapshots.
Called via shell_command after each new snapshot is saved.
Output: /config/www/snapshots/indkorsel_gallery.html
"""
import os
import glob
from datetime import datetime
SNAPSHOT_DIR = "/config/www/snapshots/indkorsel"
OUTPUT_FILE = "/config/www/snapshots/indkorsel_gallery.html"
LOADER_FILE = "/config/www/snapshots/indkorsel_loader.html"
MAX_SNAPSHOTS = 500
def parse_timestamp(filename):
base = os.path.basename(filename).replace(".jpg", "")
try:
dt = datetime.strptime(base, "%Y-%m-%d_%H-%M-%S")
return dt.strftime("%-d. %b %Y %H:%M:%S")
except Exception:
return base
os.makedirs(SNAPSHOT_DIR, exist_ok=True)
files = sorted(
[f for f in glob.glob(os.path.join(SNAPSHOT_DIR, "*.jpg"))
if os.path.basename(f) != "latest.jpg"],
reverse=True
)[:MAX_SNAPSHOTS]
items_html = ""
images_js = [] # [{src, ts}, ...] for JS navigation
for f in files:
rel = "/local/snapshots/indkorsel/" + os.path.basename(f)
ts = parse_timestamp(f)
idx = len(images_js)
items_html += f"""
<div class="thumb" onclick="openModal({idx})">
<img src="{rel}" loading="lazy" alt="{ts}"/>
<div class="ts">{ts}</div>
</div>"""
images_js.append({"src": rel, "ts": ts})
import json as _json
images_js_str = _json.dumps(images_js)
version = datetime.now().strftime("%Y%m%d%H%M%S")
html = f"""<!DOCTYPE html>
<html lang="da"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Indkorsel snapshots</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{background:#111;color:#ddd;font-family:sans-serif;padding:10px}}
h2{{padding:8px 0 14px;font-size:13px;opacity:.5;font-weight:normal}}
.grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:6px}}
.thumb{{cursor:pointer;border-radius:7px;overflow:hidden;background:#222;transition:transform .12s,opacity .12s}}
.thumb:hover{{transform:scale(1.02);opacity:.9}}
.thumb img{{width:100%;aspect-ratio:16/9;object-fit:cover;display:block}}
.ts{{font-size:10px;padding:4px 6px;opacity:.5;text-align:center}}
.modal{{display:none;position:fixed;inset:0;background:rgba(0,0,0,.93);z-index:9999;flex-direction:column;justify-content:center;align-items:center}}
.modal.show{{display:flex}}
.modal img{{max-width:96vw;max-height:82vh;border-radius:8px;object-fit:contain;box-shadow:0 4px 40px rgba(0,0,0,.6)}}
.modal-ts{{margin-top:12px;font-size:14px;opacity:.6;letter-spacing:.3px}}
.modal-counter{{margin-top:4px;font-size:11px;opacity:.35;letter-spacing:.3px}}
.close{{position:absolute;top:14px;right:18px;font-size:32px;cursor:pointer;opacity:.5;line-height:1;user-select:none}}
.close:hover{{opacity:1}}
.nav{{position:absolute;top:50%;transform:translateY(-50%);font-size:44px;cursor:pointer;opacity:.35;user-select:none;padding:0 18px;line-height:1}}
.nav:hover{{opacity:.9}}
.nav.prev{{left:0}} .nav.next{{right:0}}
.nav.disabled{{opacity:.08;cursor:default;pointer-events:none}}
.empty{{padding:40px;text-align:center;opacity:.4;font-size:14px}}
.reload-badge{{position:fixed;bottom:14px;right:14px;background:#1a73e8;color:#fff;padding:6px 14px;border-radius:20px;font-size:12px;cursor:pointer;display:none;z-index:9998}}
</style>
</head><body>
<h2>Viser {len(files)} person-snapshots &ndash; Indkorsel</h2>
{"<div class='grid'>" + items_html + "</div>" if files else "<div class='empty'>Ingen snapshots endnu.</div>"}
<div id="reload-badge" class="reload-badge" onclick="window.location.reload(true)">Nye billeder &#8211; tryk for at opdatere</div>
<div class="modal" id="modal">
<span class="close" onclick="closeModal()">&#x2715;</span>
<span class="nav prev" id="navPrev" onclick="navigate(-1)">&#x2039;</span>
<img id="mimg" src="" alt=""/>
<span class="nav next" id="navNext" onclick="navigate(1)">&#x203a;</span>
<div class="modal-ts" id="mts"></div>
<div class="modal-counter" id="mcounter"></div>
</div>
<script>
const IMGS = {images_js_str};
const VERSION = '{version}';
let cur = 0;
function openModal(idx){{
cur = idx;
render();
document.getElementById('modal').classList.add('show');
}}
function render(){{
const item = IMGS[cur];
document.getElementById('mimg').src = item.src;
document.getElementById('mts').textContent = item.ts;
document.getElementById('mcounter').textContent = (cur+1) + ' / ' + IMGS.length;
document.getElementById('navPrev').classList.toggle('disabled', cur === 0);
document.getElementById('navNext').classList.toggle('disabled', cur === IMGS.length - 1);
}}
function navigate(dir){{
const next = cur + dir;
if(next >= 0 && next < IMGS.length){{ cur = next; render(); }}
}}
function closeModal(){{
document.getElementById('modal').classList.remove('show');
document.getElementById('mimg').src = '';
}}
document.addEventListener('keydown', e => {{
if(!document.getElementById('modal').classList.contains('show')) return;
if(e.key === 'ArrowLeft') navigate(-1);
if(e.key === 'ArrowRight') navigate(1);
if(e.key === 'Escape') closeModal();
}});
document.getElementById('modal').addEventListener('click', function(e){{
if(e.target === this) closeModal();
}});
// Tjek hvert 60 sek om der er en nyere version af galleriet
setInterval(() => {{
fetch('/local/snapshots/indkorsel_loader.html?_=' + Date.now())
.then(r => r.text())
.then(html => {{
const m = html.match(/[?]v=(\\d+)/);
if(m && m[1] !== VERSION) document.getElementById('reload-badge').style.display = 'block';
}}).catch(() => {{}});
}}, 60000);
</script>
</body></html>"""
with open(OUTPUT_FILE, "w", encoding="utf-8") as fh:
fh.write(html)
# Write a tiny loader page that always redirects to gallery with current timestamp
# so the iframe in HA never serves a cached version
cache_bust = version
loader = f"""<!DOCTYPE html>
<html><head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta http-equiv="refresh" content="0; url=/local/snapshots/indkorsel_gallery.html?v={cache_bust}">
</head><body></body></html>"""
with open(LOADER_FILE, "w", encoding="utf-8") as fh:
fh.write(loader)
print(f"Gallery updated: {len(files)} snapshots -> {OUTPUT_FILE}")
# Update dashboard YAML iframe URL version so browser always fetches fresh loader
DASHBOARD_VIEW = "/config/dashboards/views/06c_indkorsel_snapshots.yaml"
try:
import re as _re
with open(DASHBOARD_VIEW, "r", encoding="utf-8") as fh:
dash = fh.read()
updated = _re.sub(
r'(url: /local/snapshots/indkorsel_loader\.html)(\?v=\d+)?',
rf'\1?v={version}',
dash
)
if updated != dash:
with open(DASHBOARD_VIEW, "w", encoding="utf-8") as fh:
fh.write(updated)
except Exception as _e:
print(f"Warning: could not update dashboard YAML: {_e}")