switch to using bahn.de api instead of bahnhof.de
This commit is contained in:
parent
8f9c90a689
commit
1d4ba20039
@ -1,33 +1,40 @@
|
|||||||
use std::collections::HashMap;
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
use chrono::{DateTime, Local};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DeparturesMerklingen {
|
pub struct DeparturesMerklingen {
|
||||||
#[serde(rename = "entries")]
|
#[serde(rename = "entries")]
|
||||||
pub entries: Vec<Vec<DeparturesMerklingen1>>,
|
pub entries: Vec<DeparturesMerklingen1>,
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DeparturesMerklingen1 {
|
pub struct DeparturesMerklingen1 {
|
||||||
#[serde(rename = "timeSchedule")]
|
#[serde(rename = "zeit")]
|
||||||
pub time_schedule: Option<DateTime<Local>>,
|
pub time_schedule: Option<NaiveDateTime>,
|
||||||
#[serde(rename = "timeDelayed")]
|
#[serde(rename = "ezZeit")]
|
||||||
pub time_delayed: Option<DateTime<Local>>,
|
pub time_delayed: Option<NaiveDateTime>,
|
||||||
#[serde(rename = "platform")]
|
#[serde(rename = "ezGleis")]
|
||||||
pub platform: Option<String>,
|
pub platform: Option<String>,
|
||||||
#[serde(rename = "platformSchedule")]
|
#[serde(rename = "gleis")]
|
||||||
pub platform_schedule: Option<String>,
|
pub platform_schedule: Option<String>,
|
||||||
|
// TODO: find this in api if it exists
|
||||||
#[serde(rename = "canceled")]
|
#[serde(rename = "canceled")]
|
||||||
pub canceled: Option<bool>,
|
pub canceled: Option<bool>,
|
||||||
#[serde(rename = "lineName")]
|
#[serde(rename = "verkehrmittel")]
|
||||||
pub line_name: Option<String>,
|
pub line_name: Option<DeparturesMerklingen4>,
|
||||||
|
// TODO: find this in api if it exists
|
||||||
#[serde(rename = "stopPlace")]
|
#[serde(rename = "stopPlace")]
|
||||||
pub stop_place: Option<DeparturesMerklingen2>,
|
pub stop_place: Option<DeparturesMerklingen2>,
|
||||||
#[serde(rename = "destination")]
|
#[serde(rename = "terminus")]
|
||||||
pub destination: Option<DeparturesMerklingen2>,
|
pub destination: Option<String>,
|
||||||
#[serde(rename = "messages")]
|
#[serde(rename = "meldungen")]
|
||||||
pub messages: Option<HashMap<String, Vec<DeparturesMerklingen3>>>,
|
pub messages: Option<Vec<DeparturesMerklingen3>>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct DeparturesMerklingen4 {
|
||||||
|
#[serde(rename = "linienNummer")]
|
||||||
|
pub line_number: Option<String>,
|
||||||
|
#[serde(rename = "kurzText")]
|
||||||
|
pub product: Option<String>,
|
||||||
}
|
}
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct DeparturesMerklingen2 {
|
pub struct DeparturesMerklingen2 {
|
||||||
@ -40,22 +47,20 @@ pub struct DeparturesMerklingen2 {
|
|||||||
pub struct DeparturesMerklingen3 {
|
pub struct DeparturesMerklingen3 {
|
||||||
#[serde(rename = "text")]
|
#[serde(rename = "text")]
|
||||||
pub text: Option<String>,
|
pub text: Option<String>,
|
||||||
#[serde(rename = "important")]
|
#[serde(rename = "prioritaet")]
|
||||||
pub important: Option<bool>,
|
pub priority: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn departures_merklingen() -> Result<DeparturesMerklingen, String> {
|
pub async fn departures_merklingen() -> Result<DeparturesMerklingen, String> {
|
||||||
let url = format!(
|
let url = r#"https://www.bahn.de/web/api/reiseloesung/abfahrten?ortExtId=8003983&mitVias=true&maxVias=8&verkehrsmittel[]=ICE&verkehrsmittel[]=EC_IC&verkehrsmittel[]=IR&verkehrsmittel[]=REGIONAL&verkehrsmittel[]=SBAHN"#;
|
||||||
r#"https://www.bahnhof.de/api/boards/departures?evaNumbers=8003983&filterTransports=REGIONAL_TRAIN&duration=120&stationCategory=5&locale=de&sortBy=TIME_SCHEDULE"#
|
match reqwest::get(url).await {
|
||||||
);
|
|
||||||
match reqwest::get(&url).await {
|
|
||||||
Ok(response) => match response.text().await {
|
Ok(response) => match response.text().await {
|
||||||
Ok(response) => {
|
Ok(response) => match serde_json::from_str::<DeparturesMerklingen>(&response) {
|
||||||
match serde_json::from_str::<DeparturesMerklingen>(&response) {
|
Ok(response) => Ok(response),
|
||||||
Ok(response) => Ok(response),
|
Err(e) => Err(format!(
|
||||||
Err(e) => Err(format!("Couldn't parse HTTP response from URL {url:?}: {e}\nResponse was (raw):\n{response}"))?,
|
"Couldn't parse HTTP response from URL {url:?}: {e}\nResponse was (raw):\n{response}"
|
||||||
}
|
))?,
|
||||||
}
|
},
|
||||||
Err(e) => Err(format!("Couldn't get HTTP response from URL {url:?}: {e}"))?,
|
Err(e) => Err(format!("Couldn't get HTTP response from URL {url:?}: {e}"))?,
|
||||||
},
|
},
|
||||||
Err(e) => Err(format!("Couldn't make GET request to URL {url:?}: {e}"))?,
|
Err(e) => Err(format!("Couldn't make GET request to URL {url:?}: {e}"))?,
|
||||||
|
@ -36,7 +36,7 @@ async function con(change) {
|
|||||||
nmv('oberdrackenstein_rathaus', 26)
|
nmv('oberdrackenstein_rathaus', 26)
|
||||||
break;
|
break;
|
||||||
case "967UDrackensteinKirche":
|
case "967UDrackensteinKirche":
|
||||||
nmv('unterdrackenstain_kirche', 32)
|
nmv('unterdrackenstein_kirche', 32)
|
||||||
break;
|
break;
|
||||||
case "967GosbachEinkaufszentrum":
|
case "967GosbachEinkaufszentrum":
|
||||||
nmv('gosbach_einkaufszentrum', 37)
|
nmv('gosbach_einkaufszentrum', 37)
|
||||||
@ -72,7 +72,7 @@ async function con(change) {
|
|||||||
<button id="hohenstadt_kirche" onclick="con('967HohenstadtKirche')">Hohenstadt<small> Kirche</small></button> /
|
<button id="hohenstadt_kirche" onclick="con('967HohenstadtKirche')">Hohenstadt<small> Kirche</small></button> /
|
||||||
<button id="hohenstadt_waltertal" onclick="con('967HohenstadtWaltertal')">Hohenstadt<small> Abzw. Waltertal</small></button> /
|
<button id="hohenstadt_waltertal" onclick="con('967HohenstadtWaltertal')">Hohenstadt<small> Abzw. Waltertal</small></button> /
|
||||||
<button id="oberdrackenstein_rathaus" onclick="con('967ODrackensteinRathaus')">Oberdrackenstein<small> Rathaus</small></button> /
|
<button id="oberdrackenstein_rathaus" onclick="con('967ODrackensteinRathaus')">Oberdrackenstein<small> Rathaus</small></button> /
|
||||||
<button id="unterdrackenstain_kirche" onclick="con('967UDrackensteinKirche')">Unterdrackenstein<small> Kirche</small></button> /
|
<button id="unterdrackenstein_kirche" onclick="con('967UDrackensteinKirche')">Unterdrackenstein<small> Kirche</small></button> /
|
||||||
<button id="gosbach_einkaufszentrum" onclick="con('967GosbachEinkaufszentrum')">Gosbach<small> Einkaufszentrum</small></button> /
|
<button id="gosbach_einkaufszentrum" onclick="con('967GosbachEinkaufszentrum')">Gosbach<small> Einkaufszentrum</small></button> /
|
||||||
<button id="gosbach_lamm" onclick="con('967GosbachLamm')">Gosbach<small> Lamm</small></button> /
|
<button id="gosbach_lamm" onclick="con('967GosbachLamm')">Gosbach<small> Lamm</small></button> /
|
||||||
<button id="gosbach_abzw_drackenstein" onclick="con('967GosbachDrackenstein')">Gosbach<small> Abzw. Drackenstein</small></button> /
|
<button id="gosbach_abzw_drackenstein" onclick="con('967GosbachDrackenstein')">Gosbach<small> Abzw. Drackenstein</small></button> /
|
||||||
@ -90,7 +90,7 @@ async function con(change) {
|
|||||||
Diese Seite soll eine einfache Möglichkeit darstellen, zu prüfen,
|
Diese Seite soll eine einfache Möglichkeit darstellen, zu prüfen,
|
||||||
ob der Bus und Zug von/nach Merklingen heute kommt, oder ob er mal wieder streikt oder aus sonstigen Gründen fehlt.<br>
|
ob der Bus und Zug von/nach Merklingen heute kommt, oder ob er mal wieder streikt oder aus sonstigen Gründen fehlt.<br>
|
||||||
Das ganze sollte aber auch für andere Busse funktionieren, sofern der VVS den Bus kennt (→ <a href="https://www.vvs.de/">vvs.de</a>).<br>
|
Das ganze sollte aber auch für andere Busse funktionieren, sofern der VVS den Bus kennt (→ <a href="https://www.vvs.de/">vvs.de</a>).<br>
|
||||||
Zug-Informationen kommen von <a href="https://www.bahnhof.de/merklingen-schwaebische-alb/abfahrt">bahnhof.de</a>.
|
Zug-Informationen kommen von <a href="https://www.bahn.de/buchung/abfahrten-ankuenfte">bahn.de</a>.
|
||||||
</div>
|
</div>
|
||||||
<div><a href="./api">API documentation</a></div>
|
<div><a href="./api">API documentation</a></div>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
281
src/main.rs
281
src/main.rs
@ -14,7 +14,7 @@ use tokio::{
|
|||||||
time::{Instant, sleep},
|
time::{Instant, sleep},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::vvs::VvsStopIdentifier;
|
use crate::{bahnhof::DeparturesMerklingen3, vvs::VvsStopIdentifier};
|
||||||
|
|
||||||
#[rocket::launch]
|
#[rocket::launch]
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
@ -103,164 +103,169 @@ async fn index(
|
|||||||
let mut table_rows = BTreeMap::<_, (String, String, usize, usize, bool)>::new();
|
let mut table_rows = BTreeMap::<_, (String, String, usize, usize, bool)>::new();
|
||||||
for departure in &departures.entries {
|
for departure in &departures.entries {
|
||||||
let mut messages = vec![];
|
let mut messages = vec![];
|
||||||
for departure in departure.iter() {
|
if let Some(message) = &departure.messages {
|
||||||
if let Some(message) = &departure.messages {
|
for DeparturesMerklingen3 { priority, text } in message {
|
||||||
for (_message_type, message) in message {
|
if let Some(text) = &text {
|
||||||
for message in message {
|
if priority
|
||||||
if let Some(text) = message.text.clone() {
|
.as_ref()
|
||||||
if message.important.is_some_and(|v| v) {
|
.is_some_and(|v| ["HOCH"].contains(&v.to_uppercase().as_str()))
|
||||||
if !messages.contains(&text) {
|
{
|
||||||
messages.push(text);
|
if !messages.contains(text) {
|
||||||
}
|
messages.push(text.to_owned());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(departure) = departure.first() {
|
if let Some(time) = departure.time_delayed.or(departure.time_schedule) {
|
||||||
if let Some(time) = departure.time_delayed.or(departure.time_schedule) {
|
let row = table_rows
|
||||||
let row = table_rows
|
.entry((time.date(), time.hour(), time.minute() >= 30))
|
||||||
.entry((time.date_naive(), time.hour(), time.minute() >= 30))
|
.or_default();
|
||||||
.or_default();
|
let line_name = departure
|
||||||
let line_name = departure
|
.line_name
|
||||||
.line_name
|
.as_ref()
|
||||||
.as_ref()
|
.map(|v| {
|
||||||
.map(|v| v.as_str())
|
v.product.as_ref().map(|prod| {
|
||||||
.unwrap_or("[RE?]");
|
format!(
|
||||||
let is_ulm = {
|
"{prod} {}",
|
||||||
if let Some(destination) = departure.destination.as_ref() {
|
v.line_number.as_ref().map(|v| v.as_str()).unwrap_or("?")
|
||||||
if let Some(name) = &destination.name {
|
)
|
||||||
let name = name.to_lowercase();
|
})
|
||||||
if name.starts_with("ulm") || name.contains(" ulm") {
|
})
|
||||||
Some(true)
|
.flatten()
|
||||||
} else if name.contains("wendlingen")
|
.unwrap_or_else(|| "[RE ?]".to_owned());
|
||||||
|| name.contains("plochingen")
|
let is_ulm = {
|
||||||
|| name.contains("stuttgart")
|
if let Some(destination) = departure.destination.as_ref() {
|
||||||
{
|
let name = destination.to_lowercase();
|
||||||
Some(false)
|
if name.starts_with("ulm") || name.contains(" ulm") {
|
||||||
} else {
|
Some(true)
|
||||||
None
|
} else if name.contains("wendlingen")
|
||||||
}
|
|| name.contains("plochingen")
|
||||||
} else {
|
|| name.contains("stuttgart")
|
||||||
None
|
{
|
||||||
}
|
Some(false)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
.or_else(|| {
|
}
|
||||||
match departure.platform_schedule.as_ref().map(|v| v.as_str()) {
|
.or_else(|| {
|
||||||
Some("1") => Some(true),
|
match departure.platform_schedule.as_ref().map(|v| v.as_str()) {
|
||||||
Some("4") => Some(false),
|
Some("1") => Some(true),
|
||||||
_ => None,
|
Some("4") => Some(false),
|
||||||
}
|
_ => None,
|
||||||
});
|
}
|
||||||
let mut dep_str = "".to_owned();
|
});
|
||||||
dep_str.push_str(html_escape::encode_safe(line_name).as_ref());
|
let mut dep_str = "".to_owned();
|
||||||
let platform = departure
|
dep_str.push_str(html_escape::encode_safe(&line_name).as_ref());
|
||||||
.platform
|
let platform = departure
|
||||||
.as_ref()
|
.platform
|
||||||
.map(|v| v.as_str())
|
.as_ref()
|
||||||
.or(departure.platform_schedule.as_ref().map(|v| v.as_str()));
|
.map(|v| v.as_str())
|
||||||
dep_str.push_str(&format!(
|
.or(departure.platform_schedule.as_ref().map(|v| v.as_str()));
|
||||||
" <small{}>{:0>2}:{:0>2}</small>{}{}",
|
dep_str.push_str(&format!(
|
||||||
if let Some(schedule) = &departure.time_schedule {
|
" <small{}>{:0>2}:{:0>2}</small>{}{}",
|
||||||
let delay = ({
|
if let Some(schedule) = &departure.time_schedule {
|
||||||
if time > *schedule {
|
let delay = ({
|
||||||
time - *schedule
|
if time > *schedule {
|
||||||
} else {
|
time - *schedule
|
||||||
TimeDelta::zero()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.num_minutes() as f64
|
|
||||||
/ 15.0)
|
|
||||||
.max(0.0)
|
|
||||||
.min(1.0);
|
|
||||||
format!(
|
|
||||||
r#" style="color: hsl({}, 60%, 60%)""#,
|
|
||||||
(120u8.saturating_sub((120.0 * delay) as u8))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
},
|
|
||||||
time.hour(),
|
|
||||||
time.minute(),
|
|
||||||
if let Some(platform) = platform {
|
|
||||||
if platform.trim().is_empty() {
|
|
||||||
""
|
|
||||||
} else if departure
|
|
||||||
.platform_schedule
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|scheduled| platform != scheduled)
|
|
||||||
{
|
|
||||||
r#"<small style="color:darkorange;"> auf Gleis "#
|
|
||||||
} else {
|
} else {
|
||||||
r#"<small style="color:gray"> auf Gleis "#
|
TimeDelta::zero()
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
.num_minutes() as f64
|
||||||
|
/ 15.0)
|
||||||
|
.max(0.0)
|
||||||
|
.min(1.0);
|
||||||
|
format!(
|
||||||
|
r#" style="color: hsl({}, 60%, 60%)""#,
|
||||||
|
(120u8.saturating_sub((120.0 * delay) as u8))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
},
|
||||||
|
time.hour(),
|
||||||
|
time.minute(),
|
||||||
|
if let Some(platform) = platform {
|
||||||
|
if platform.trim().is_empty() {
|
||||||
""
|
""
|
||||||
},
|
} else if departure
|
||||||
if let Some(platform) = platform {
|
.platform_schedule
|
||||||
if platform.trim().is_empty() {
|
.as_ref()
|
||||||
String::new()
|
.is_some_and(|scheduled| platform != scheduled)
|
||||||
} else {
|
{
|
||||||
format!("{platform}</small>")
|
r#"<small style="color:darkorange;"> auf Gleis "#
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
r#"<small style="color:gray"> auf Gleis "#
|
||||||
},
|
|
||||||
));
|
|
||||||
if let Some(dest) = &departure.destination {
|
|
||||||
if let Some(name) = &dest.name {
|
|
||||||
dep_str.push_str(r#"<br><span style="color:gray;"> → </span>"#);
|
|
||||||
dep_str.push_str(html_escape::encode_safe(name).as_ref());
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
|
if let Some(platform) = platform {
|
||||||
|
if platform.trim().is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!("{platform}</small>")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
if let Some(dest) = &departure.destination {
|
||||||
|
dep_str.push_str(r#"<br><span style="color:gray;"> → </span>"#);
|
||||||
|
dep_str.push_str(html_escape::encode_safe(dest).as_ref());
|
||||||
|
}
|
||||||
|
if departure.canceled.is_some_and(|canceled| canceled) {
|
||||||
|
dep_str.push_str(
|
||||||
|
r#"<div style="color:darkorange;"> ▸ fällt aus / cancelled</div>"#,
|
||||||
|
);
|
||||||
|
} else if let Some(stop_place) = departure
|
||||||
|
.stop_place
|
||||||
|
.as_ref()
|
||||||
|
.filter(|stop_place| stop_place.canceled.is_some_and(|canceled| canceled))
|
||||||
|
{
|
||||||
|
dep_str.push_str(r#"<div style="color:darkorange;"> ▸ stop/halt "#);
|
||||||
|
html_escape::encode_safe_to_string(
|
||||||
|
stop_place
|
||||||
|
.name
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or("[stopPlace.name]"),
|
||||||
|
&mut dep_str,
|
||||||
|
);
|
||||||
|
dep_str.push_str(": fällt aus / cancelled</div>");
|
||||||
|
}
|
||||||
|
for message in &messages {
|
||||||
|
dep_str.push_str(r#"<div style="color:darkorange;"> ▹ "#);
|
||||||
|
html_escape::encode_safe_to_string(message, &mut dep_str);
|
||||||
|
dep_str.push_str("</div>");
|
||||||
|
}
|
||||||
|
row.4 = is_ulm.is_some();
|
||||||
|
if !row.0.is_empty() {
|
||||||
|
row.0.push_str("<br>");
|
||||||
|
}
|
||||||
|
if !row.1.is_empty() {
|
||||||
|
row.1.push_str("<br>");
|
||||||
|
}
|
||||||
|
match is_ulm {
|
||||||
|
Some(true) => {
|
||||||
|
row.0.push_str(&dep_str);
|
||||||
|
row.2 += 1;
|
||||||
}
|
}
|
||||||
if departure.canceled.is_some_and(|canceled| canceled) {
|
Some(false) => {
|
||||||
dep_str.push_str(
|
row.1.push_str(&dep_str);
|
||||||
r#"<div style="color:darkorange;"> ▸ fällt aus / cancelled</div>"#,
|
row.3 += 1;
|
||||||
);
|
|
||||||
} else if let Some(stop_place) =
|
|
||||||
departure.stop_place.as_ref().filter(|stop_place| {
|
|
||||||
stop_place.canceled.is_some_and(|canceled| canceled)
|
|
||||||
})
|
|
||||||
{
|
|
||||||
dep_str.push_str(r#"<div style="color:darkorange;"> ▸ stop/halt "#);
|
|
||||||
html_escape::encode_safe_to_string(
|
|
||||||
stop_place
|
|
||||||
.name
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.as_str())
|
|
||||||
.unwrap_or("[stopPlace.name]"),
|
|
||||||
&mut dep_str,
|
|
||||||
);
|
|
||||||
dep_str.push_str(": fällt aus / cancelled</div>");
|
|
||||||
}
|
}
|
||||||
for message in &messages {
|
None => {
|
||||||
dep_str.push_str(r#"<div style="color:darkorange;"> ▹ "#);
|
if row.2 <= row.3 {
|
||||||
html_escape::encode_safe_to_string(message, &mut dep_str);
|
|
||||||
dep_str.push_str("</div>");
|
|
||||||
}
|
|
||||||
row.4 = is_ulm.is_some();
|
|
||||||
match is_ulm {
|
|
||||||
Some(true) => {
|
|
||||||
row.0.push_str(&dep_str);
|
row.0.push_str(&dep_str);
|
||||||
row.2 += 1;
|
row.2 += 1;
|
||||||
}
|
} else {
|
||||||
Some(false) => {
|
|
||||||
row.1.push_str(&dep_str);
|
row.1.push_str(&dep_str);
|
||||||
row.3 += 1;
|
row.3 += 1;
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
if row.2 <= row.3 {
|
|
||||||
row.0.push_str(&dep_str);
|
|
||||||
row.2 += 1;
|
|
||||||
} else {
|
|
||||||
row.1.push_str(&dep_str);
|
|
||||||
row.3 += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user