webhook_receiver.py
1 """ 2 Overcast OPML Webhook Receiver 3 4 Receives OPML exports from iOS Shortcut and saves to Sovereign OS data directory. 5 6 Usage: 7 python webhook_receiver.py 8 9 Then configure your iOS Shortcut to POST to: 10 http://<your-mac-ip>:5050/overcast 11 12 Find your Mac's IP: 13 ipconfig getifaddr en0 14 """ 15 16 from flask import Flask, request, jsonify 17 from datetime import datetime 18 from pathlib import Path 19 import xml.etree.ElementTree as ET 20 21 app = Flask(__name__) 22 23 # Configuration 24 DATA_DIR = Path.home() / 'repos/Sovereign_OS/data/overcast' 25 PORT = 5050 26 27 28 def parse_opml_stats(opml_content: str) -> dict: 29 """Extract quick stats from OPML for the response.""" 30 try: 31 root = ET.fromstring(opml_content) 32 body = root.find('body') 33 34 podcasts = 0 35 episodes = 0 36 played = 0 37 38 for outline in body.iter('outline'): 39 if outline.get('type') == 'rss': 40 podcasts += 1 41 elif outline.get('type') == 'podcast-episode': 42 episodes += 1 43 if outline.get('played') == '1': 44 played += 1 45 46 return { 47 'podcasts': podcasts, 48 'episodes': episodes, 49 'played': played 50 } 51 except Exception as e: 52 return {'error': str(e)} 53 54 55 @app.route('/overcast', methods=['POST']) 56 def receive_overcast(): 57 """Receive OPML from iOS Shortcut.""" 58 opml_content = request.data.decode('utf-8') 59 60 if not opml_content or '<opml' not in opml_content: 61 return jsonify({ 62 'status': 'error', 63 'message': 'Invalid OPML content' 64 }), 400 65 66 # Ensure directory exists 67 DATA_DIR.mkdir(parents=True, exist_ok=True) 68 69 # Save with timestamp 70 timestamp = datetime.now().strftime('%Y-%m-%d_%H%M%S') 71 timestamped_path = DATA_DIR / f'overcast_{timestamp}.opml' 72 timestamped_path.write_text(opml_content) 73 74 # Also save as latest (always overwritten) 75 latest_path = DATA_DIR / 'latest.opml' 76 latest_path.write_text(opml_content) 77 78 # Parse stats 79 stats = parse_opml_stats(opml_content) 80 81 print(f"[{timestamp}] Received OPML: {len(opml_content):,} bytes") 82 print(f" Podcasts: {stats.get('podcasts', '?')}, Episodes: {stats.get('episodes', '?')}, Played: {stats.get('played', '?')}") 83 84 return jsonify({ 85 'status': 'ok', 86 'bytes': len(opml_content), 87 'timestamp': timestamp, 88 'path': str(timestamped_path), 89 'stats': stats 90 }) 91 92 93 @app.route('/health', methods=['GET']) 94 def health(): 95 """Health check endpoint.""" 96 return jsonify({ 97 'status': 'healthy', 98 'service': 'Sovereign OS Overcast Webhook', 99 'data_dir': str(DATA_DIR) 100 }) 101 102 103 @app.route('/', methods=['GET']) 104 def index(): 105 """Landing page with instructions.""" 106 return """ 107 <html> 108 <head><title>Sovereign OS - Overcast Webhook</title></head> 109 <body style="font-family: system-ui; max-width: 600px; margin: 50px auto; padding: 20px;"> 110 <h1>Overcast OPML Webhook</h1> 111 <p>This server receives Overcast OPML exports from iOS Shortcuts.</p> 112 113 <h2>Endpoints</h2> 114 <ul> 115 <li><code>POST /overcast</code> - Receive OPML data</li> 116 <li><code>GET /health</code> - Health check</li> 117 </ul> 118 119 <h2>iOS Shortcut Setup</h2> 120 <p>In your Shortcut, use "Get Contents of URL" with:</p> 121 <ul> 122 <li>URL: <code>http://YOUR_MAC_IP:5050/overcast</code></li> 123 <li>Method: POST</li> 124 <li>Request Body: The OPML file content</li> 125 </ul> 126 127 <p><a href="/health">Check Health</a></p> 128 </body> 129 </html> 130 """ 131 132 133 if __name__ == '__main__': 134 print(f""" 135 ╔═══════════════════════════════════════════════════════════╗ 136 ║ Sovereign OS - Overcast Webhook Receiver ║ 137 ╠═══════════════════════════════════════════════════════════╣ 138 ║ Server running on port {PORT} ║ 139 ║ Data directory: {str(DATA_DIR):<40} ║ 140 ║ ║ 141 ║ Find your Mac's IP: ║ 142 ║ ipconfig getifaddr en0 ║ 143 ║ ║ 144 ║ Configure iOS Shortcut to POST to: ║ 145 ║ http://<your-ip>:{PORT}/overcast ║ 146 ╚═══════════════════════════════════════════════════════════╝ 147 """) 148 app.run(host='0.0.0.0', port=PORT, debug=False)