308 lines
10 KiB
Rust
308 lines
10 KiB
Rust
use axum::{
|
|
extract::{
|
|
State,
|
|
Path,
|
|
Query,
|
|
},
|
|
http::StatusCode,
|
|
response::Html,
|
|
routing::get,
|
|
Router,
|
|
};
|
|
|
|
use serde::Deserialize;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Deserialize)]
|
|
struct Train {
|
|
name: String,
|
|
number: String,
|
|
//line: String,
|
|
r#type: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Stop {
|
|
name: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct DepartureInfo {
|
|
time: String,
|
|
platform: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Departure {
|
|
departure: Option<DepartureInfo>,
|
|
#[serde(rename = "initialDeparture")]
|
|
initial_departure: String,
|
|
route: Vec<Stop>,
|
|
train: Train,
|
|
destination: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Departures {
|
|
departures: Vec<Departure>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct Station {
|
|
name: String,
|
|
code: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
stations: HashMap<String, Station>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct RoutingQuery {
|
|
all: Option<String>,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
let stations = HashMap::from([
|
|
(String::from("berlin-hbf"), Station {
|
|
name: String::from("Berlin Hbf"),
|
|
code: String::from("8011160"),
|
|
}),
|
|
(String::from("berlin-ostbahnhof"), Station {
|
|
name: String::from("Berlin Ostbahnhof"),
|
|
code: String::from("8010255"),
|
|
}),
|
|
(String::from("berlin-spandau"), Station {
|
|
name: String::from("Berlin-Spandau"),
|
|
code: String::from("8010404"),
|
|
}),
|
|
(String::from("berlin-suedkreuz"), Station {
|
|
name: String::from("Berlin Südkreuz"),
|
|
code: String::from("8011113"),
|
|
}),
|
|
(String::from("berlin-gesundbrunnen"), Station {
|
|
name: String::from("Berlin Gesundbrunnen"),
|
|
code: String::from("8011102"),
|
|
}),
|
|
]);
|
|
|
|
|
|
let mut args = std::env::args();
|
|
let _name = args.next().unwrap();
|
|
|
|
let mut listen = String::from("[::]:3000");
|
|
|
|
loop {
|
|
let arg = if let Some(arg) = args.next() {
|
|
arg
|
|
} else {
|
|
break;
|
|
};
|
|
|
|
match arg.as_str() {
|
|
"--listen" => {
|
|
listen = args.next().unwrap();
|
|
}
|
|
unknown => {
|
|
println!("unknown option: {}", unknown);
|
|
std::process::exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
let state = AppState {
|
|
stations,
|
|
};
|
|
|
|
let app = Router::new()
|
|
.route("/", get(route_index))
|
|
.route("/station/:station_id", get(route_station_overview))
|
|
.route("/station/:station_id/to/:dest_station_id", get(route_station_to_dest_station))
|
|
.with_state(state);
|
|
|
|
let listener = tokio::net::TcpListener::bind(listen).await.unwrap();
|
|
println!("Server listening on: http://{}", listener.local_addr().unwrap());
|
|
|
|
axum::serve(listener, app).await.unwrap();
|
|
}
|
|
|
|
async fn route_index(
|
|
State(state): State<AppState>,
|
|
) -> Result<Html<String>, String> {
|
|
let mut out = String::new();
|
|
|
|
out.push_str("<html>\n");
|
|
out.push_str("<header><title>Nur Ausstieg</title></header>\n");
|
|
out.push_str("<body>\n");
|
|
out.push_str("<h1>Nur ausstieg</h1>\n");
|
|
out.push_str("<p>Es gibt Züge die innerhalb Berlins fahren, aber nicht im Fahrplan Routing der DB auftauchen, weil sie entweder in Berlin enden oder in Berlin beginnen.<br>\n");
|
|
out.push_str("Diese Seite zeigt für verschiene Stationen genau diese Züge an.</p>\n");
|
|
out.push_str("<ul>\n");
|
|
|
|
for (id, station) in state.stations.into_iter() {
|
|
out.push_str(&format!("<li><a href=\"/station/{}\">{}</a></li>\n", id, station.name));
|
|
}
|
|
|
|
out.push_str("</ul>\n");
|
|
out.push_str("<p><small><a href=\"/\">Nur Ausstieg</a> | <a href=\"https://git.clerie.de/clerie/nurausstieg\">Source Code</a></small></p>\n");
|
|
out.push_str("</body></html>\n");
|
|
|
|
return Ok(Html(out));
|
|
}
|
|
|
|
async fn route_station_overview(
|
|
State(state): State<AppState>,
|
|
Path(station_id): Path<String>
|
|
) -> Result<Html<String>, (StatusCode, String)> {
|
|
if !state.stations.contains_key(&station_id) {
|
|
return Err((StatusCode::NOT_FOUND, String::from("Unknown departing station")));
|
|
}
|
|
|
|
let station_properties = match state.stations.get(&station_id) {
|
|
Some(v) => v,
|
|
None => return Err((StatusCode::INTERNAL_SERVER_ERROR, String::from("Station properties for departing station missing"))),
|
|
};
|
|
|
|
let mut out = String::new();
|
|
|
|
out.push_str("<html>\n");
|
|
out.push_str(&format!("<header><title>Ziele für {} | Nur Ausstieg</title></header>\n", station_properties.name));
|
|
out.push_str("<body>\n");
|
|
out.push_str(&format!("<h1>Ziele für {}</h1>\n", station_properties.name));
|
|
out.push_str("<ul>\n");
|
|
|
|
for (dest_station_id, dest_station) in state.stations.into_iter() {
|
|
if dest_station_id == station_id {
|
|
continue;
|
|
}
|
|
|
|
out.push_str(&format!("<li><a href=\"/station/{}/to/{}\">{}</a></li>\n", station_id, dest_station_id, dest_station.name));
|
|
}
|
|
|
|
out.push_str("</ul>\n");
|
|
out.push_str("<p><small><a href=\"/\">Nur Ausstieg</a> | <a href=\"https://git.clerie.de/clerie/nurausstieg\">Source Code</a></small></p>\n");
|
|
out.push_str("</body></html>\n");
|
|
|
|
return Ok(Html(out));
|
|
}
|
|
|
|
async fn route_station_to_dest_station(
|
|
State(state): State<AppState>,
|
|
Path((station_id, dest_station_id)): Path<(String, String)>,
|
|
query: Query<RoutingQuery>,
|
|
) -> Result<Html<String>, (StatusCode, String)> {
|
|
if !state.stations.contains_key(&station_id) {
|
|
return Err((StatusCode::NOT_FOUND, String::from("Unknown departing station")));
|
|
}
|
|
|
|
if !state.stations.contains_key(&dest_station_id) {
|
|
return Err((StatusCode::NOT_FOUND, String::from("Unknown destination station")));
|
|
}
|
|
|
|
if dest_station_id == station_id {
|
|
return Err((StatusCode::NOT_FOUND, String::from("Cannot route to departing station as destination")));
|
|
}
|
|
|
|
let station_properties = match state.stations.get(&station_id) {
|
|
Some(v) => v,
|
|
None => return Err((StatusCode::INTERNAL_SERVER_ERROR, String::from("Station properties for departing station missing"))),
|
|
};
|
|
|
|
let dest_station_properties = match state.stations.get(&dest_station_id) {
|
|
Some(v) => v,
|
|
None => return Err((StatusCode::INTERNAL_SERVER_ERROR, String::from("Station properties for destination station missing"))),
|
|
};
|
|
|
|
|
|
let client = reqwest::Client::new();
|
|
let stationdata_req = client.get(format!("https://bahn.expert/api/iris/v2/abfahrten/{}?lookahead=150&lookbehind=10", station_properties.code))
|
|
.header(reqwest::header::USER_AGENT, "https://nurausstieg.clerie.de - abuse: nurausstieg@clerie.de")
|
|
.send().await
|
|
.map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, String::from("Station Data cannot be fetched")))?;
|
|
|
|
if stationdata_req.status() != reqwest::StatusCode::OK {
|
|
return Err((StatusCode::INTERNAL_SERVER_ERROR, String::from("Station Data returned an unexpected response")));
|
|
}
|
|
|
|
let stationdata_body = stationdata_req.text().await
|
|
.map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, String::from("Station Data cannot be read")))?;
|
|
|
|
let stationdata: Departures = serde_json::from_str(stationdata_body.as_str())
|
|
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", err)))?;
|
|
|
|
let mut out = String::new();
|
|
|
|
out.push_str("<html>\n");
|
|
out.push_str(&format!("<header><title>Abfahrten für {} nach {} | Nur Ausstieg</title></header>\n", station_properties.name, dest_station_properties.name));
|
|
out.push_str("<body>\n");
|
|
out.push_str(&format!("<h1>Abfahrten für {} nach {}</h1>\n", station_properties.name, dest_station_properties.name));
|
|
|
|
let display_all_departures = match query.all {
|
|
Some(_) => true,
|
|
None => false,
|
|
};
|
|
|
|
if display_all_departures {
|
|
out.push_str("<a href=\"?\">Don't show all departures</a>\n");
|
|
}
|
|
else {
|
|
out.push_str("<a href=\"?all=1\">Show all departures</a>\n");
|
|
}
|
|
|
|
out.push_str("<ul>\n");
|
|
|
|
for departure in stationdata.departures {
|
|
if !display_all_departures && !vec![String::from("ICE"), String::from("IC"), String::from("EC")].contains(&departure.train.r#type) {
|
|
continue;
|
|
}
|
|
|
|
let departure_info = match departure.departure {
|
|
Some(d) => d,
|
|
None => continue,
|
|
};
|
|
|
|
// Skip trains, that end here
|
|
if departure.destination.starts_with(&station_properties.name) {
|
|
continue;
|
|
}
|
|
|
|
let mut after_current_station = false;
|
|
|
|
let mut stops_at_destination = false;
|
|
for stop in &departure.route {
|
|
if !after_current_station {
|
|
if stop.name.starts_with(&station_properties.name) {
|
|
after_current_station = true;
|
|
}
|
|
}
|
|
else {
|
|
if stop.name.starts_with(&dest_station_properties.name) {
|
|
stops_at_destination = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let departure_time = chrono::DateTime::parse_from_rfc3339(&departure_info.time)
|
|
.map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, String::from("Cannot parse departure time")))?
|
|
.with_timezone(&chrono_tz::Europe::Berlin);
|
|
|
|
if stops_at_destination {
|
|
out.push_str(&format!("<li><time datetime=\"{}\">{}</time>: ", departure_info.time, departure_time.format("%H:%M")));
|
|
out.push_str(&format!("<a href=\"https://bahn.expert/details/{}%20{}/{}?evaNumberAlongRoute={}\">", &departure.train.r#type, &departure.train.number, &departure.initial_departure, station_properties.code));
|
|
out.push_str(&format!("{} ({} {})</a>", &departure.train.name, &departure.train.r#type, &departure.train.number));
|
|
out.push_str(&format!("[{}]-> {}\n", departure_info.platform, &departure.destination));
|
|
}
|
|
}
|
|
|
|
out.push_str("</ul>\n");
|
|
out.push_str("<p><small><a href=\"/\">Nur Ausstieg</a> | <a href=\"https://git.clerie.de/clerie/nurausstieg\">Source Code</a></small></p>\n");
|
|
out.push_str("</body></html>\n");
|
|
|
|
return Ok(Html(out));
|
|
|
|
}
|