Flugverkehr: adsb.lol als neue Primaerquelle (10.796 Flugzeuge)
OpenSky + airplanes.live ersetzt durch adsb.lol: - Ein Call: 10.796 Flugzeuge global - Kein Rate-Limiting - 15s Refresh, 12s Cache - Fallback: adsb.one (~7.000 Flugzeuge) - Mehr Datenfelder: Registration, Flugzeugtyp, Squawk
Dieser Commit ist enthalten in:
@@ -1,4 +1,4 @@
|
|||||||
"""Flugverkehr: OpenSky (primary) + airplanes.live (fallback)."""
|
"""Flugverkehr: adsb.lol (primary, 10.000+) + adsb.one (fallback)."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
@@ -9,82 +9,71 @@ from fastapi import APIRouter
|
|||||||
logger = logging.getLogger("globe.flights")
|
logger = logging.getLogger("globe.flights")
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
_OPENSKY_URL = "https://opensky-network.org/api/states/all"
|
|
||||||
|
|
||||||
# Fallback: airplanes.live Stuetzpunkte (wenige, wichtigste)
|
|
||||||
_FALLBACK_GRID = [
|
|
||||||
(48, 10), (52, 13), (40, -4), (41, 12), (55, 10), # Europa
|
|
||||||
(40, -74), (34, -118), (42, -88), # USA
|
|
||||||
(33, 36), (26, 56), (25, 45), # Nahost
|
|
||||||
(35, 140), (31, 121), (22, 114), # Asien
|
|
||||||
]
|
|
||||||
|
|
||||||
_cache: dict = {"data": None, "ts": 0}
|
_cache: dict = {"data": None, "ts": 0}
|
||||||
_lock = asyncio.Lock()
|
_lock = asyncio.Lock()
|
||||||
_task = None
|
_task = None
|
||||||
|
|
||||||
|
# adsb.lol: Ein Call, 10.000+ Flugzeuge, kein Rate-Limit
|
||||||
|
_PRIMARY_URL = "https://api.adsb.lol/v2/point/30/0/10000"
|
||||||
|
# adsb.one: Fallback, ~7.000 Flugzeuge
|
||||||
|
_FALLBACK_URL = "https://api.adsb.one/v2/point/30/0/10000"
|
||||||
|
|
||||||
async def _fetch_opensky(client):
|
|
||||||
"""Versucht OpenSky — liefert alle Flugzeuge oder None bei Fehler."""
|
def _parse_ac(data):
|
||||||
try:
|
"""Parst Flugzeuge aus adsb.lol/adsb.one Format."""
|
||||||
resp = await client.get(_OPENSKY_URL)
|
|
||||||
if resp.status_code == 429:
|
|
||||||
logger.info("OpenSky: Rate-Limited (429), nutze Fallback")
|
|
||||||
return None
|
|
||||||
resp.raise_for_status()
|
|
||||||
raw = resp.json()
|
|
||||||
states = raw.get("states", [])
|
|
||||||
ac = []
|
ac = []
|
||||||
for s in states:
|
for a in data.get("ac", []):
|
||||||
if not s or len(s) < 12 or s[5] is None or s[6] is None:
|
lat = a.get("lat")
|
||||||
continue
|
lon = a.get("lon")
|
||||||
if s[8]: # on_ground
|
if lat is None or lon is None:
|
||||||
continue
|
continue
|
||||||
ac.append({
|
ac.append({
|
||||||
"hex": s[0] or "", "flight": (s[1] or "").strip(),
|
"hex": a.get("hex", ""),
|
||||||
"lat": s[6], "lon": s[5],
|
"flight": (a.get("flight") or "").strip(),
|
||||||
"alt_baro": round(s[7] * 3.281) if s[7] else None,
|
"reg": a.get("r", ""),
|
||||||
"gs": round(s[9] * 1.944) if s[9] else None,
|
"type": a.get("t", ""),
|
||||||
"track": s[10], "origin": s[2] or "",
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
"alt_baro": a.get("alt_baro"),
|
||||||
|
"alt_geom": a.get("alt_geom"),
|
||||||
|
"gs": a.get("gs"),
|
||||||
|
"track": a.get("track"),
|
||||||
|
"squawk": a.get("squawk", ""),
|
||||||
})
|
})
|
||||||
logger.info(f"OpenSky: {len(ac)} Flugzeuge")
|
|
||||||
return ac
|
return ac
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"OpenSky: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_fallback(client):
|
|
||||||
"""Fallback: airplanes.live mit wenigen Stuetzpunkten."""
|
|
||||||
seen = {}
|
|
||||||
for lat, lon in _FALLBACK_GRID:
|
|
||||||
try:
|
|
||||||
resp = await client.get(f"https://api.airplanes.live/v2/point/{lat:.0f}/{lon:.0f}/250")
|
|
||||||
if resp.status_code == 200:
|
|
||||||
for a in resp.json().get("ac", []):
|
|
||||||
h = a.get("hex")
|
|
||||||
if h and h not in seen:
|
|
||||||
seen[h] = a
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
await asyncio.sleep(2) # Langsam um Rate-Limits zu vermeiden
|
|
||||||
logger.info(f"airplanes.live Fallback: {len(seen)} Flugzeuge")
|
|
||||||
return list(seen.values())
|
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_all():
|
async def _fetch_all():
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if _cache["data"] and now - _cache["ts"] < 55:
|
if _cache["data"] and now - _cache["ts"] < 12:
|
||||||
return _cache["data"]
|
return _cache["data"]
|
||||||
|
|
||||||
async with _lock:
|
async with _lock:
|
||||||
if _cache["data"] and time.time() - _cache["ts"] < 55:
|
if _cache["data"] and time.time() - _cache["ts"] < 12:
|
||||||
return _cache["data"]
|
return _cache["data"]
|
||||||
|
|
||||||
|
ac = None
|
||||||
async with httpx.AsyncClient(timeout=20) as client:
|
async with httpx.AsyncClient(timeout=20) as client:
|
||||||
ac = await _fetch_opensky(client)
|
# Primary: adsb.lol
|
||||||
if ac is None:
|
try:
|
||||||
ac = await _fetch_fallback(client)
|
resp = await client.get(_PRIMARY_URL)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
ac = _parse_ac(resp.json())
|
||||||
|
if ac:
|
||||||
|
logger.info(f"adsb.lol: {len(ac)} Flugzeuge")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"adsb.lol: {e}")
|
||||||
|
|
||||||
|
# Fallback: adsb.one
|
||||||
|
if not ac:
|
||||||
|
try:
|
||||||
|
resp = await client.get(_FALLBACK_URL)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
ac = _parse_ac(resp.json())
|
||||||
|
if ac:
|
||||||
|
logger.info(f"adsb.one Fallback: {len(ac)} Flugzeuge")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"adsb.one: {e}")
|
||||||
|
|
||||||
if ac:
|
if ac:
|
||||||
_cache["data"] = {"ac": ac, "total": len(ac)}
|
_cache["data"] = {"ac": ac, "total": len(ac)}
|
||||||
@@ -96,20 +85,20 @@ async def _fetch_all():
|
|||||||
|
|
||||||
|
|
||||||
async def _collector_loop():
|
async def _collector_loop():
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(3)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
await _fetch_all()
|
await _fetch_all()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Flight collector: {e}")
|
logger.warning(f"Flight collector: {e}")
|
||||||
await asyncio.sleep(60)
|
await asyncio.sleep(15)
|
||||||
|
|
||||||
|
|
||||||
def start_flight_collector():
|
def start_flight_collector():
|
||||||
global _task
|
global _task
|
||||||
if _task is None or _task.done():
|
if _task is None or _task.done():
|
||||||
_task = asyncio.create_task(_collector_loop())
|
_task = asyncio.create_task(_collector_loop())
|
||||||
logger.info("Flight collector gestartet (OpenSky + Fallback)")
|
logger.info("Flight collector gestartet (adsb.lol + adsb.one)")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/flights")
|
@router.get("/flights")
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren