diff --git a/Cargo.lock b/Cargo.lock index 7e5d422..83b54a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1722,6 +1722,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "thiserror", "tokio", ] diff --git a/programs/traveldings/Cargo.toml b/programs/traveldings/Cargo.toml index 3b8ad29..c21a6d1 100644 --- a/programs/traveldings/Cargo.toml +++ b/programs/traveldings/Cargo.toml @@ -8,6 +8,7 @@ serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.128" reqwest = {version = "0.12.7", default-features = false, features = ["rustls-tls", "charset", "http2"]} tokio = { version = "1", features = ["full"] } +thiserror = "1" anyhow = "1" chrono = { version = "0.4", features = ["serde"]} clap = { version = "4.5", features = ["derive"]} diff --git a/programs/traveldings/src/commands/current_journey.rs b/programs/traveldings/src/commands/current_journey.rs index ee57973..c7a8792 100644 --- a/programs/traveldings/src/commands/current_journey.rs +++ b/programs/traveldings/src/commands/current_journey.rs @@ -1,9 +1,182 @@ -use crate::traewelling::TraewellingClient; +use std::time::Duration; + +use chrono::Local; +use reqwest::StatusCode; +use serde::Serialize; +use tokio::time::sleep; + +use crate::traewelling::{ + model::{JsonableData, Status, StopJourneyPart}, + RequestErr, TraewellingClient, +}; pub async fn get_current_journey() -> anyhow::Result<()> { let client = TraewellingClient::new()?; - println!("active: {:#?}", client.get_active_checkin().await?); + let mut state; + let mut cur_active_checkin = None; + + loop { + match client.get_active_checkin().await { + Ok(status) => { + cur_active_checkin = Some(status); + state = State::Live; + } + Err(err) => { + if err == RequestErr::WithStatus(StatusCode::NOT_FOUND) { + state = State::NoCheckin; + cur_active_checkin = None; + } else { + state = State::NoConnectionOrSomethingElseDoesntWork; + } + } + }; + + match (state, &cur_active_checkin) { + (State::Live | State::NoConnectionOrSomethingElseDoesntWork, Some(status)) => { + let live = state == State::Live; + let out = CurrentJourneyOutput::new(&status, live); + + println!( + "{}", + serde_json::to_string(&out) + .expect("serde should not make you sad but it does because it's serde") + ); + sleep(Duration::from_secs(20)).await; + } + (_, None) | (State::NoCheckin, Some(_)) => { + println!("null"); + sleep(Duration::from_secs(60)).await; + } + } + } Ok(()) } + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +enum State { + Live, + NoConnectionOrSomethingElseDoesntWork, + NoCheckin, +} + +#[derive(Serialize)] +struct CurrentJourneyOutput { + live: bool, + // Journey progress, 0.0-1.0 + progress: Option, + time_left: Option, + icon: String, + + // Invalid data received? + departure_err: bool, + departure_planned: Option, + departure_real: Option, + departure_station: String, + departure_ril100: Option, + departure_platform_data_available: bool, + departure_platform_planned: Option, + departure_platform_real: Option, + + // Invalid data received? + arrival_err: bool, + arrival_planned: Option, + arrival_real: Option, + arrival_station: String, + arrival_ril100: Option, + arrival_platform_data_available: bool, + arrival_platform_planned: Option, + arrival_platform_real: Option, +} + +impl CurrentJourneyOutput { + fn new(checkin: &Status, live: bool) -> Self { + let JsonableData { + time_err: departure_err, + time_planned: departure_planned, + time_real: departure_real, + station: departure_station, + ril100: departure_ril100, + platform_data_available: departure_platform_data_available, + platform_planned: departure_platform_planned, + platform_real: departure_platform_real, + } = checkin.train.origin.get_time_data(StopJourneyPart::Origin); + let JsonableData { + time_err: arrival_err, + time_planned: arrival_planned, + time_real: arrival_real, + station: arrival_station, + ril100: arrival_ril100, + platform_data_available: arrival_platform_data_available, + platform_planned: arrival_platform_planned, + platform_real: arrival_platform_real, + } = checkin + .train + .destination + .get_time_data(StopJourneyPart::Destination); + + let (progress, time_left) = if !departure_err && !arrival_err { + let departure = departure_real.unwrap_or(departure_planned.unwrap()); + let arrival = arrival_real.unwrap_or(arrival_planned.unwrap()); + let dur = arrival - departure; + + let now = Local::now().timestamp(); + + let progress = ((now - departure) as f32) / dur as f32; + let time_left = arrival - now; + + (Some(progress), Some(time_left)) + } else { + (None, None) + }; + + let icon = match checkin.train.category.as_str() { + "nationalExpress" | "national" => "longDistanceTrans", + "regionalExp" | "regional" => "regionalTrans", + "suburban" => "localTrans", + "subway" => "subTrans", + "bus" => "bus", + "tram" => "tram", + "ferry" => "ferry", + _ => "other", + } + .to_string(); + + CurrentJourneyOutput { + live, + progress, + time_left, + icon, + departure_err, + departure_planned, + departure_real, + departure_station, + departure_ril100, + departure_platform_data_available, + departure_platform_planned, + departure_platform_real, + arrival_err, + arrival_planned, + arrival_real, + arrival_station, + arrival_ril100, + arrival_platform_data_available, + arrival_platform_planned, + arrival_platform_real, + } + } +} + +enum TransportType { + // FV, ob jetzt NJ, IC, ICE... egal + LongDistanceTrans, + RegionalTrans, + // S-bahn... + LocalTrans, + // U-bahn + SubTrans, + Bus, + Tram, + Ferry, +} diff --git a/programs/traveldings/src/traewelling.rs b/programs/traveldings/src/traewelling.rs index 3f55f5a..e1537e8 100644 --- a/programs/traveldings/src/traewelling.rs +++ b/programs/traveldings/src/traewelling.rs @@ -3,7 +3,7 @@ use std::{fmt, fs}; use model::{Container, Status}; use reqwest::{ header::{self, HeaderMap}, - Client, ClientBuilder, + Client, ClientBuilder, StatusCode, }; const KEY_PATH: &str = "/home/jade/Docs/traveldings-key"; @@ -19,7 +19,6 @@ impl TraewellingClient { let mut headers = HeaderMap::new(); let token = fs::read_to_string(KEY_PATH)?; let key = header::HeaderValue::from_str(&format!("Bearer {token}"))?; - println!("meow"); headers.insert("Authorization", key); headers.insert( header::ACCEPT, @@ -33,16 +32,17 @@ impl TraewellingClient { }) } - pub async fn get_active_checkin(&self) -> anyhow::Result { - let txt = self + pub async fn get_active_checkin(&self) -> Result { + let res = self .client .get(Self::fmt_url("user/statuses/active")) .send() - .await? - .text() .await?; + if res.status() != StatusCode::OK { + return Err(RequestErr::WithStatus(res.status())); + } - println!("{txt}"); + let txt = res.text().await?; let res: Container = serde_json::de::from_str(&txt)?; Ok(res.data) @@ -53,4 +53,35 @@ impl TraewellingClient { } } +#[derive(thiserror::Error, Debug, PartialEq, Eq)] +pub enum RequestErr { + #[error("Couldn't deserialize the json :(")] + DeserializationError, + #[error("an error related to connect happened!!")] + RelatedToConnect, + #[error("error haz status: {0}")] + WithStatus(StatusCode), + #[error("fuck if i know what went wrong :333 am silly ")] + Other, +} + +impl From for RequestErr { + fn from(value: serde_json::Error) -> Self { + eprintln!("serde error: {value:?}"); + Self::DeserializationError + } +} + +impl From for RequestErr { + fn from(value: reqwest::Error) -> Self { + if let Some(status) = value.status() { + Self::WithStatus(status) + } else if value.is_connect() { + Self::RelatedToConnect + } else { + Self::Other + } + } +} + pub mod model; diff --git a/programs/traveldings/src/traewelling/model.rs b/programs/traveldings/src/traewelling/model.rs index b40554a..4b54c7b 100644 --- a/programs/traveldings/src/traewelling/model.rs +++ b/programs/traveldings/src/traewelling/model.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, FixedOffset}; +use chrono::{DateTime, FixedOffset, Timelike}; use serde::Deserialize; #[derive(Deserialize, Debug)] @@ -9,39 +9,77 @@ pub struct Container { #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Status { - train: TransportResource, + pub train: TransportResource, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct TransportResource { - category: String, - line_name: String, - distance: u32, - duration: u32, - operator: OperatorResource, - origin: StopOverResource, - destination: StopOverResource, + pub category: String, + pub line_name: String, + pub distance: u32, + pub duration: u32, + pub operator: Option, + pub origin: StopOverResource, + pub destination: StopOverResource, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct StopOverResource { - name: String, - ril_identifier: Option, - arrival: Option>, - arrival_planned: Option>, - arrival_real: Option>, - departure: Option>, - departure_planned: Option>, - departure_real: Option>, - platform: Option, - departure_platform_planned: Option, - departure_platform_real: Option, + pub name: String, + pub ril_identifier: Option, + pub arrival_planned: Option>, + pub arrival_real: Option>, + pub departure_planned: Option>, + pub departure_real: Option>, + pub platform: Option, + pub departure_platform_planned: Option, + pub departure_platform_real: Option, +} + +// ???? +pub struct JsonableData { + pub time_err: bool, + pub time_planned: Option, + pub time_real: Option, + pub station: String, + pub ril100: Option, + pub platform_data_available: bool, + pub platform_planned: Option, + pub platform_real: Option, +} + +// What the meaning of the stop in the journey is +pub enum StopJourneyPart { + Origin, + Destination, +} +impl StopOverResource { + pub fn get_time_data(&self, journey_part: StopJourneyPart) -> JsonableData { + let (time_planned, time_real) = match journey_part { + StopJourneyPart::Origin => (self.departure_planned, self.departure_real), + StopJourneyPart::Destination => (self.arrival_planned, self.arrival_real), + }; + + let time_err = time_planned == None; + + JsonableData { + time_err, + time_planned: time_planned.map(|ts| ts.timestamp()), + time_real: time_real.map(|ts| ts.timestamp()), + station: self.name.clone(), + ril100: self.ril_identifier.clone(), + platform_data_available: self.departure_platform_planned.is_none() + || self.departure_platform_real.is_none(), + platform_planned: self.departure_platform_planned.clone(), + platform_real: self.departure_platform_real.clone(), + } + } } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct OperatorResource { - name: String, + pub name: String, }