a rust tui to view amtrak train status
at main 103 lines 2.8 kB view raw
1// Amtrak API client - fetch and decrypt train data 2 3use anyhow::{Context, Result}; 4use serde::Deserialize; 5 6use crate::crypto::{self, DecryptionKeys}; 7use crate::model::{self, Station, Train}; 8 9const TRAINS_URL: &str = "https://maps.amtrak.com/services/MapDataService/trains/getTrainsData"; 10const STATIONS_URL: &str = "https://maps.amtrak.com/services/MapDataService/stations/trainStations"; 11const ROUTES_URL: &str = "https://maps.amtrak.com/rttl/js/RoutesList.json"; 12const ROUTES_V_URL: &str = "https://maps.amtrak.com/rttl/js/RoutesList.v.json"; 13 14#[derive(Deserialize)] 15struct RouteEntry { 16 #[serde(default)] 17 #[serde(alias = "ZoomLevel")] 18 zoom_level: Option<u32>, 19} 20 21#[derive(Deserialize)] 22struct RoutesVConfig { 23 arr: Vec<String>, 24 s: Vec<String>, 25 v: Vec<String>, 26} 27 28pub async fn fetch_decryption_keys(client: &reqwest::Client) -> Result<DecryptionKeys> { 29 // Fetch route list to calculate masterZoom 30 let routes: Vec<RouteEntry> = client 31 .get(ROUTES_URL) 32 .send() 33 .await 34 .context("failed to fetch routes list")? 35 .json() 36 .await 37 .context("failed to parse routes list")?; 38 39 let master_zoom = routes 40 .iter() 41 .map(|r| r.zoom_level.unwrap_or(0)) 42 .sum::<u32>() as usize; 43 44 // Fetch keys config 45 let config: RoutesVConfig = client 46 .get(ROUTES_V_URL) 47 .send() 48 .await 49 .context("failed to fetch routes config")? 50 .json() 51 .await 52 .context("failed to parse routes config")?; 53 54 let public_key = config 55 .arr 56 .get(master_zoom) 57 .context("masterZoom index out of bounds")? 58 .clone(); 59 60 let salt = crypto::extract_salt(&config.s); 61 let iv = crypto::extract_iv(&config.v); 62 63 Ok(DecryptionKeys { 64 public_key, 65 salt, 66 iv, 67 }) 68} 69 70pub async fn fetch_stations( 71 client: &reqwest::Client, 72 keys: &DecryptionKeys, 73) -> Result<Vec<Station>> { 74 let encrypted = client 75 .get(STATIONS_URL) 76 .send() 77 .await 78 .context("failed to fetch station data")? 79 .text() 80 .await 81 .context("failed to read station data response")?; 82 83 let json = crypto::decrypt_train_data(encrypted.trim(), keys) 84 .context("failed to decrypt station data")?; 85 86 model::parse_station_response(&json) 87} 88 89pub async fn fetch_trains(client: &reqwest::Client, keys: &DecryptionKeys) -> Result<Vec<Train>> { 90 let encrypted = client 91 .get(TRAINS_URL) 92 .send() 93 .await 94 .context("failed to fetch train data")? 95 .text() 96 .await 97 .context("failed to read train data response")?; 98 99 let json = crypto::decrypt_train_data(encrypted.trim(), keys) 100 .context("failed to decrypt train data")?; 101 102 model::parse_train_response(&json) 103}