Quazim0t0's picture
Upload 34 files
41e0c9e verified
Raw
History Blame Contribute Delete
4.73 kB
"""Thin client for the official FlightRadar24 API (fr24api.flightradar24.com).
Docs: https://fr24api.flightradar24.com/docs/getting-started
Auth is via a Bearer token supplied in the FR24_API_TOKEN env var.
"""
from __future__ import annotations
import datetime as dt
import os
import time
from functools import lru_cache
import requests
BASE = "https://fr24api.flightradar24.com"
class FR24Error(RuntimeError):
pass
def _headers() -> dict:
token = os.environ.get("FR24_API_TOKEN", "").strip()
if not token:
raise FR24Error(
"FR24_API_TOKEN is not set. Put your FlightRadar24 API token in the "
"environment (see .env.example)."
)
return {
"Accept": "application/json",
"Accept-Version": os.environ.get("FR24_API_VERSION", "v1"),
"Authorization": f"Bearer {token}",
}
def _clamp_bounds(north, south, west, east):
"""FR24 caps the queryable area; keep the box sane."""
north = max(-90.0, min(90.0, float(north)))
south = max(-90.0, min(90.0, float(south)))
west = max(-180.0, min(180.0, float(west)))
east = max(-180.0, min(180.0, float(east)))
if north < south:
north, south = south, north
return north, south, west, east
FLIGHT_POSITIONS_URL = f"{BASE}/api/live/flight-positions/full"
def query_positions(params: dict, *, timeout=25):
"""Low-level call to live/flight-positions/full with arbitrary FR24 filters.
Returns (data_list, request_url). At least one filter param is required by
FR24 (bounds, routes, airports, callsigns, ...).
"""
resp = requests.get(FLIGHT_POSITIONS_URL, headers=_headers(),
params=params, timeout=timeout)
if resp.status_code == 400:
raise FR24Error(f"FR24 bad request (400): {resp.text[:300]}")
if resp.status_code == 401:
raise FR24Error("FR24 rejected the token (401). Check FR24_API_TOKEN.")
if resp.status_code == 402:
raise FR24Error("FR24 says payment/credits required (402).")
if resp.status_code == 429:
raise FR24Error("FR24 rate limit hit (429). Slow down the refresh.")
if resp.status_code >= 400:
raise FR24Error(f"FR24 error {resp.status_code}: {resp.text[:300]}")
payload = resp.json()
data = payload.get("data", payload if isinstance(payload, list) else [])
return data, resp.url
def live_positions(north, south, west, east, *, extra_params=None, timeout=25):
"""Return a list of live flight dicts inside the bounding box."""
north, south, west, east = _clamp_bounds(north, south, west, east)
params = {"bounds": f"{north},{south},{west},{east}"}
if extra_params:
params.update(extra_params)
data, _ = query_positions(params, timeout=timeout)
return data
def search_route(origin: str, destination: str, *, limit=200):
"""Live flights on a route, e.g. origin='LHR', destination='JFK'."""
params = {"routes": f"{origin.strip().upper()}-{destination.strip().upper()}",
"limit": limit}
return query_positions(params)
def search_airport(airport: str, direction: str = "both", *, limit=200):
"""Live flights to/from an airport. direction in {inbound,outbound,both}."""
direction = (direction or "both").strip().lower()
if direction not in {"inbound", "outbound", "both"}:
direction = "both"
params = {"airports": f"{direction}:{airport.strip().upper()}", "limit": limit}
return query_positions(params)
def airport_coords(code: str):
"""lat/lon for an IATA/ICAO code via the bundled OpenFlights table.
(FR24's airport endpoints don't return coordinates on this plan, so this is
resolved locally — fast and reliable.)
"""
import airports
return airports.coords(code)
def parse_eta(eta_value):
"""FR24 'eta' is an ISO timestamp. Return (iso_string, seconds_remaining)."""
if not eta_value:
return None, None
try:
if isinstance(eta_value, (int, float)):
target = dt.datetime.fromtimestamp(eta_value, tz=dt.timezone.utc)
else:
s = str(eta_value).replace("Z", "+00:00")
target = dt.datetime.fromisoformat(s)
if target.tzinfo is None:
target = target.replace(tzinfo=dt.timezone.utc)
now = dt.datetime.now(tz=dt.timezone.utc)
remaining = (target - now).total_seconds()
return target.isoformat(), remaining
except Exception:
return str(eta_value), None
def human_duration(seconds):
if seconds is None:
return "—"
seconds = int(max(0, seconds))
h, rem = divmod(seconds, 3600)
m, _ = divmod(rem, 60)
if h:
return f"{h}h {m:02d}m"
return f"{m}m"