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}, time::{Duration, Instant},
}; };
use chrono::DateTime; use chrono::{DateTime, NaiveDateTime};
use reqwest::get; use reqwest::get;
use serde::Deserialize; use serde::Deserialize;
@ -157,38 +157,99 @@ impl ApiClient {
} }
pub async fn bahnhof_get_departures() -> Result<String, String> { pub async fn bahnhof_get_departures() -> Result<String, String> {
match get( 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"#;
"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", match get(url).await {
)
.await
{
Ok(response) => match response.text().await { Ok(response) => match response.text().await {
Ok(response) => match serde_json::from_str::<BahnhofResDepartures>(&response) { Ok(response) => match serde_json::from_str::<BahnhofResDepartures>(&response) {
Ok(response) => { Ok(response) => {
let mut o = format!("<div class=\"dbflex\" style=\"display:flex;flex-wrap:wrap;padding:1%;\">\n"); 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 { if let Some(line_name) = &departure.line_name {
o.push_str(if departure.canceled { if let Some(messages) =
r#"<div style="text-decoration: line-through wavy DarkRed; color:gray;">"# 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 { } else {
r#"<div>"# o.push_str("<div>");
}); }
o.push_str("<b>"); 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>"); o.push_str("</b>");
if let Some(departure_time) = if let Some(departure_time) =
departure.time_delayed.or(departure.time_schedule) departure.time_delayed.or(departure.time_schedule)
{ {
o.push_str(" "); o.push_str("<small> ");
o.push_str( o.push_str(&departure_time.format("%H:%M").to_string());
&departure_time.naive_local().format("%H:%M").to_string(), if let Some(platform) = departure
); .platform
} .as_ref()
if let Some(destination) = .or(departure.platform_schedule.as_ref())
departure.destination.as_ref().and_then(|v| v.name.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); html_escape::encode_safe_to_string(destination, &mut o);
o.push_str("</span>");
} }
o.push_str("</div>\n"); o.push_str("</div>\n");
} }
@ -333,25 +394,37 @@ pub struct JourneyData {
#[derive(Deserialize)] #[derive(Deserialize)]
struct BahnhofResDepartures { struct BahnhofResDepartures {
entries: Vec<Vec<BahnhofResDeparture>>, #[serde(rename = "entries")]
pub entries: Vec<BahnhofResDeparture>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct BahnhofResDeparture { struct BahnhofResDeparture {
#[serde(default, rename = "timeSchedule")] #[serde(rename = "zeit")]
time_schedule: Option<DateTime<chrono::Local>>, pub time_schedule: Option<NaiveDateTime>,
#[serde(default, rename = "timeDelayed")] #[serde(rename = "ezZeit")]
time_delayed: Option<DateTime<chrono::Local>>, pub time_delayed: Option<NaiveDateTime>,
#[serde(default, rename = "canceled")] #[serde(rename = "ezGleis")]
canceled: bool, pub platform: Option<String>,
// #[serde(default, rename = "platform")] #[serde(rename = "gleis")]
// platform: Option<String>, pub platform_schedule: Option<String>,
#[serde(default, rename = "lineName")] #[serde(rename = "verkehrmittel")]
line_name: Option<String>, pub line_name: Option<BahnhofResDepartureDestination>,
#[serde(default, rename = "destination")] #[serde(rename = "terminus")]
destination: Option<BahnhofResDepartureDestination>, pub destination: Option<String>,
#[serde(rename = "meldungen")]
pub messages: Option<Vec<BahnhofResDepartureMessage>>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct BahnhofResDepartureDestination { pub struct BahnhofResDepartureDestination {
#[serde(default, rename = "name")] #[serde(rename = "linienNummer")]
name: Option<String>, 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; margin-right: 1em;
border-left: solid gray; 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> </style>
</head> </head>
<body> <body>