Created
March 11, 2026 17:18
-
-
Save HoughIO/8d90c0c8d4426617e12b8e3ac2d2896d to your computer and use it in GitHub Desktop.
KittyCam Daily Viewing Report
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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