From e2fcae8e8ece76cb11716eaf3a7a065c0ce6fa7c Mon Sep 17 00:00:00 2001 From: Mark Date: Fri, 18 Jul 2025 11:13:44 +0200 Subject: [PATCH] use bahn.de api --- src/api.rs | 145 +++++++++++++++++++++++++++++++++++++------------ src/index.html | 13 +++++ 2 files changed, 122 insertions(+), 36 deletions(-) diff --git a/src/api.rs b/src/api.rs index ead0ecb..37265c6 100644 --- a/src/api.rs +++ b/src/api.rs @@ -3,7 +3,7 @@ use std::{ time::{Duration, Instant}, }; -use chrono::DateTime; +use chrono::{DateTime, NaiveDateTime}; use reqwest::get; use serde::Deserialize; @@ -157,38 +157,99 @@ impl ApiClient { } 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 - { + let url = r#"https://www.bahn.de/web/api/reiseloesung/abfahrten?ortExtId=8000170&mitVias=true&maxVias=8&verkehrsmittel[]=ICE&verkehrsmittel[]=EC_IC&verkehrsmittel[]=IR&verkehrsmittel[]=REGIONAL&verkehrsmittel[]=SBAHN"#; + match get(url).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()) { + for departure in response.entries.into_iter() { if let Some(line_name) = &departure.line_name { - o.push_str(if departure.canceled { - r#"
"# + if let Some(messages) = + departure.messages.as_ref().filter(|v| !v.is_empty()) + { + if messages.iter().any(|msg| { + msg.priority + .as_ref() + .is_some_and(|prio| ["HOCH"].contains(&prio.as_str())) + }) { + o.push_str(r#"
"#); + } else { + o.push_str(r#"
"#); + } + for message in messages.iter() { + o.push_str( + match message.priority.as_ref().map(|v| v.as_str()) { + Some("HOCH") => r#"

"#, + Some("NORMAL") => r#"

"#, + Some("NIEDRIG") => r#"

"#, + None | Some(_) => r#"

"#, + }, + ); + if let Some(text) = &message.text { + html_escape::encode_safe_to_string(text, &mut o); + } + o.push_str("

"); + } + o.push_str(r#"
"#); } else { - r#"
"# - }); + o.push_str("
"); + } o.push_str(""); - html_escape::encode_safe_to_string(line_name, &mut o); + let line_name = if let Some(p) = &line_name.product { + if let Some(nr) = &line_name.line_number { + format!("{p} {nr}") + } else { + format!("{p}") + } + } else { + "[?]".to_owned() + }; + 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(" "); + o.push_str(&departure_time.format("%H:%M").to_string()); + if let Some(platform) = departure + .platform + .as_ref() + .or(departure.platform_schedule.as_ref()) + { + o.push_str(""); + o.push_str(&format!(" @ {platform}")); + o.push_str(""); + } else { + o.push_str(""); + } + } else if let Some(platform) = departure + .platform + .as_ref() + .or(departure.platform_schedule.as_ref()) { - o.push_str("
"); + o.push_str(""); + o.push_str(&format!(" @ {platform}")); + o.push_str(""); + } + if let Some(destination) = &departure.destination { + if let Some(messages) = + departure.messages.as_ref().filter(|v| !v.is_empty()) + { + if messages.iter().any(|msg| { + msg.priority + .as_ref() + .is_some_and(|prio| ["HOCH"].contains(&prio.as_str())) + }) { + o.push_str(r#"
"#); + } else { + o.push_str(r#"
"#); + } + } else { + o.push_str("
"); + } html_escape::encode_safe_to_string(destination, &mut o); + o.push_str(""); } o.push_str("
\n"); } @@ -333,25 +394,37 @@ pub struct JourneyData { #[derive(Deserialize)] struct BahnhofResDepartures { - entries: Vec>, + #[serde(rename = "entries")] + pub 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, + #[serde(rename = "zeit")] + pub time_schedule: Option, + #[serde(rename = "ezZeit")] + pub time_delayed: Option, + #[serde(rename = "ezGleis")] + pub platform: Option, + #[serde(rename = "gleis")] + pub platform_schedule: Option, + #[serde(rename = "verkehrmittel")] + pub line_name: Option, + #[serde(rename = "terminus")] + pub destination: Option, + #[serde(rename = "meldungen")] + pub messages: Option>, } #[derive(Deserialize)] -struct BahnhofResDepartureDestination { - #[serde(default, rename = "name")] - name: Option, +pub struct BahnhofResDepartureDestination { + #[serde(rename = "linienNummer")] + pub line_number: Option, + #[serde(rename = "kurzText")] + pub product: Option, +} +#[derive(Deserialize)] +pub struct BahnhofResDepartureMessage { + #[serde(rename = "text")] + pub text: Option, + #[serde(rename = "prioritaet")] + pub priority: Option, } diff --git a/src/index.html b/src/index.html index 580b6e2..6da9a05 100644 --- a/src/index.html +++ b/src/index.html @@ -20,6 +20,19 @@ margin-right: 1em; border-left: solid gray; } +.affected_departure > .affected_departure_messages { + pointer-events: none; + display: none; +} +.affected_departure:hover > .affected_departure_messages { + pointer-events: none; + display: block; + position: fixed; + inset: 5%; + padding-left: 1em; + padding-right: 1em; + background: #000C; +}