mirror of
https://github.com/R0GGER/iperf3-map.git
synced 2026-11-11 01:20:06 +01:00
267 lines
7.9 KiB
Python
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)
|