apiVersion: v1 kind: ConfigMap metadata: name: photo-stats-script namespace: photo-stats data: scan.py: | import os import time import json import threading from datetime import datetime, timezone ROOT = "/mnt/sdb2/immich/uploads/library" OUT_DIR = "/www" CUTOFF = datetime(2025, 1, 1, tzinfo=timezone.utc).timestamp() RESCAN_INTERVAL_SECONDS = 6 * 3600 state_lock = threading.Lock() state = { "status": "starting", "processed": 0, "total": 0, "percent": 0.0, "total_files": 0, "total_size": 0, "recent_files": 0, "recent_size": 0, "started_at": None, "finished_at": None, "error": None, } def human_size(n): n = float(n) for unit in ("B", "KiB", "MiB", "GiB", "TiB", "PiB"): if abs(n) < 1024.0: return f"{n:.1f}{unit}" n /= 1024.0 return f"{n:.1f}EiB" def render_html(s): progress_html = "" if s["status"] == "counting": progress_html = "

Counting files...

" elif s["status"] == "scanning": pct = s["percent"] progress_html = f"""

Progress: {s['processed']} / {s['total']} ({pct:.1f}%)

""" error_html = f"

Error: {s['error']}

" if s["error"] else "" return f""" Photo library stats

Photo library stats

Path: {ROOT}

Status: {s['status']}

{progress_html} {error_html}
Total files{s['total_files']}
Total size{human_size(s['total_size'])} ({s['total_size']} bytes)
Files newer than 2025-01-01{s['recent_files']}
Size of files newer than 2025-01-01{human_size(s['recent_size'])} ({s['recent_size']} bytes)
Last scan started{s['started_at'] or '-'}
Last scan finished{s['finished_at'] or '-'}
""" def write_output(): with state_lock: s = dict(state) html = render_html(s) tmp = os.path.join(OUT_DIR, "index.html.tmp") with open(tmp, "w") as f: f.write(html) os.replace(tmp, os.path.join(OUT_DIR, "index.html")) tmp = os.path.join(OUT_DIR, "stats.json.tmp") with open(tmp, "w") as f: json.dump(s, f, indent=2) os.replace(tmp, os.path.join(OUT_DIR, "stats.json")) def scan_once(): with state_lock: state.update(status="counting", started_at=datetime.now(timezone.utc).isoformat(), finished_at=None, error=None, processed=0, total=0, percent=0.0) write_output() total = 0 try: for _dirpath, _dirs, files in os.walk(ROOT): total += len(files) except OSError as e: with state_lock: state.update(status="error", error=str(e)) write_output() return with state_lock: state.update(total=total, status="scanning") write_output() processed = 0 total_files = 0 total_size = 0 recent_files = 0 recent_size = 0 last_write = time.time() try: for dirpath, _dirs, files in os.walk(ROOT): for name in files: fp = os.path.join(dirpath, name) processed += 1 try: st = os.stat(fp) except OSError: continue total_files += 1 total_size += st.st_size if st.st_mtime >= CUTOFF: recent_files += 1 recent_size += st.st_size now = time.time() if now - last_write > 2: with state_lock: state.update( processed=processed, total_files=total_files, total_size=total_size, recent_files=recent_files, recent_size=recent_size, percent=(processed / total * 100) if total else 100.0, ) write_output() last_write = now except OSError as e: with state_lock: state.update(status="error", error=str(e)) write_output() return with state_lock: state.update( processed=processed, total_files=total_files, total_size=total_size, recent_files=recent_files, recent_size=recent_size, percent=100.0, status="done", finished_at=datetime.now(timezone.utc).isoformat(), ) write_output() def main(): os.makedirs(OUT_DIR, exist_ok=True) write_output() while True: scan_once() time.sleep(RESCAN_INTERVAL_SECONDS) if __name__ == "__main__": main()