Files
2026-01-29 17:14:50 +01:00

267 lines
7.9 KiB
Python

import os
import json
import subprocess
import requests
import threading
import time
from flask import Flask, jsonify, request, send_from_directory
from geopy.geocoders import Nominatim
from geopy.exc import GeocoderTimedOut
import geonamescache
from generate_locations import generate_locations
app = Flask(__name__, static_url_path='')
LOCATION_CACHE_FILE = 'location_cache.json'
location_cache = {}
failed_lookups = set()
processed_servers = []
processing_lock = threading.Lock()
gc = geonamescache.GeonamesCache()
cities_by_name = {}
CITY_ALIASES = {
"new york": "new york city",
"frankfurt": "frankfurt am main",
"st. louis": "saint louis",
"montreal": "montréal",
"reykjavik": "reykjavík",
"sao paulo": "são paulo",
"zurich": "zürich",
"quebec": "québec"
}
COUNTRY_FIXES = {
"UK": "GB",
"USA": "US"
}
def setup_offline_geocoding():
print("Loading offline city database...", flush=True)
cities = gc.get_cities()
for city_id, city_data in cities.items():
name = city_data['name'].lower()
country = city_data['countrycode']
key = f"{name}, {country}"
cities_by_name[key] = {
"lat": float(city_data['latitude']),
"lon": float(city_data['longitude'])
}
print(f"Offline database loaded ({len(cities_by_name)} cities).", flush=True)
geolocator = Nominatim(user_agent="iperf3-map-app", timeout=10)
def load_cache():
global location_cache
if os.path.exists(LOCATION_CACHE_FILE):
try:
with open(LOCATION_CACHE_FILE, 'r') as f:
location_cache = json.load(f)
except:
location_cache = {}
if os.path.exists('prefilled_locations.json'):
try:
with open('prefilled_locations.json', 'r') as f:
static_cache = json.load(f)
for k, v in static_cache.items():
if k not in location_cache:
location_cache[k] = v
except Exception as e:
print(f"Error loading prefilled locations: {e}")
def save_cache():
with open(LOCATION_CACHE_FILE, 'w') as f:
json.dump(location_cache, f)
def get_coordinates(city, country):
if country in COUNTRY_FIXES:
country = COUNTRY_FIXES[country]
key = f"{city}, {country}"
if key in location_cache:
return location_cache[key]
if key in failed_lookups:
return None
city_lower = city.lower()
if city_lower in CITY_ALIASES:
search_city = CITY_ALIASES[city_lower]
else:
search_city = city_lower
offline_key = f"{search_city}, {country}"
if offline_key in cities_by_name:
print(f"Found offline: {key} (via {offline_key})", flush=True)
coords = cities_by_name[offline_key]
location_cache[key] = coords
save_cache()
return coords
try:
print(f"Geocoding online: {key}...", flush=True)
search_key = f"{search_city}, {country}"
location = geolocator.geocode(search_key)
if location:
coords = {"lat": location.latitude, "lon": location.longitude}
location_cache[key] = coords
save_cache()
time.sleep(1.1)
return coords
else:
print(f"Could not find location for: {key}", flush=True)
failed_lookups.add(key)
except Exception as e:
print(f"Error geocoding {key}: {e}", flush=True)
return None
def update_servers_background():
global processed_servers
if not cities_by_name:
setup_offline_geocoding()
print("Starting background server update...", flush=True)
url = "https://export.iperf3serverlist.net/listed_iperf3_servers.json"
try:
print(f"Fetching {url}...", flush=True)
response = requests.get(url, timeout=30)
response.raise_for_status()
servers = response.json()
print(f"Fetched {len(servers)} servers.", flush=True)
load_cache()
temp_processed = []
for server in servers:
city = server.get('SITE')
country = server.get('COUNTRY')
if not city or not country:
continue
key = f"{city}, {country}"
if key in location_cache:
server['lat'] = location_cache[key]['lat']
server['lon'] = location_cache[key]['lon']
temp_processed.append(server)
with processing_lock:
processed_servers = list(temp_processed)
print(f"Initial load: {len(processed_servers)} servers from cache.", flush=True)
changes_made = False
for server in servers:
city = server.get('SITE')
country = server.get('COUNTRY')
if not city or not country:
continue
if 'lat' in server:
continue
key = f"{city}, {country}"
if key in location_cache:
server['lat'] = location_cache[key]['lat']
server['lon'] = location_cache[key]['lon']
with processing_lock:
processed_servers.append(server)
continue
coords = get_coordinates(city, country)
if coords:
server['lat'] = coords['lat']
server['lon'] = coords['lon']
with processing_lock:
processed_servers.append(server)
changes_made = True
print("Background update finished.", flush=True)
except Exception as e:
print(f"Error in background update: {e}")
update_thread = threading.Thread(target=update_servers_background, daemon=True)
update_thread.start()
@app.errorhandler(Exception)
def handle_unexpected_error(error):
print(f"Unhandled error: {error}", flush=True)
return jsonify({"error": "Internal server error"}), 500
@app.route('/')
def index():
return send_from_directory('static', 'index.html')
@app.route('/api/servers')
def get_servers():
with processing_lock:
return jsonify(processed_servers)
@app.route('/api/update-locations', methods=['POST'])
def update_locations_endpoint():
try:
ok = generate_locations()
if not ok:
raise RuntimeError("Location generation failed")
load_cache()
threading.Thread(target=update_servers_background, daemon=True).start()
return jsonify({"message": "Locations updated successfully! Server list is refreshing."})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/test', methods=['POST'])
def run_test():
data = request.json
host = data.get('host')
port = data.get('port')
flags = data.get('flags', '')
if not host:
return jsonify({"error": "Host is required"}), 400
if any(c in host for c in [';', '&', '|', ' ']):
return jsonify({"error": "Invalid host format"}), 400
cmd = ["iperf3", "-c", host, "-p", str(port) if port else "5201"]
if flags:
if any(c in flags for c in [';', '&', '|']):
return jsonify({"error": "Invalid flags"}), 400
for flag in flags.split():
cmd.append(flag)
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
output = result.stdout
if result.stderr:
output += "\n" + result.stderr
return jsonify({"output": output})
except subprocess.TimeoutExpired:
return jsonify({"error": "Test timed out"}), 504
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
load_cache()
debug_enabled = os.getenv('FLASK_DEBUG', '0') == '1'
app.run(host='0.0.0.0', port=5000, debug=debug_enabled)