1use crate::error::FlightRadarError;
2use reqwest::blocking::Client;
3use serde::{Deserialize, Serialize};
4
5pub struct FlightRadarClient {
7 client: Client,
8 base_url: String,
9 api_key: String,
10}
11
12impl FlightRadarClient {
13 pub fn new(api_key: String) -> Self {
17 FlightRadarClient {
18 client: Client::new(),
19 base_url: "https://fr24api.flightradar24.com/api/".to_string(),
21 api_key,
22 }
23 }
24
25 pub fn update_base_url(&mut self, base_url: String) {
26 self.base_url = base_url;
27 }
28
29 fn build_query_params(params: &FullLiveFlightQuery) -> Result<String, FlightRadarError> {
35 let mut url: String = String::new();
36
37 if let Some(bounds) = ¶ms.bounds {
38 let bounds_str = format!(
39 "?bounds={},{},{},{}",
40 bounds.north, bounds.south, bounds.west, bounds.east
41 );
42 url.push_str(&bounds_str);
43 }
44 if let Some(flights) = ¶ms.flights {
45 url.push_str("&flights=");
46 for flight in flights {
47 if flight.chars().all(char::is_alphanumeric) && flight.len() > 2 {
48 url.push_str(&flight.to_string());
49 url.push(',');
50 } else {
51 return Err(FlightRadarError::Parameter(format!("Flight #: {}", flight)));
52 }
53 }
54 url.pop();
55 }
56 if let Some(callsigns) = ¶ms.callsigns {
57 url.push_str("&callsigns=");
58 for callsign in callsigns {
59 if callsign.chars().all(char::is_alphanumeric)
60 && callsign.len() > 2
61 && callsign.len() <= 8
62 {
63 url.push_str(&callsign.to_string());
64 url.push(',');
65 } else {
66 return Err(FlightRadarError::Parameter(format!(
67 "Callsign: {}",
68 callsign
69 )));
70 }
71 }
72 url.pop();
73 }
74 if let Some(registrations) = ¶ms.registrations {
75 url.push_str("®istrations=");
76 for registration in registrations {
77 if registration
78 .chars()
79 .all(|c| c.is_alphanumeric() || c == '-')
80 && registration.len() > 1
81 && registration.len() <= 12
82 {
83 url.push_str(®istration.to_string());
84 url.push(',');
85 } else {
86 return Err(FlightRadarError::Parameter(format!(
87 "Registration #: {}",
88 registration
89 )));
90 }
91 }
92 url.pop();
93 }
94 if let Some(painted_as) = ¶ms.painted_as {
95 url.push_str("&painted_as=");
96 for painted in painted_as {
97 if painted.chars().all(char::is_alphabetic) && painted.len() == 3 {
98 url.push_str(&painted.to_string());
99 url.push(',');
100 } else {
101 return Err(FlightRadarError::Parameter(format!(
102 "Painted As: {}",
103 painted
104 )));
105 }
106 }
107 url.pop();
108 }
109 if let Some(operating_as) = ¶ms.operating_as {
110 url.push_str("&operating_as=");
111 for operating in operating_as {
112 if operating.chars().all(char::is_alphabetic) && operating.len() == 3 {
113 url.push_str(&operating.to_string());
114 url.push(',');
115 } else {
116 return Err(FlightRadarError::Parameter(format!(
117 "Operating As: {}",
118 operating
119 )));
120 }
121 }
122 url.pop();
123 }
124 if let Some(airports) = ¶ms.airports {
125 url.push_str("&airports=");
126 for airport in airports {
127 if airport
128 .chars()
129 .all(|c| (c.is_alphabetic() && (c != 'i' || c != 'q' || c != 'x')) || c == ':')
130 {
131 url.push_str(&airport.to_string());
132 url.push(',');
133 } else {
134 return Err(FlightRadarError::Parameter(format!("Airport: {}", airport)));
135 }
136 }
137 url.pop();
138 }
139 if let Some(routes) = ¶ms.routes {
140 url.push_str("&routes=");
141 for route in routes {
142 if route
143 .chars()
144 .all(|c| (c.is_alphabetic() && (c != 'i' || c != 'q' || c != 'x')) || c == '-')
145 {
146 url.push_str(&route.to_string());
147 url.push(',');
148 } else {
149 return Err(FlightRadarError::Parameter(format!("Route: {}", route)));
150 }
151 }
152 url.pop();
153 }
154 if let Some(aircraft) = ¶ms.aircraft {
155 url.push_str("&aircraft=");
156 for aircraft_iter in aircraft {
157 if aircraft_iter
158 .chars()
159 .all(|c| c.is_alphanumeric() || c == '*')
160 {
161 if aircraft_iter.chars().filter(|c| c == &'*').count() == 1 {
162 url.push_str(&aircraft_iter.to_string());
163 url.push(',');
164 }
165 } else {
166 return Err(FlightRadarError::Parameter(format!(
167 "Aircraft: {}",
168 aircraft_iter
169 )));
170 }
171 }
172 url.pop();
173 }
174 if let Some(altitude_ranges) = ¶ms.altitude_ranges {
175 url.push_str("&altitude_ranges=");
176 for altitude_range in altitude_ranges {
177 url.push_str(&altitude_range.min.to_string());
178 url.push('-');
179 url.push_str(&altitude_range.max.to_string());
180 url.push(',');
181 }
182 url.pop();
183 }
184 if let Some(squawks) = ¶ms.squawks {
185 url.push_str("&squawks=");
186 for squawk in squawks {
187 if squawk <= &7777 {
188 url.push_str(&squawk.to_string());
189 url.push(',');
190 } else {
191 return Err(FlightRadarError::Parameter(format!("Squawk: {}", squawk)));
192 }
193 }
194 url.pop();
195 }
196 if let Some(categories) = ¶ms.categories {
197 url.push_str("&categories=");
198 for category in categories {
199 if "PCMJTHBGDVON".contains(*category) {
200 url.push(*category);
201 url.push(',');
202 } else {
203 return Err(FlightRadarError::Parameter(format!(
204 "Category: {}",
205 category
206 )));
207 }
208 }
209 url.pop();
210 }
211 if let Some(data_sources) = ¶ms.data_sources {
212 let valid_data_sources = [
213 "ADSB".to_string(),
214 "MLAT".to_string(),
215 "ESTIMATED".to_string(),
216 ];
217 url.push_str("&data_sources=");
218 for data_source in data_sources {
219 if valid_data_sources.contains(data_source) {
220 url.push_str(data_source);
221 url.push(',');
222 } else {
223 return Err(FlightRadarError::Parameter(format!(
224 "Data Source: {}",
225 data_source
226 )));
227 }
228 }
229 url.pop();
230 }
231 if let Some(airspaces) = ¶ms.airspaces {
232 url.push_str("&airspaces=");
234 for airspace in airspaces {
235 if airspace.chars().all(char::is_alphabetic) {
236 url.push_str(&airspace.to_string());
237 url.push(',');
238 } else {
239 return Err(FlightRadarError::Parameter(format!(
240 "Airspace: {}",
241 airspace
242 )));
243 }
244 }
245 url.pop();
246 }
247 if let Some(gspeed) = ¶ms.gspeed {
248 match gspeed {
249 ApiRangeEnum::U32(val) => {
250 if val > &5000 {
252 return Err(FlightRadarError::Parameter(format!("GSpeed: {}", val)));
253 }
254 url.push_str("&gspeed=");
255 url.push_str(&val.to_string());
256 }
257 ApiRangeEnum::ApiRange(gspeed) => {
258 if gspeed.max > 5000 {
260 return Err(FlightRadarError::Parameter(format!(
261 "GSpeed: {}",
262 gspeed.max
263 )));
264 }
265 url.push_str("&gspeed=");
266 url.push_str(&gspeed.min.to_string());
267 url.push('-');
268 url.push_str(&gspeed.max.to_string());
269 }
270 }
271 };
272 if let Some(limit) = ¶ms.limit {
273 url.push_str("&limit=");
274 url.push_str(&limit.to_string());
275 }
276
277 Ok(url)
278 }
279
280 pub fn check_live_parameters(query_in: &FullLiveFlightQuery) -> bool {
286 if query_in.aircraft.is_some()
287 || query_in.airports.is_some()
288 || query_in.airspaces.is_some()
289 || query_in.altitude_ranges.is_some()
290 || query_in.bounds.is_some()
291 || query_in.callsigns.is_some()
292 || query_in.categories.is_some()
293 || query_in.data_sources.is_some()
294 || query_in.flights.is_some()
295 || query_in.gspeed.is_some()
296 || query_in.operating_as.is_some()
297 || query_in.painted_as.is_some()
298 || query_in.registrations.is_some()
299 || query_in.routes.is_some()
300 || query_in.squawks.is_some()
301 {
302 return true;
303 };
304
305 false
307 }
308
309 pub fn check_historic_parameters(query_in: &FullLiveFlightQuery) -> bool {
310 if query_in.aircraft.is_some()
311 || query_in.airports.is_some()
312 || query_in.altitude_ranges.is_some()
313 || query_in.bounds.is_some()
314 || query_in.callsigns.is_some()
315 || query_in.categories.is_some()
316 || query_in.data_sources.is_some()
317 || query_in.flights.is_some()
318 || query_in.gspeed.is_some()
319 || query_in.operating_as.is_some()
320 || query_in.painted_as.is_some()
321 || query_in.registrations.is_some()
322 || query_in.routes.is_some()
323 || query_in.squawks.is_some()
324 {
325 return true;
326 };
327
328 false
330 }
331
332 pub fn query_endpoint(&self, url: String) -> Result<String, FlightRadarError> {
338 let response = self
339 .client
340 .get(&url)
341 .header("Accept-Version", "v1")
342 .bearer_auth(&self.api_key)
343 .send()?; let response_text = response.text()?; Ok(response_text)
346 }
347
348 pub fn get_airline_by_icao(&self, icao: &str) -> Result<Airline, FlightRadarError> {
354 let url = format!("{}static/airlines/{}/light", self.base_url, icao);
356 let text = match self.query_endpoint(url) {
357 Ok(data) => data,
358 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
359 };
360
361 let airline: Airline = serde_json::from_str(&text)
363 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
364
365 Ok(airline)
366 }
367
368 pub fn get_airport_by_code(&self, code: &str) -> Result<Airport, FlightRadarError> {
374 let url = format!("{}static/airports/{}/full", self.base_url, code);
376 let text = match self.query_endpoint(url) {
377 Ok(data) => data,
378 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
379 };
380
381 let airport: Airport = serde_json::from_str(&text)
383 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
384
385 Ok(airport)
386 }
387
388 pub fn get_airport_lite_by_code(&self, code: &str) -> Result<AirportLite, FlightRadarError> {
394 let url = format!("{}static/airports/{}/light", self.base_url, code);
396 let text = match self.query_endpoint(url) {
397 Ok(data) => data,
398 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
399 };
400
401 let airport: AirportLite = serde_json::from_str(&text)
403 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
404
405 Ok(airport)
406 }
407
408 pub fn get_live_flight(
414 &self,
415 other_queries: Option<&FullLiveFlightQuery>,
416 ) -> Result<FullLiveFlightResponse, FlightRadarError> {
417 let defualt_query_in = &FullLiveFlightQuery::default();
419 let other_query_in = match other_queries {
420 Some(data) => data,
421 _ => defualt_query_in,
422 };
423
424 if !Self::check_live_parameters(other_query_in) {
426 return Err(FlightRadarError::Parameter(
427 "Missing One Of Required Parameters".to_string(),
428 ));
429 }
430
431 let params_back = Self::build_query_params(other_query_in)?;
432 let endpoint = format!("{}live/flight-positions/full{}", self.base_url, params_back);
433
434 let text = match self.query_endpoint(endpoint) {
438 Ok(data) => data,
439 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
440 };
441 let live_data: FullLiveFlightResponse = serde_json::from_str(&text)
445 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
446
447 Ok(live_data)
448 }
449
450 pub fn get_live_flight_light(
456 &self,
457 other_queries: Option<&FullLiveFlightQuery>,
458 ) -> Result<LightLiveFlightResponse, FlightRadarError> {
459 let defualt_query_in = &FullLiveFlightQuery::default();
461 let other_query_in = match other_queries {
462 Some(data) => data,
463 _ => defualt_query_in,
464 };
465
466 if !Self::check_live_parameters(other_query_in) {
468 return Err(FlightRadarError::Parameter(
469 "Missing One Of Required Parameters".to_string(),
470 ));
471 }
472
473 let params_back = Self::build_query_params(other_query_in)?;
474 let endpoint = format!("{}live/flight-positions/full{}", self.base_url, params_back);
475
476 let text = match self.query_endpoint(endpoint) {
480 Ok(data) => data,
481 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
482 };
483 let live_data: LightLiveFlightResponse = serde_json::from_str(&text)
487 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
488
489 Ok(live_data)
490 }
491
492 pub fn get_historic_flight(
499 &self,
500 timestamp: &u64,
501 other_queries: Option<&FullLiveFlightQuery>,
502 ) -> Result<FullLiveFlightResponse, FlightRadarError> {
503 const MIN_TIMESTAMP: u64 = 1462924800;
505 if timestamp < &MIN_TIMESTAMP {
506 return Err(FlightRadarError::Parameter(format!(
507 "Invalid Timestamp: {}",
508 timestamp
509 )));
510 };
511
512 let stuff = match other_queries {
514 Some(stuff) => stuff,
515 None => &FullLiveFlightQuery::default(),
516 };
517
518 if !Self::check_historic_parameters(stuff) {
519 return Err(FlightRadarError::Parameter(
520 "Missing One Of Required Parameters".to_string(),
521 ));
522 }
523
524 let params_back = if let Some(other_queries) = other_queries {
526 match Self::build_query_params(other_queries) {
527 Ok(param_str) => param_str[1..param_str.len()].to_string(),
528 Err(_) => String::from(""),
529 }
530 } else {
531 String::from("")
532 };
533
534 let endpoint = format!(
535 "{}historic/flight-positions/full?timestamp={}&{}",
536 self.base_url, timestamp, params_back
537 );
538
539 let text = match self.query_endpoint(endpoint) {
541 Ok(data) => data,
542 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
543 };
544 let historic_data: FullLiveFlightResponse = serde_json::from_str(&text)
548 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
549
550 Ok(historic_data)
551 }
552
553 pub fn get_historic_flight_light(
560 &self,
561 timestamp: &u64,
562 other_queries: Option<&FullLiveFlightQuery>,
563 ) -> Result<LightLiveFlightResponse, FlightRadarError> {
564 const MIN_TIMESTAMP: u64 = 1462924800;
566 if timestamp < &MIN_TIMESTAMP {
567 return Err(FlightRadarError::Parameter(format!(
568 "Invalid Timestamp: {}",
569 timestamp
570 )));
571 };
572
573 let stuff = match other_queries {
575 Some(stuff) => stuff,
576 None => &FullLiveFlightQuery::default(),
577 };
578
579 if !Self::check_historic_parameters(stuff) {
580 return Err(FlightRadarError::Parameter(
581 "Missing One Of Required Parameters".to_string(),
582 ));
583 }
584
585 let params_back = if let Some(other_queries) = other_queries {
587 match Self::build_query_params(other_queries) {
588 Ok(param_str) => param_str[1..param_str.len()].to_string(),
589 Err(_) => String::from(""),
590 }
591 } else {
592 String::from("")
593 };
594
595 let endpoint = format!(
596 "{}historic/flight-positions/light?timestamp={}&{}",
597 self.base_url, timestamp, params_back
598 );
599
600 let text = match self.query_endpoint(endpoint) {
602 Ok(data) => data,
603 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
604 };
605 let historic_data: LightLiveFlightResponse = serde_json::from_str(&text)
609 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
610
611 Ok(historic_data)
612 }
613
614 pub fn get_flight_tracks_by_id(
620 &self,
621 flight_id: &str,
622 ) -> Result<Vec<Flight>, FlightRadarError> {
623 if u64::from_str_radix(flight_id, 16).is_err() {
625 return Err(FlightRadarError::Parameter(format!(
626 "Flight ID Not Hexadecimal: {}",
627 flight_id
628 )));
629 }
630
631 let url = format!("{}flight-tracks?flight_id={}", self.base_url, flight_id);
633 let text = match self.query_endpoint(url) {
634 Ok(data) => data,
635 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
636 };
637 let flights: Vec<Flight> = serde_json::from_str(&text)
641 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
642
643 Ok(flights)
644 }
645
646 pub fn get_api_usage(&self, period: &str) -> Result<ApiUsageResponse, FlightRadarError> {
652 (match period {
654 "24h" | "7d" | "30d" | "1y" => Ok(()),
655 _ => Err(FlightRadarError::Parameter(format!(
656 "Period: {}. Should be: 24h|7d|30d|1y",
657 period
658 ))),
659 })?;
660
661 let url = format!("{}usage?period={}", self.base_url, period);
663 let text = match self.query_endpoint(url) {
664 Ok(data) => data,
665 Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
666 };
667 let usage: ApiUsageResponse = serde_json::from_str(&text)
671 .map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
672
673 Ok(usage)
674 }
675}
676
677#[derive(Debug, Deserialize, Default)]
679pub struct Track {
680 pub timestamp: String,
681 pub lat: f64,
682 pub lon: f64,
683 pub alt: u32,
684 pub gspeed: u32,
685 pub vspeed: u32,
686 pub track: u32,
687 pub squawk: String,
688 pub callsign: String,
689 pub source: String,
690}
691
692#[derive(Debug, Deserialize, Default)]
694pub struct Flight {
695 #[serde(rename = "fr24_id")]
696 pub id: String,
697 pub tracks: Vec<Track>,
698}
699
700#[derive(Debug, Deserialize, Default)]
702pub struct ApiUsageResponse {
703 pub data: Vec<ApiEndpointUsage>,
704}
705
706#[derive(Debug, Deserialize, Default)]
708pub struct ApiEndpointUsage {
709 pub endpoint: String,
710 pub metadata: String,
711 pub request_count: u32,
712 pub results: u32,
713 pub credits: u32,
714}
715
716#[derive(Debug, Deserialize, Default)]
718pub struct Airline {
719 pub name: String,
720 pub iata: Option<String>,
721 pub icao: String,
722}
723
724#[derive(Debug, Deserialize, Default)]
726pub struct Airport {
727 pub name: String,
728 pub iata: String,
729 pub icao: String,
730 pub lon: f64,
731 pub lat: f64,
732 pub elevation: i32,
733 pub country: Country,
734 pub city: String,
735 pub state: Option<String>,
736 pub timezone: Timezone,
737}
738
739#[derive(Debug, Deserialize, Default)]
741pub struct Country {
742 pub code: String,
743 pub name: String,
744}
745
746#[derive(Debug, Deserialize, Default)]
748pub struct Timezone {
749 pub name: String,
750 pub offset: i32,
751}
752
753#[derive(Debug, Deserialize, Default)]
755pub struct AirportLite {
756 pub name: String,
757 pub iata: String,
758 pub icao: String,
759}
760
761#[derive(Debug, Deserialize, Default)]
763pub struct FullLiveFlightQuery {
764 pub bounds: Option<Bounds>,
765 pub flights: Option<Vec<String>>,
766 pub callsigns: Option<Vec<String>>,
767 pub registrations: Option<Vec<String>>,
768 pub painted_as: Option<Vec<String>>,
769 pub operating_as: Option<Vec<String>>,
770 pub airports: Option<Vec<String>>,
771 pub routes: Option<Vec<String>>,
772 pub aircraft: Option<Vec<String>>,
773 pub altitude_ranges: Option<Vec<ApiRange>>,
774 pub squawks: Option<Vec<u16>>,
775 pub categories: Option<Vec<char>>,
776 pub data_sources: Option<Vec<String>>,
777 pub airspaces: Option<Vec<String>>,
778 pub gspeed: Option<ApiRangeEnum>,
779 pub limit: Option<u32>,
780}
781
782#[derive(Debug, Deserialize, Default, Serialize)]
784pub struct Bounds {
785 pub north: f64,
786 pub south: f64,
787 pub west: f64,
788 pub east: f64,
789}
790
791#[derive(Debug, Deserialize, Default)]
793pub struct ApiRange {
794 pub min: u32,
795 pub max: u32,
796}
797
798#[derive(Debug, Deserialize)]
800pub enum ApiRangeEnum {
801 U32(u32),
802 ApiRange(ApiRange),
803}
804
805#[derive(Deserialize, Debug, Default)]
807pub struct FullLiveFlightResponse {
808 pub data: Vec<FullLiveFlightData>,
809}
810
811#[derive(Deserialize, Debug, Default)]
813pub struct FullLiveFlightData {
814 pub fr24_id: String,
815 pub flight: String,
816 pub callsign: String,
817 pub lat: f64,
818 pub lon: f64,
819 pub track: u32,
820 pub alt: u32,
821 pub gspeed: u32,
822 pub vspeed: i32, pub squawk: String,
824 pub timestamp: String,
825 pub source: String,
826 pub hex: String,
827 #[serde(rename = "type")]
829 pub type_field: String,
830 pub reg: String,
831 pub painted_as: String,
832 pub operating_as: String,
833 pub orig_iata: String,
834 pub orig_icao: String,
835 pub dest_iata: String,
836 pub dest_icao: String,
837 pub eta: String,
838}
839
840#[derive(Deserialize, Debug, Default)]
842pub struct LightLiveFlightResponse {
843 pub data: Vec<LightLiveFlightData>,
844}
845
846#[derive(Deserialize, Debug, Default)]
848pub struct LightLiveFlightData {
849 pub fr24_id: String,
850 pub hex: String,
851 pub callsign: String,
852 pub lat: f64,
853 pub lon: f64,
854 pub track: u32,
855 pub alt: u32,
856 pub gspeed: u32,
857 pub vspeed: i32, pub squawk: String,
859 pub timestamp: String,
860 pub source: String,
861}