Spaces:
Running
Running
| from flask import Flask, jsonify, request | |
| from flask_cors import CORS | |
| import json | |
| import os | |
| import random | |
| from src.crawler import Crawler | |
| from src.analyzer import ImageAnalyzer | |
| from src.monitor import SystemMonitor | |
| app = Flask(__name__) | |
| CORS(app) | |
| REPORT_FILE = 'seo_report.json' | |
| # --- Keep Alive Mechanism --- | |
| import threading | |
| import time | |
| import requests | |
| class KeepAlive(threading.Thread): | |
| def __init__(self, interval=60, target_url="http://127.0.0.1:7860/health"): | |
| super().__init__() | |
| self.interval = interval | |
| self.target_url = target_url | |
| self.daemon = True # Stop when main thread stops | |
| self.running = True | |
| def run(self): | |
| print("KeepAlive System Started") | |
| while self.running: | |
| try: | |
| # 1. Log Heartbeat (keeps logs active) | |
| print(f"[Heartbeat] System Active - {time.ctime()}") | |
| # 2. Self-Ping to keep connection warnings away (Wait for server to start first) | |
| time.sleep(self.interval) | |
| # Try to self-ping if server is likely up (after 10s) | |
| try: | |
| requests.get(self.target_url, timeout=5) | |
| except: | |
| pass # Ignore connection errors during startup/shutdown | |
| except Exception as e: | |
| print(f"[KeepAlive] Error: {e}") | |
| time.sleep(60) | |
| class ScheduledCrawler(threading.Thread): | |
| def __init__(self, interval=14400): # 14400 seconds = 4 Hours | |
| super().__init__() | |
| self.interval = interval | |
| self.daemon = True | |
| self.running = True | |
| def run(self): | |
| print(f"[Scheduler] Auto-Crawler initialized. Schedule: Every {self.interval/3600} hours.") | |
| # Initial delay to let server start | |
| time.sleep(60) | |
| while self.running: | |
| try: | |
| # 1. Identify Target | |
| # User configures this via Environment Variable | |
| target_domain = os.environ.get("AUTO_CRAWL_TARGET") | |
| if target_domain: | |
| print(f"\n[Scheduler] 🕒 Triggering scheduled crawl for: {target_domain}") | |
| # 2. Run Crawl (Reuse logic via internal call or simulating request) | |
| # We instantiate the classes directly to avoid network overhead | |
| crawler = Crawler() | |
| site_data, _, _ = crawler.crawl_domain(target_domain, max_pages=50) # Limit to 50 for auto-runs | |
| if site_data: | |
| analyzer = ImageAnalyzer() | |
| results = analyzer.analyze_site(site_data) | |
| # Save Report | |
| with open(REPORT_FILE, 'w', encoding='utf-8') as f: | |
| json.dump(results, f, indent=4) | |
| print(f"[Scheduler] ✅ Crawl finished for {target_domain}. Report saved.") | |
| else: | |
| print(f"[Scheduler] ⚠️ Crawl returned no data.") | |
| else: | |
| print("[Scheduler] ℹ️ waiting... (Set 'AUTO_CRAWL_TARGET' Env Var to enable auto-crawling)") | |
| # 3. Wait for next interval | |
| time.sleep(self.interval) | |
| except Exception as e: | |
| print(f"[Scheduler] Error: {e}") | |
| time.sleep(60) | |
| # Start KeepAlive & Scheduler | |
| pinger = KeepAlive(interval=300) | |
| pinger.start() | |
| scheduler = ScheduledCrawler(interval=14400) # 4 Hours | |
| scheduler.start() | |
| # ---------------------------- | |
| def home(): | |
| return "Antigravity API is Running. Use /api/status for system info." | |
| def health_check(): | |
| return jsonify({"status": "alive"}), 200 | |
| def get_system_status(): | |
| """ | |
| Returns real-time system metadata including dynamic region and latency. | |
| Accepts optional 'domain' parameter to check latency to a specific target. | |
| """ | |
| domain = request.args.get('domain') | |
| stats = SystemMonitor.get_system_stats(target_url=domain) | |
| return jsonify(stats) | |
| def get_seo_report(): | |
| # Cache variable attached to function to persist state | |
| if not hasattr(get_seo_report, "cache"): | |
| get_seo_report.cache = {"data": None, "mtime": 0} | |
| def get_cached_report(): | |
| if not os.path.exists(REPORT_FILE): | |
| return None | |
| current_mtime = os.path.getmtime(REPORT_FILE) | |
| if get_seo_report.cache["data"] is None or current_mtime > get_seo_report.cache["mtime"]: | |
| try: | |
| with open(REPORT_FILE, 'r', encoding='utf-8') as f: | |
| get_seo_report.cache["data"] = json.load(f) | |
| get_seo_report.cache["mtime"] = current_mtime | |
| except Exception as e: | |
| # If read fails, return None or raise | |
| return None | |
| return get_seo_report.cache["data"] | |
| def get_param(name, default): | |
| val = request.args.get(name) or request.form.get(name) | |
| if val is None and request.is_json: | |
| val = request.json.get(name) | |
| return val if val is not None else default | |
| # Global Caching for Domain Crawls (Active Memory Cache) | |
| if not hasattr(get_seo_report, "domain_cache"): | |
| get_seo_report.domain_cache = {} | |
| domain = get_param('domain', None) | |
| limit = int(get_param('limit', 25)) | |
| if domain: | |
| # Check Cache (TTL 10 minutes) | |
| cache_key = f"{domain}_{limit}" | |
| cached_item = get_seo_report.domain_cache.get(cache_key) | |
| import time | |
| if cached_item: | |
| timestamp, data = cached_item | |
| # 600 seconds = 10 minutes | |
| # DISABLE CACHE TEMPORARILY to ensure fresh code logic is used | |
| # if time.time() - timestamp < 600: | |
| # print(f"Returning Cached Result for {domain}") | |
| # return jsonify(data) | |
| try: | |
| print(f"Starting live scan for: {domain}") | |
| # Initialize Crawler | |
| crawler = Crawler() | |
| # Crawl the domain with the requested limit | |
| site_data, total_discovered, _ = crawler.crawl_domain(domain, max_pages=limit) | |
| if not site_data: | |
| response = { | |
| "summary": { | |
| "total_pages_scanned": 0, | |
| "total_images_found": 0, | |
| "total_images_missing_alt": 0, | |
| "total_pages_discovered": 0 | |
| }, | |
| "details": [] | |
| } | |
| return jsonify(response) | |
| # Analyze Results | |
| analyzer = ImageAnalyzer() | |
| results = analyzer.analyze_site(site_data) | |
| # Add discovery stats | |
| results['summary']['total_pages_discovered'] = total_discovered | |
| results['details_count'] = len(results['details']) | |
| # Save to Cache | |
| get_seo_report.domain_cache[cache_key] = (time.time(), results) | |
| return jsonify(results) | |
| except Exception as e: | |
| return jsonify({"error": f"Scraping failed: {str(e)}"}), 500 | |
| if not os.path.exists(REPORT_FILE): | |
| return jsonify({"error": "Report file not found. Please run result logic first."}), 404 | |
| try: | |
| data = get_cached_report() | |
| if data is None: | |
| # Fallback if file doesn't exist or read failed | |
| # But if we are here, we passed the os.path.exists check earlier, | |
| # so strictly speaking we should just handle the None case. | |
| # The previous logic had a check for os.path.exists(REPORT_FILE) at line 59. | |
| # We can retain that or rely on get_cached_report returning None. | |
| return jsonify({"error": "Report file not found or unreadable."}), 404 | |
| # Get parameters for filtering existing report | |
| # limit is already extracted above | |
| random_param = str(get_param('random', 'true')).lower() | |
| is_random = random_param == 'true' | |
| summary = data.get('summary', {}) | |
| details = data.get('details', []) | |
| # Filter details if limit is provided | |
| if limit is not None and limit > 0: | |
| if is_random and limit < len(details): | |
| details = random.sample(details, limit) | |
| else: | |
| details = details[:limit] | |
| response = { | |
| "summary": summary, | |
| "details_count": len(details), # Useful logic for client | |
| "details": details | |
| } | |
| return jsonify(response) | |
| except Exception as e: | |
| return jsonify({"error": f"Failed to read report: {str(e)}"}), 500 | |
| if __name__ == '__main__': | |
| # Run on 0.0.0.0 to be accessible if needed, default port 5000 | |
| app.run(debug=True, host='0.0.0.0', port=5050) | |