use bahn.de api

This commit is contained in:
Mark 2025-07-18 11:13:44 +02:00
parent 3d3754b116
commit e2fcae8e8e
2 changed files with 122 additions and 36 deletions

View File

@ -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<String, String> {
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::<BahnhofResDepartures>(&response) {
Ok(response) => {
let mut o = format!("<div class=\"dbflex\" style=\"display:flex;flex-wrap:wrap;padding:1%;\">\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#"<div style="text-decoration: line-through wavy DarkRed; color:gray;">"#
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#"<div class="affected_departure"><div class="affected_departure_messages">"#);
} else {
o.push_str(r#"<div class="affected_departure"><div class="affected_departure_messages">"#);
}
for message in messages.iter() {
o.push_str(
match message.priority.as_ref().map(|v| v.as_str()) {
Some("HOCH") => r#"<p style="color:#ED153D;max-width:100%;text-wrap:wrap;font-size:large;">"#,
Some("NORMAL") => r#"<p style="color:white;max-width:100%;text-wrap:wrap;font-size:large;">"#,
Some("NIEDRIG") => r#"<p style="color:gray;max-width:100%;text-wrap:wrap;font-size:large;">"#,
None | Some(_) => r#"<p style="color:#CA5CE1;max-width:100%;text-wrap:wrap;font-size:large;">"#,
},
);
if let Some(text) = &message.text {
html_escape::encode_safe_to_string(text, &mut o);
}
o.push_str("</p>");
}
o.push_str(r#"</div>"#);
} else {
r#"<div>"#
});
o.push_str("<div>");
}
o.push_str("<b>");
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("</b>");
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("<small> ");
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("<small>");
o.push_str(&format!(" @ {platform}"));
o.push_str("</small></small>");
} else {
o.push_str("</small>");
}
} else if let Some(platform) = departure
.platform
.as_ref()
.or(departure.platform_schedule.as_ref())
{
o.push_str("<br>");
o.push_str("<small><small>");
o.push_str(&format!(" @ {platform}"));
o.push_str("</small></small>");
}
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#"<br><span style="text-decoration: underline darkred;">"#);
} else {
o.push_str(r#"<br><span style="text-decoration: underline dotted #908070;">"#);
}
} else {
o.push_str("<br><span>");
}
html_escape::encode_safe_to_string(destination, &mut o);
o.push_str("</span>");
}
o.push_str("</div>\n");
}
@ -333,25 +394,37 @@ pub struct JourneyData {
#[derive(Deserialize)]
struct BahnhofResDepartures {
entries: Vec<Vec<BahnhofResDeparture>>,
#[serde(rename = "entries")]
pub entries: Vec<BahnhofResDeparture>,
}
#[derive(Deserialize)]
struct BahnhofResDeparture {
#[serde(default, rename = "timeSchedule")]
time_schedule: Option<DateTime<chrono::Local>>,
#[serde(default, rename = "timeDelayed")]
time_delayed: Option<DateTime<chrono::Local>>,
#[serde(default, rename = "canceled")]
canceled: bool,
// #[serde(default, rename = "platform")]
// platform: Option<String>,
#[serde(default, rename = "lineName")]
line_name: Option<String>,
#[serde(default, rename = "destination")]
destination: Option<BahnhofResDepartureDestination>,
#[serde(rename = "zeit")]
pub time_schedule: Option<NaiveDateTime>,
#[serde(rename = "ezZeit")]
pub time_delayed: Option<NaiveDateTime>,
#[serde(rename = "ezGleis")]
pub platform: Option<String>,
#[serde(rename = "gleis")]
pub platform_schedule: Option<String>,
#[serde(rename = "verkehrmittel")]
pub line_name: Option<BahnhofResDepartureDestination>,
#[serde(rename = "terminus")]
pub destination: Option<String>,
#[serde(rename = "meldungen")]
pub messages: Option<Vec<BahnhofResDepartureMessage>>,
}
#[derive(Deserialize)]
struct BahnhofResDepartureDestination {
#[serde(default, rename = "name")]
name: Option<String>,
pub struct BahnhofResDepartureDestination {
#[serde(rename = "linienNummer")]
pub line_number: Option<String>,
#[serde(rename = "kurzText")]
pub product: Option<String>,
}
#[derive(Deserialize)]
pub struct BahnhofResDepartureMessage {
#[serde(rename = "text")]
pub text: Option<String>,
#[serde(rename = "prioritaet")]
pub priority: Option<String>,
}

View File

@ -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;
}
</style>
</head>
<body>