Initial commit: susan automation scripts
Overnight transcoding, music discovery/import, system health reports, stats page generator, and bookmark management. Secrets stored in /etc/automation/ — not in repo.
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate a retro stats page for Susan.
|
||||
|
||||
90s CS professor homepage aesthetic. Pure HTML, no JS.
|
||||
Regenerate periodically via cron.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
OUTPUT = "/var/www/webdav/obsidian/stats.html"
|
||||
|
||||
def cmd(c):
|
||||
try:
|
||||
return subprocess.check_output(c, shell=True, stderr=subprocess.DEVNULL, timeout=10).decode().strip()
|
||||
except:
|
||||
return ""
|
||||
|
||||
def get_uptime():
|
||||
up = cmd("uptime -p")
|
||||
return up.replace("up ", "") if up else "unknown"
|
||||
|
||||
def get_uptime_since():
|
||||
return cmd("uptime -s")
|
||||
|
||||
def get_load():
|
||||
load = cmd("cat /proc/loadavg")
|
||||
return load.split()[:3] if load else ["?", "?", "?"]
|
||||
|
||||
def get_memory():
|
||||
mem = cmd("free -h | grep Mem")
|
||||
parts = mem.split()
|
||||
if len(parts) >= 7:
|
||||
return {"total": parts[1], "used": parts[2], "free": parts[3], "available": parts[6]}
|
||||
return {"total": "?", "used": "?", "free": "?", "available": "?"}
|
||||
|
||||
def get_disk():
|
||||
disks = []
|
||||
for line in cmd("df -h /disks /home 2>/dev/null").split("\n")[1:]:
|
||||
parts = line.split()
|
||||
if len(parts) >= 6:
|
||||
disks.append({"fs": parts[0], "size": parts[1], "used": parts[2], "avail": parts[3], "pct": parts[4], "mount": parts[5]})
|
||||
return disks
|
||||
|
||||
def get_services():
|
||||
services = []
|
||||
lines = cmd("systemctl list-units --type=service --state=running --no-pager --no-legend").split("\n")
|
||||
targets = ["jellyfin", "navidrome", "qbittorrent", "sonarr", "radarr", "lidarr",
|
||||
"readarr", "prowlarr", "slskd", "nginx", "audiobookshelf", "openclaw-gateway"]
|
||||
for line in lines:
|
||||
for t in targets:
|
||||
if t in line.lower():
|
||||
name = line.split()[0].replace(".service", "")
|
||||
services.append(name)
|
||||
return sorted(services)
|
||||
|
||||
def get_media_counts():
|
||||
films = int(cmd("find /disks/Plex/Films -maxdepth 1 -type d | wc -l") or 1) - 1
|
||||
tv = int(cmd("find /disks/Plex/TV -maxdepth 1 -type d | wc -l") or 1) - 1
|
||||
anime = int(cmd("find /disks/Plex/Anime -maxdepth 1 -type d | wc -l") or 1) - 1
|
||||
tracks = int(cmd("find /disks/Plex/Music -type f \\( -name '*.flac' -o -name '*.mp3' -o -name '*.ogg' -o -name '*.opus' \\) | wc -l") or 0)
|
||||
artists = int(cmd("ls -1d /disks/Plex/Music/*/ 2>/dev/null | grep -v -E 'venv|lib|bin|data|include|_|pyvenv' | wc -l") or 0)
|
||||
return {"films": films, "tv": tv, "anime": anime, "tracks": tracks, "artists": artists}
|
||||
|
||||
def get_cpu():
|
||||
model = cmd("grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2").strip()
|
||||
cores = cmd("nproc")
|
||||
return model, cores
|
||||
|
||||
def get_packages():
|
||||
return cmd("dpkg -l | grep '^ii' | wc -l")
|
||||
|
||||
def get_kernel():
|
||||
return cmd("uname -r")
|
||||
|
||||
def get_os():
|
||||
return cmd("grep PRETTY_NAME /etc/os-release | cut -d'\"' -f2")
|
||||
|
||||
def get_install_date():
|
||||
d = cmd("stat -c %w /")
|
||||
if d and d != "-":
|
||||
return d.split(".")[0]
|
||||
return "unknown"
|
||||
|
||||
COUNTER_FILE = os.path.join(os.path.dirname(__file__), "..", "data", "visitor_counter.txt")
|
||||
|
||||
def get_and_increment_counter():
|
||||
"""Read and increment a persistent visitor counter."""
|
||||
os.makedirs(os.path.dirname(COUNTER_FILE), exist_ok=True)
|
||||
count = 0
|
||||
if os.path.exists(COUNTER_FILE):
|
||||
try:
|
||||
count = int(open(COUNTER_FILE).read().strip())
|
||||
except:
|
||||
count = 0
|
||||
count += 1
|
||||
with open(COUNTER_FILE, "w") as f:
|
||||
f.write(str(count))
|
||||
return count
|
||||
|
||||
now = datetime.datetime.now()
|
||||
uptime = get_uptime()
|
||||
uptime_since = get_uptime_since()
|
||||
load = get_load()
|
||||
mem = get_memory()
|
||||
disks = get_disk()
|
||||
services = get_services()
|
||||
media = get_media_counts()
|
||||
cpu_model, cpu_cores = get_cpu()
|
||||
packages = get_packages()
|
||||
kernel = get_kernel()
|
||||
os_name = get_os()
|
||||
install_date = get_install_date()
|
||||
visitor_count = get_and_increment_counter()
|
||||
|
||||
html = f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta http-equiv="refresh" content="300">
|
||||
<title>Susan - System Status</title>
|
||||
<style type="text/css">
|
||||
body {{
|
||||
background-color: #e8e8e8;
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAMklEQVQYV2N89+7dfwYGBgZGRkYGBgYmBjIBE7mKR5UOPIUMDIyMjGQrHlU68BQCABPnC/0ZkzYUAAAAAElFTkSuQmCC");
|
||||
font-family: "Times New Roman", Times, serif;
|
||||
color: #333;
|
||||
margin: 20px 40px;
|
||||
font-size: 14pt;
|
||||
}}
|
||||
h1 {{
|
||||
font-size: 22pt;
|
||||
color: #003366;
|
||||
border-bottom: 2px solid #003366;
|
||||
padding-bottom: 4px;
|
||||
}}
|
||||
h2 {{
|
||||
font-size: 16pt;
|
||||
color: #003366;
|
||||
margin-top: 24px;
|
||||
}}
|
||||
table {{
|
||||
border-collapse: collapse;
|
||||
margin: 8px 0;
|
||||
}}
|
||||
td, th {{
|
||||
border: 2px solid #003366;
|
||||
padding: 4px 10px;
|
||||
text-align: left;
|
||||
background-color: #f5f5f0;
|
||||
}}
|
||||
th {{
|
||||
background-color: #d0d0c8;
|
||||
font-weight: bold;
|
||||
color: #003366;
|
||||
}}
|
||||
a {{
|
||||
color: #003366;
|
||||
}}
|
||||
hr {{
|
||||
border: none;
|
||||
border-top: 2px solid #003366;
|
||||
margin: 16px 0;
|
||||
}}
|
||||
.footer {{
|
||||
margin-top: 30px;
|
||||
font-size: 10pt;
|
||||
color: #666;
|
||||
border-top: 1px solid #999;
|
||||
padding-top: 6px;
|
||||
}}
|
||||
.status-ok {{
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.status-warn {{
|
||||
color: #cc7700;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.counter {{
|
||||
font-family: "Courier New", monospace;
|
||||
background-color: #000;
|
||||
color: #0f0;
|
||||
padding: 2px 6px;
|
||||
font-size: 10pt;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Susan — System Status</h1>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>General Information</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Hostname</th><td>susan</td></tr>
|
||||
<tr><th>Operating System</th><td>{os_name}</td></tr>
|
||||
<tr><th>Kernel</th><td>{kernel}</td></tr>
|
||||
<tr><th>Processor</th><td>{cpu_model} ({cpu_cores} cores)</td></tr>
|
||||
<tr><th>Installed Packages</th><td>{packages}</td></tr>
|
||||
<tr><th>First Installed</th><td>{install_date}</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Uptime & Load</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Uptime</th><td>{uptime}</td></tr>
|
||||
<tr><th>Up Since</th><td>{uptime_since}</td></tr>
|
||||
<tr><th>Load Average</th><td>{load[0]}, {load[1]}, {load[2]} (1, 5, 15 min)</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Memory</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Total</th><th>Used</th><th>Free</th><th>Available</th></tr>
|
||||
<tr><td>{mem['total']}</td><td>{mem['used']}</td><td>{mem['free']}</td><td>{mem['available']}</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Disk Usage</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Mount</th><th>Size</th><th>Used</th><th>Available</th><th>Use%</th></tr>
|
||||
"""
|
||||
|
||||
for d in disks:
|
||||
pct = int(d["pct"].replace("%", "")) if d["pct"].replace("%", "").isdigit() else 0
|
||||
cls = "status-warn" if pct > 85 else "status-ok"
|
||||
html += f'<tr><td>{d["mount"]}</td><td>{d["size"]}</td><td>{d["used"]}</td><td>{d["avail"]}</td><td class="{cls}">{d["pct"]}</td></tr>\n'
|
||||
|
||||
html += f"""</table>
|
||||
|
||||
<h2>Running Services</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Service</th><th>Status</th></tr>
|
||||
"""
|
||||
|
||||
for s in services:
|
||||
html += f'<tr><td>{s}</td><td class="status-ok">running</td></tr>\n'
|
||||
|
||||
html += f"""</table>
|
||||
|
||||
<h2>Media Library</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Category</th><th>Count</th></tr>
|
||||
<tr><td>Films</td><td>{media['films']}</td></tr>
|
||||
<tr><td>TV Shows</td><td>{media['tv']}</td></tr>
|
||||
<tr><td>Anime</td><td>{media['anime']}</td></tr>
|
||||
<tr><td>Music Artists</td><td>{media['artists']}</td></tr>
|
||||
<tr><td>Music Tracks</td><td>{media['tracks']}</td></tr>
|
||||
</table>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="footer">
|
||||
<p>
|
||||
You are visitor number <span class="counter">{visitor_count:06,}</span> since December 2023.
|
||||
</p>
|
||||
<p>
|
||||
This page was last generated on <b>{now.strftime("%A, %d %B %Y at %H:%M:%S")}</b>.
|
||||
<br>
|
||||
No JavaScript was harmed in the making of this page.
|
||||
<br>
|
||||
Best viewed with any browser. Optimised for 800×600.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
os.makedirs(os.path.dirname(OUTPUT), exist_ok=True)
|
||||
with open(OUTPUT, "w") as f:
|
||||
f.write(html)
|
||||
|
||||
print(f"Generated stats page: {OUTPUT}")
|
||||
print(f" Uptime: {uptime}")
|
||||
print(f" Services: {len(services)}")
|
||||
print(f" Films: {media['films']}, TV: {media['tv']}, Anime: {media['anime']}")
|
||||
print(f" Music: {media['tracks']} tracks, {media['artists']} artists")
|
||||
Reference in New Issue
Block a user