use serde_json::Value;
use crate::{
client::FlightRadarClient,
config::ClientConfig,
error::{FlightRadarError, Result},
models::{
Airline, AirlinesResponse, AirportDetailsResponse, AirportDisruptionsResponse,
AirportResponse, AirportsResponse, AuthResponse, BookmarksResponse, FlightDetailsResponse,
FlightsResponse, MostTrackedResponse, SearchResponse, VolcanicEruptionsResponse,
ZonesResponse,
},
options::{
AirportDetailsOptions, Countries, FlightTrackerConfig, HistoryFileType, Pagination,
PresetZone,
},
};
#[derive(Debug, Clone, Default)]
pub struct FlightQuery {
pub airline: Option<String>,
pub bounds: Option<String>,
pub registration: Option<String>,
pub aircraft_type: Option<String>,
pub with_details: bool,
}
impl FlightQuery {
fn to_params(&self) -> Vec<(&'static str, String)> {
let mut params: Vec<(&'static str, String)> = Vec::new();
if let Some(airline) = &self.airline {
params.push(("airline", airline.clone()));
}
if let Some(bounds) = &self.bounds {
params.push(("bounds", bounds.clone()));
}
if let Some(registration) = &self.registration {
params.push(("registration", registration.clone()));
}
if let Some(aircraft_type) = &self.aircraft_type {
params.push(("aircraft_type", aircraft_type.clone()));
}
params.push((
"details",
if self.with_details {
"1".to_owned()
} else {
"0".to_owned()
},
));
params
}
}
#[derive(Debug, Clone)]
pub struct FlightRadarApi {
client: FlightRadarClient,
flight_tracker_config: FlightTrackerConfig,
}
impl FlightRadarApi {
pub fn new() -> Result<Self> {
Self::with_config(ClientConfig::default())
}
pub fn with_config(config: ClientConfig) -> Result<Self> {
let client = FlightRadarClient::new(config)?;
Ok(Self {
client,
flight_tracker_config: FlightTrackerConfig::default(),
})
}
pub fn from_client(client: FlightRadarClient) -> Self {
Self {
client,
flight_tracker_config: FlightTrackerConfig::default(),
}
}
pub fn client(&self) -> &FlightRadarClient {
&self.client
}
pub fn flight_tracker_config(&self) -> &FlightTrackerConfig {
&self.flight_tracker_config
}
pub fn set_flight_tracker_config(&mut self, flight_tracker_config: FlightTrackerConfig) {
self.flight_tracker_config = flight_tracker_config;
}
pub async fn get_flights(&self, query: &FlightQuery) -> Result<FlightsResponse> {
let payload = self.get_flights_raw(query).await?;
FlightsResponse::from_value(payload)
}
pub async fn get_flights_raw(&self, query: &FlightQuery) -> Result<Value> {
let mut params = query.to_params();
params.extend(self.flight_tracker_config.to_params());
self.client
.get_json("common/v1/flight/list.json", ¶ms)
.await
}
pub async fn get_flights_by_zone(&self, zone: PresetZone) -> Result<FlightsResponse> {
let bounds = zone.get_bounds();
let query = FlightQuery {
bounds: Some(bounds),
..Default::default()
};
self.get_flights(&query).await
}
pub async fn search(&self, query: &str, limit: usize) -> Result<SearchResponse> {
let payload = self.search_raw(query, limit).await?;
SearchResponse::from_value(payload)
}
pub async fn search_raw(&self, query: &str, limit: usize) -> Result<Value> {
if query.trim().is_empty() {
return Err(FlightRadarError::InvalidInput(
"search query must not be empty",
));
}
let params = vec![("query", query.to_owned()), ("limit", limit.to_string())];
self.client.get_json("common/v1/search.json", ¶ms).await
}
pub async fn get_airport(&self, code: &str) -> Result<AirportResponse> {
let payload = self.get_airport_raw(code).await?;
let response = AirportResponse::from_value(payload)?;
if response.airport().is_none() {
return Err(FlightRadarError::AirportNotFound {
code: code.trim().to_uppercase(),
});
}
Ok(response)
}
pub async fn get_airport_raw(&self, code: &str) -> Result<Value> {
if code.trim().len() < 3 {
return Err(FlightRadarError::InvalidInput(
"airport code must have at least 3 characters",
));
}
let params = vec![("code", code.trim().to_uppercase())];
self.client
.get_json("common/v1/airport.json", ¶ms)
.await
}
pub async fn get_zones(&self) -> Result<ZonesResponse> {
let payload = self.get_zones_raw().await?;
ZonesResponse::from_value(payload)
}
pub async fn get_zones_raw(&self) -> Result<Value> {
self.client.get_json("zones/fcgi/feed.js", &[]).await
}
pub async fn get_airlines(&self) -> Result<AirlinesResponse> {
let payload = self.get_airlines_raw().await?;
AirlinesResponse::from_value(payload)
}
pub async fn get_airlines_raw(&self) -> Result<Value> {
self.client
.get_json("common/v1/airline/list.json", &[])
.await
}
pub async fn get_airline_by_icao(&self, icao: &str) -> Result<Option<Airline>> {
let normalized = icao.trim().to_uppercase();
if normalized.len() != 3 {
return Err(FlightRadarError::InvalidInput(
"airline ICAO code must have exactly 3 characters",
));
}
let airlines = self.get_airlines().await?;
Ok(airlines
.items()
.iter()
.find(|airline| airline.icao.as_deref() == Some(normalized.as_str()))
.cloned())
}
pub async fn get_airports(&self, countries: &[String]) -> Result<AirportsResponse> {
let payload = self.get_airports_raw(countries).await?;
AirportsResponse::from_value(payload)
}
pub async fn get_airports_by_countries(
&self,
countries: &[Countries],
) -> Result<AirportsResponse> {
let normalized: Vec<String> = countries.iter().map(Countries::as_api_value).collect();
self.get_airports(&normalized).await
}
pub async fn get_airports_raw(&self, countries: &[String]) -> Result<Value> {
if countries.is_empty() {
return Err(FlightRadarError::InvalidInput(
"countries list must not be empty",
));
}
let normalized: Vec<String> = countries
.iter()
.map(|country| country.trim().to_ascii_lowercase())
.filter(|country| !country.is_empty())
.collect();
if normalized.is_empty() {
return Err(FlightRadarError::InvalidInput(
"countries list must not contain only empty values",
));
}
self.client
.get_json(
"common/v1/airport/list.json",
&[("countries", normalized.join(","))],
)
.await
}
pub async fn get_flight_details(&self, flight_id: &str) -> Result<FlightDetailsResponse> {
let payload = self.get_flight_details_raw(flight_id).await?;
FlightDetailsResponse::from_value(payload)
}
pub async fn get_flight_details_raw(&self, flight_id: &str) -> Result<Value> {
let normalized = flight_id.trim();
if normalized.is_empty() {
return Err(FlightRadarError::InvalidInput(
"flight id must not be empty",
));
}
self.client
.get_json(
"common/v1/flight/details.json",
&[("flight_id", normalized.to_owned())],
)
.await
}
pub async fn login(&self, user: &str, password: &str) -> Result<AuthResponse> {
let payload = self.login_raw(user, password).await?;
let auth = AuthResponse::from_value(payload)?;
if auth.is_success() {
return Ok(auth);
}
Err(FlightRadarError::LoginError {
message: auth
.message
.clone()
.unwrap_or_else(|| "authentication failed".to_owned()),
})
}
pub async fn login_raw(&self, user: &str, password: &str) -> Result<Value> {
if user.trim().is_empty() || password.is_empty() {
return Err(FlightRadarError::InvalidInput(
"user and password must not be empty",
));
}
let form_data = vec![
("email", user.trim().to_owned()),
("password", password.to_owned()),
];
self.client.post_form_json("user/login", &form_data).await
}
pub async fn logout(&self) -> Result<AuthResponse> {
let payload = self.logout_raw().await?;
let auth = AuthResponse::from_value(payload)?;
if auth.is_success() {
return Ok(auth);
}
Err(FlightRadarError::LoginError {
message: auth
.message
.clone()
.unwrap_or_else(|| "logout failed".to_owned()),
})
}
pub async fn logout_raw(&self) -> Result<Value> {
self.client.post_form_json("user/logout", &[]).await
}
pub async fn get_airport_details(
&self,
code: &str,
flight_limit: usize,
page: usize,
) -> Result<AirportDetailsResponse> {
self.get_airport_details_with_options(code, AirportDetailsOptions { flight_limit, page })
.await
}
pub async fn get_airport_details_with_options(
&self,
code: &str,
options: AirportDetailsOptions,
) -> Result<AirportDetailsResponse> {
let payload = self
.get_airport_details_raw(code, options.flight_limit, options.page)
.await?;
AirportDetailsResponse::from_value(payload)
}
pub async fn get_airport_details_raw(
&self,
code: &str,
flight_limit: usize,
page: usize,
) -> Result<Value> {
if code.trim().len() < 3 {
return Err(FlightRadarError::InvalidInput(
"airport code must have at least 3 characters",
));
}
self.client
.get_json(
"common/v1/airport/details.json",
&[
("code", code.trim().to_uppercase()),
("flight_limit", flight_limit.to_string()),
("page", page.to_string()),
],
)
.await
}
pub async fn get_airport_disruptions(&self, code: &str) -> Result<AirportDisruptionsResponse> {
let payload = self.get_airport_disruptions_raw(code).await?;
AirportDisruptionsResponse::from_value(payload)
}
pub async fn get_airport_disruptions_raw(&self, code: &str) -> Result<Value> {
if code.trim().len() < 3 {
return Err(FlightRadarError::InvalidInput(
"airport code must have at least 3 characters",
));
}
self.client
.get_json(
"common/v1/airport/disruptions.json",
&[("code", code.trim().to_uppercase())],
)
.await
}
pub async fn get_most_tracked(&self, limit: usize) -> Result<MostTrackedResponse> {
self.get_most_tracked_with_pagination(Pagination { limit, page: 1 })
.await
}
pub async fn get_most_tracked_with_pagination(
&self,
pagination: Pagination,
) -> Result<MostTrackedResponse> {
let payload = self
.get_most_tracked_raw(pagination.limit, pagination.page)
.await?;
MostTrackedResponse::from_value(payload)
}
pub async fn get_most_tracked_raw(&self, limit: usize, page: usize) -> Result<Value> {
self.client
.get_json(
"common/v1/most-tracked.json",
&[("limit", limit.to_string()), ("page", page.to_string())],
)
.await
}
pub async fn get_history_data(
&self,
flight_id: &str,
file_type: &str,
timestamp: i64,
) -> Result<String> {
let normalized_file_type = file_type.trim().to_ascii_uppercase();
let history_file_type = match normalized_file_type.as_str() {
"CSV" => HistoryFileType::Csv,
"KML" => HistoryFileType::Kml,
_ => {
return Err(FlightRadarError::InvalidInput(
"file_type must be either CSV or KML",
));
}
};
self.get_history_data_typed(flight_id, history_file_type, timestamp)
.await
}
pub async fn get_history_data_typed(
&self,
flight_id: &str,
file_type: HistoryFileType,
timestamp: i64,
) -> Result<String> {
let normalized = flight_id.trim();
if normalized.is_empty() {
return Err(FlightRadarError::InvalidInput(
"flight id must not be empty",
));
}
self.client
.get_text(
"common/v1/flight/history-file",
&[
("flight_id", normalized.to_owned()),
("file_type", file_type.as_str().to_owned()),
("timestamp", timestamp.to_string()),
],
)
.await
}
pub async fn get_airline_logo(&self, airline_icao: &str) -> Result<Vec<u8>> {
let normalized = airline_icao.trim().to_ascii_uppercase();
if normalized.len() != 3 {
return Err(FlightRadarError::InvalidInput(
"airline ICAO code must have exactly 3 characters",
));
}
let path = format!("static/images/data/operators/{normalized}.logo0.png");
self.client.get_bytes(&path, &[]).await
}
pub async fn get_country_flag(&self, country: &str) -> Result<Vec<u8>> {
let normalized = country.trim().to_ascii_lowercase().replace(' ', "-");
if normalized.is_empty() {
return Err(FlightRadarError::InvalidInput("country must not be empty"));
}
let path = format!("static/images/data/flags-small/{normalized}.png");
self.client.get_bytes(&path, &[]).await
}
pub async fn get_volcanic_eruption_data(&self) -> Result<VolcanicEruptionsResponse> {
let payload = self
.client
.get_json("common/v1/volcanic-eruptions.json", &[])
.await?;
VolcanicEruptionsResponse::from_value(payload)
}
pub async fn get_bookmarks(&self) -> Result<BookmarksResponse> {
let payload = self
.client
.get_json("common/v1/bookmarks.json", &[])
.await?;
BookmarksResponse::from_value(payload)
}
pub fn get_bounds_by_point(latitude: f64, longitude: f64, radius_km: f64) -> Result<String> {
if !(-90.0..=90.0).contains(&latitude) {
return Err(FlightRadarError::InvalidInput(
"latitude must be between -90 and 90",
));
}
if !(-180.0..=180.0).contains(&longitude) {
return Err(FlightRadarError::InvalidInput(
"longitude must be between -180 and 180",
));
}
if radius_km <= 0.0 {
return Err(FlightRadarError::InvalidInput("radius must be positive"));
}
const EARTH_RADIUS_KM: f64 = 6371.0;
let angular_radius = radius_km / EARTH_RADIUS_KM;
let lat_rad = latitude.to_radians();
let lat_min = (lat_rad - angular_radius).to_degrees();
let lat_max = (lat_rad + angular_radius).to_degrees();
let lon_delta = (angular_radius
.sin()
.atan2(angular_radius.cos() * lat_rad.sin()))
.abs()
.to_degrees();
let lon_min = longitude - lon_delta;
let lon_max = longitude + lon_delta;
let lat_min = lat_min.max(-90.0);
let lat_max = lat_max.min(90.0);
let lon_min = if lon_min < -180.0 {
lon_min + 360.0
} else {
lon_min
};
let lon_max = if lon_max > 180.0 {
lon_max - 360.0
} else {
lon_max
};
Ok(format!(
"{:.2},{:.2},{:.2},{:.2}",
lat_min, lon_min, lat_max, lon_max
))
}
}