156 lines
6.7 KiB
Python
156 lines
6.7 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 – 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 – tryk for at opdatere</div>
|
|
<div class="modal" id="modal">
|
|
<span class="close" onclick="closeModal()">✕</span>
|
|
<span class="nav prev" id="navPrev" onclick="navigate(-1)">‹</span>
|
|
<img id="mimg" src="" alt=""/>
|
|
<span class="nav next" id="navNext" onclick="navigate(1)">›</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}")
|