Skip to content

Instantly share code, notes, and snippets.

@HoughIO
Created March 11, 2026 17:18
Show Gist options
  • Select an option

  • Save HoughIO/8d90c0c8d4426617e12b8e3ac2d2896d to your computer and use it in GitHub Desktop.

Select an option

Save HoughIO/8d90c0c8d4426617e12b8e3ac2d2896d to your computer and use it in GitHub Desktop.
KittyCam Daily Viewing Report
#!/bin/bash
# Kevin & Todd Cam - Daily Viewing Report
# Run on Pi
echo "πŸ“Š Installing daily viewing report..."
# Update gacha server to include view tracking
sudo tee /opt/kittycam/gacha_server.py > /dev/null << 'PYEOF'
#!/usr/bin/env python3
from flask import Flask, jsonify, request
import json, os, random
from datetime import date, datetime
app = Flask(__name__)
GACHA_FILE = '/opt/kittycam/gacha.json'
VIEWS_FILE = '/opt/kittycam/views.json'
# All cards: (pet, theme) pairs
CARDS = [
('kevin', 'samurai'), ('kevin', 'space-captain'), ('kevin', 'vampire'),
('kevin', 'chef'), ('kevin', 'disco'), ('kevin', 'sopranos'), ('kevin', 'classical'),
('todd', 'samurai'), ('todd', 'space-captain'), ('todd', 'vampire'),
('todd', 'chef'), ('todd', 'disco'), ('todd', 'sopranos'), ('todd', 'classical'),
('simba', 'samurai'), ('simba', 'space-captain'), ('simba', 'vampire'),
('simba', 'chef'), ('simba', 'disco'), ('simba', 'sopranos'), ('simba', 'classical'),
]
def get_weight(card):
return 0.1 if card[0] == 'simba' else 1.0
def load_json(path, default):
if os.path.exists(path):
try:
with open(path) as f:
return json.load(f)
except:
pass
return default
def save_json(path, data):
with open(path, 'w') as f:
json.dump(data, f, indent=2)
# ============ GACHA ============
@app.route('/api/gacha/state')
def get_state():
state = load_json(GACHA_FILE, {'collected': [], 'last_pull': None})
today = date.today().isoformat()
can_pull = state.get('last_pull') != today
return jsonify({
'collected': state['collected'],
'canPull': can_pull,
'totalCards': len(CARDS)
})
@app.route('/api/gacha/pull', methods=['POST'])
def pull():
state = load_json(GACHA_FILE, {'collected': [], 'last_pull': None})
today = date.today().isoformat()
if state.get('last_pull') == today:
return jsonify({'error': 'Already pulled today', 'canPull': False}), 400
collected_set = set(tuple(c) for c in state['collected'])
available = [c for c in CARDS if tuple(c) not in collected_set]
if not available:
return jsonify({'error': 'All cards collected!', 'complete': True}), 400
weights = [get_weight(c) for c in available]
total = sum(weights)
r = random.random() * total
cumulative = 0
chosen = available[0]
for card, weight in zip(available, weights):
cumulative += weight
if r <= cumulative:
chosen = card
break
state['collected'].append(list(chosen))
state['last_pull'] = today
save_json(GACHA_FILE, state)
return jsonify({
'card': {'pet': chosen[0], 'theme': chosen[1]},
'isSimba': chosen[0] == 'simba',
'collected': len(state['collected']),
'total': len(CARDS)
})
@app.route('/api/gacha/reset', methods=['POST'])
def reset():
save_json(GACHA_FILE, {'collected': [], 'last_pull': None})
return jsonify({'reset': True})
# ============ VIEW TRACKING ============
@app.route('/api/log-view', methods=['POST'])
def log_view():
data = request.get_json() or {}
views = load_json(VIEWS_FILE, {'views': []})
view = {
'timestamp': datetime.now().isoformat(),
'date': date.today().isoformat(),
'userAgent': request.headers.get('User-Agent', 'unknown')[:200],
'page': data.get('page', 'live')
}
views['views'].append(view)
# Keep only last 7 days of views
cutoff = date.today().isoformat()[:8] # YYYY-MM-
views['views'] = [v for v in views['views'] if v.get('date', '')[:8] >= cutoff][-1000:]
save_json(VIEWS_FILE, views)
return jsonify({'logged': True})
@app.route('/api/views/today')
def views_today():
views = load_json(VIEWS_FILE, {'views': []})
today = date.today().isoformat()
today_views = [v for v in views['views'] if v.get('date') == today]
return jsonify({'count': len(today_views), 'views': today_views})
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8082)
PYEOF
# Create daily report script
sudo tee /opt/kittycam/send_daily_report.py > /dev/null << 'PYEOF'
#!/usr/bin/env python3
import json
import os
import subprocess
from datetime import date, datetime, timedelta
from collections import defaultdict
VIEWS_FILE = '/opt/kittycam/views.json'
EMAILS = ['l337chode@gmail.com']
def load_views():
if os.path.exists(VIEWS_FILE):
try:
with open(VIEWS_FILE) as f:
return json.load(f)
except:
pass
return {'views': []}
def parse_device(ua):
ua = ua.lower()
if 'iphone' in ua:
return 'πŸ“± iPhone'
elif 'android' in ua:
return 'πŸ“± Android'
elif 'ipad' in ua:
return 'πŸ“± iPad'
elif 'mac' in ua:
return 'πŸ’» Mac'
elif 'windows' in ua:
return 'πŸ’» Windows'
else:
return '🌐 Other'
def main():
yesterday = (date.today() - timedelta(days=1)).isoformat()
views_data = load_views()
# Get yesterday's views
day_views = [v for v in views_data['views'] if v.get('date') == yesterday]
if not day_views:
subject = f"🐱 Kevin & Todd Cam - No views on {yesterday}"
body = f"Nobody checked on the babies yesterday 😿\n\nMaybe they were too busy being adorable."
else:
# Analyze views
by_hour = defaultdict(int)
by_device = defaultdict(int)
for v in day_views:
try:
hour = datetime.fromisoformat(v['timestamp']).strftime('%I %p')
by_hour[hour] += 1
except:
pass
by_device[parse_device(v.get('userAgent', ''))] += 1
total = len(day_views)
# Build report
subject = f"🐱 Kevin & Todd Cam - {total} views on {yesterday}"
lines = [
f"πŸ“Š Daily Viewing Report for {yesterday}",
f"",
f"Total views: {total}",
f"",
f"πŸ“± By Device:",
]
for device, count in sorted(by_device.items(), key=lambda x: -x[1]):
lines.append(f" {device}: {count}")
lines.append("")
lines.append("πŸ• By Hour:")
for hour, count in sorted(by_hour.items()):
bar = "β–ˆ" * min(count, 20)
lines.append(f" {hour}: {bar} ({count})")
lines.append("")
lines.append("🐱 Kevin and Todd say thanks for watching!")
body = "\n".join(lines)
# Send email via msmtp
for email in EMAILS:
try:
msg = f"Subject: {subject}\nTo: {email}\nFrom: l337chode@gmail.com\n\n{body}"
proc = subprocess.run(
['msmtp', email],
input=msg.encode(),
capture_output=True,
timeout=30
)
if proc.returncode == 0:
print(f"Sent to {email}")
else:
print(f"Failed to send to {email}: {proc.stderr.decode()}")
except Exception as e:
print(f"Error sending to {email}: {e}")
if __name__ == '__main__':
main()
PYEOF
sudo chmod +x /opt/kittycam/send_daily_report.py
# Initialize views file
if [ ! -f /opt/kittycam/views.json ]; then
echo '{"views": []}' | sudo tee /opt/kittycam/views.json > /dev/null
sudo chown andy:andy /opt/kittycam/views.json
fi
# Add cron job for 03:55 MST daily
(crontab -l 2>/dev/null | grep -v send_daily_report; echo "55 3 * * * /usr/bin/python3 /opt/kittycam/send_daily_report.py >> /var/log/kittycam_report.log 2>&1") | crontab -
# Update nginx to proxy log-view endpoint (already covered by /api/gacha/ prefix change)
# Actually need to add /api/log-view explicitly
sudo sed -i '/location \/api\/gacha\//a\
\
location = /api/log-view {\
proxy_pass http://127.0.0.1:8082;\
proxy_http_version 1.1;\
proxy_set_header Host $host;\
}\
\
location = /api/views/today {\
proxy_pass http://127.0.0.1:8082;\
proxy_http_version 1.1;\
proxy_set_header Host $host;\
}' /etc/nginx/sites-enabled/kittycam
# Restart services
sudo systemctl restart gacha.service
sudo systemctl reload nginx
echo "βœ… Daily report installed!"
echo ""
echo "Report will be sent at 03:55 MST each day"
echo "To test: python3 /opt/kittycam/send_daily_report.py"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment