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()