use std::{collections::HashMap, time::Instant}; use chrono::DateTime; use reqwest::get; use serde::Deserialize; pub struct ApiClient { pub swu_stop_number: u32, pub swu_api_limit: u8, pub departures: Vec, pub directions: HashMap>>, } impl ApiClient { pub fn new() -> Self { Self::new_custom(1240) } pub fn new_custom(swu_stop_number: u32) -> Self { Self::new_custom_with_limit(swu_stop_number, 30) } pub fn new_custom_with_limit(swu_stop_number: u32, swu_api_limit: u8) -> Self { Self { swu_stop_number, swu_api_limit, departures: vec![], directions: HashMap::new(), } } pub async fn api_get(&mut self) -> Result<(), String> { // get departures let departures = self.api_get_departures().await?; // fetch trip data (-> direction) for vehicles which have departed and cache it for next vehicles for departure in self.departures.iter() { if let Some(route_name) = &departure.route_name { if let Some(departure_direction_text) = &departure.departure_direction_text { if let Some(vehicle_number) = departure.vehicle_number { if departures.iter().all(|dep| { dep.route_name.as_ref().is_none_or(|v| v != route_name) || dep.vehicle_number.is_none_or(|v| v != vehicle_number) || dep .departure_direction_text .as_ref() .is_none_or(|v| v != departure_direction_text) }) { // vehicle has just departed, check direction via api let dir_value = self .directions .entry(route_name.clone()) .or_insert_with(HashMap::new) .entry(departure_direction_text.clone()) .or_insert(None); if dir_value.is_none_or(|(when, _, success_rate)| { when.elapsed().as_secs() >= (success_rate * 15).saturating_sub(5) as u64 }) { match Self::api_get_trip(vehicle_number).await { Ok(res) => { if res.route_number == departure.route_number && res .departure_direction_text .as_ref() .is_some_and(|ddt| ddt == departure_direction_text) { if let Some(dir) = res.direction { if let Some((when, prev, success_rate)) = dir_value { *when = Instant::now(); if dir == *prev { *success_rate = success_rate.saturating_add(1).min(60); eprintln!("[CACHE/dirs] {route_name} -> {departure_direction_text} => d{dir} (confirmed, success now {})", *success_rate); } else { if *success_rate <= 2 { eprintln!("[CACHE/dirs] {route_name} -> {departure_direction_text} => d{dir} (changed from {})", *prev); *prev = dir; *success_rate = 1; } else { *success_rate /= 2; eprintln!("[CACHE/dirs] {route_name} -> {departure_direction_text} => d{dir} (contradicted, success now {})", *success_rate); } } } else { eprintln!("[CACHE/dirs] {route_name} -> {departure_direction_text} => d{dir} (new info)"); *dir_value = Some((Instant::now(), dir, 1)); } } } else { // ignore cuz is wrong route or wrong direction } } Err(e) => eprintln!("{e}"), } } } } } } } self.departures = departures; Ok(()) } async fn api_get_departures(&self) -> Result, String> { match get(format!( "https://api.swu.de/mobility/v1/stop/passage/Departures?StopNumber={}&Limit={}", self.swu_stop_number, self.swu_api_limit )) .await { Ok(response) => match response.text().await { Ok(response) => match serde_json::from_str::(&response) { Ok(response) => Ok(response.stop_passage.departure_data), Err(e) => Err(format!("{}\n{}", e, response)), }, Err(e) => Err(e.to_string()), }, Err(e) => Err(e.to_string()), } } async fn api_get_trip(vehicle_number: u32) -> Result { match get(format!( "https://api.swu.de/mobility/v1/vehicle/trip/Trip?VehicleNumber={vehicle_number}" )) .await { Ok(response) => match response.text().await { Ok(response) => match serde_json::from_str::(&response) { Ok(trip) => Ok(trip.vehicle_trip.trip_data.journey_data), Err(e) => Err(format!("{}\n{}", e, response)), }, Err(e) => Err(e.to_string()), }, Err(e) => return Err(e.to_string()), } } } pub async fn bahnhof_get_departures() -> Result { match get( "https://www.bahnhof.de/api/boards/departures?evaNumbers=8000170&filterTransports=HIGH_SPEED_TRAIN&filterTransports=INTERCITY_TRAIN&filterTransports=INTER_REGIONAL_TRAIN&filterTransports=REGIONAL_TRAIN&filterTransports=CITY_TRAIN&filterTransports=UNKNOWN&duration=120&locale=de", ) .await { Ok(response) => match response.text().await { Ok(response) => match serde_json::from_str::(&response) { Ok(response) => { let mut o = format!("
\n"); for departure in response.entries.into_iter().flat_map(|v| v.into_iter()) { if let Some(line_name) = &departure.line_name { o.push_str(if departure.canceled { r#"
"# } else { r#"
"# }); o.push_str(""); html_escape::encode_safe_to_string(line_name, &mut o); o.push_str(""); if let Some(departure_time) = departure.time_delayed.or(departure.time_schedule) { o.push_str(" "); o.push_str( &departure_time.naive_local().format("%H:%M").to_string(), ); } if let Some(destination) = departure.destination.as_ref().and_then(|v| v.name.as_ref()) { o.push_str("
"); html_escape::encode_safe_to_string(destination, &mut o); } o.push_str("
\n"); } } o.push_str("
"); Ok(o) } Err(e) => Err(format!("{e}\n{response}")), }, Err(e) => Err(e.to_string()), }, Err(e) => Err(e.to_string()), } } #[derive(Deserialize)] struct ResDepartures { #[serde(rename = "StopPassage")] pub stop_passage: ResStopPassage, } #[derive(Deserialize)] struct ResStopPassage { #[serde(rename = "DepartureData")] pub departure_data: Vec, } #[derive(Debug, Deserialize)] pub struct Departure { #[serde(default, rename = "RouteName")] pub route_name: Option, #[serde(default, rename = "RouteNumber")] pub route_number: Option, #[serde(default, rename = "DepartureDirectionText")] pub departure_direction_text: Option, #[serde(default, rename = "VehicleNumber")] pub vehicle_number: Option, // #[serde(default, rename = "DepartureCountdown")] // pub departure_countdown: Option, #[serde(default, rename = "DepartureTimeActual")] pub departure_time_actual: Option>, #[serde(default, rename = "DepartureTimeScheduled")] pub departure_time_scheduled: Option>, } #[derive(Deserialize)] struct ResTrip { #[serde(rename = "VehicleTrip")] pub vehicle_trip: ResVehicleTrip, } #[derive(Deserialize)] struct ResVehicleTrip { #[serde(rename = "TripData")] trip_data: ResTripData, } #[derive(Deserialize)] struct ResTripData { #[serde(rename = "JourneyData")] journey_data: JourneyData, } #[derive(Deserialize)] pub struct JourneyData { #[serde(default, rename = "RouteNumber")] pub route_number: Option, #[serde(default, rename = "DepartureDirectionText")] pub departure_direction_text: Option, #[serde(default, rename = "Direction")] pub direction: Option, } #[derive(Deserialize)] struct BahnhofResDepartures { entries: Vec>, } #[derive(Deserialize)] struct BahnhofResDeparture { #[serde(default, rename = "timeSchedule")] time_schedule: Option>, #[serde(default, rename = "timeDelayed")] time_delayed: Option>, #[serde(default, rename = "canceled")] canceled: bool, // #[serde(default, rename = "platform")] // platform: Option, #[serde(default, rename = "lineName")] line_name: Option, #[serde(default, rename = "destination")] destination: Option, } #[derive(Deserialize)] struct BahnhofResDepartureDestination { #[serde(default, rename = "name")] name: Option, }