alt-scraper-api / api.py
prince1604
Add Automatic 4-Hour Crawler Scheduler
c50a4e1
Raw
History Blame
9.13 kB
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()
# ----------------------------
@app.route('/')
def home():
return "Antigravity API is Running. Use /api/status for system info."
@app.route('/health')
def health_check():
return jsonify({"status": "alive"}), 200
@app.route('/api/status', methods=['GET'])
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)
@app.route('/api/seo-report', methods=['GET', 'POST'])
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)