init
This commit is contained in:
commit
073e7d979d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/stations_list
|
1560
Cargo.lock
generated
Normal file
1560
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "bahnreise"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rand = "0.8.5"
|
||||||
|
rand_derive2 = "0.1.21"
|
||||||
|
reqwest = { version = "0.12.9", features = ["blocking"] }
|
||||||
|
serde = { version = "1.0.214", features = ["derive"] }
|
||||||
|
serde_json = "1.0.132"
|
||||||
|
tokio = "1.41.1"
|
238
src/game.rs
Normal file
238
src/game.rs
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
use std::{error::Error, sync::Arc, time::Instant};
|
||||||
|
|
||||||
|
use crate::stations_list::{Arrivals, Departures, FilterTransports, StationsList};
|
||||||
|
|
||||||
|
pub struct Game {
|
||||||
|
stations: Arc<StationsList>,
|
||||||
|
history: Vec<(String, Option<String>)>,
|
||||||
|
location: String,
|
||||||
|
location_name: Option<String>,
|
||||||
|
departures: Result<Departures, Box<dyn Error>>,
|
||||||
|
target: String,
|
||||||
|
target_name: Option<String>,
|
||||||
|
location_activity_count: usize,
|
||||||
|
target_activity_count: usize,
|
||||||
|
filter_transports: FilterTransports,
|
||||||
|
hint: Option<(Arrivals, Instant)>,
|
||||||
|
}
|
||||||
|
impl Game {
|
||||||
|
pub fn new(
|
||||||
|
filter_transports: FilterTransports,
|
||||||
|
location: Option<String>,
|
||||||
|
target: Option<String>,
|
||||||
|
stations: Arc<StationsList>,
|
||||||
|
) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let location = if let Some(location) = location {
|
||||||
|
match stations
|
||||||
|
.find_stations(&location)
|
||||||
|
.first()
|
||||||
|
.and_then(|(id, _)| {
|
||||||
|
stations.get_station(id, |station| {
|
||||||
|
station
|
||||||
|
.query_activity_count(id, false, 10, filter_transports.only_known())
|
||||||
|
.map(|c| (id.to_owned(), c))
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
Some(Ok((id, c))) => (c, id),
|
||||||
|
Some(Err(e)) => Err(e)?,
|
||||||
|
None => Err(format!("start does not exist"))?,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut location: Option<(usize, String)> = None;
|
||||||
|
let mut location_counter = 0;
|
||||||
|
for _ in 0..10 {
|
||||||
|
if let Some(st) = stations.get_random_station(3, |id, station| {
|
||||||
|
if let Some(act) = station
|
||||||
|
.query_activity_count(id, false, 10, filter_transports.only_known())
|
||||||
|
.ok()
|
||||||
|
.filter(|v| *v > 1)
|
||||||
|
{
|
||||||
|
Some((act, id.to_owned()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
location_counter += 1;
|
||||||
|
if location.as_ref().is_none_or(|p| p.0 < st.0) {
|
||||||
|
location = Some(st);
|
||||||
|
}
|
||||||
|
// after 5 locations with 2+ departures, take the best one
|
||||||
|
if location_counter >= 5 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
location.ok_or("could not find a start location")?
|
||||||
|
};
|
||||||
|
let target = if let Some(target) = target {
|
||||||
|
match stations.find_stations(&target).first().and_then(|(id, _)| {
|
||||||
|
stations.get_station(id, |station| {
|
||||||
|
station
|
||||||
|
.query_activity_count(id, false, 10, filter_transports.only_known())
|
||||||
|
.map(|c| (id.to_owned(), c))
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
Some(Ok((id, c))) => (c, id),
|
||||||
|
Some(Err(e)) => Err(e)?,
|
||||||
|
None => Err(format!("ziel does not exist"))?,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut target: Option<(usize, String)> = None;
|
||||||
|
let mut target_counter = 0;
|
||||||
|
for _ in 0..10 {
|
||||||
|
if let Some(st) = stations.get_random_station(3, |id, station| {
|
||||||
|
if let Some(act) = (id != location.1.as_str())
|
||||||
|
.then(|| {
|
||||||
|
station
|
||||||
|
.query_activity_count(id, true, 10, filter_transports.only_known())
|
||||||
|
.ok()
|
||||||
|
.filter(|v| *v > 1)
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
Some((act, id.to_owned()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
target_counter += 1;
|
||||||
|
if target.as_ref().is_none_or(|p| p.0 < st.0) {
|
||||||
|
target = Some(st);
|
||||||
|
}
|
||||||
|
// after 5 targets with 2+ arrivals, take the best one
|
||||||
|
if target_counter >= 5 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target.ok_or("could not find a target station")?
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
stations,
|
||||||
|
history: vec![],
|
||||||
|
location: location.1,
|
||||||
|
location_name: None,
|
||||||
|
departures: Err("".into()),
|
||||||
|
target: target.1,
|
||||||
|
target_name: None,
|
||||||
|
location_activity_count: location.0,
|
||||||
|
target_activity_count: target.0,
|
||||||
|
filter_transports,
|
||||||
|
hint: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn history(&self) -> &[(String, Option<String>)] {
|
||||||
|
&self.history
|
||||||
|
}
|
||||||
|
pub fn location(&self) -> &str {
|
||||||
|
&self.location
|
||||||
|
}
|
||||||
|
pub fn location_name(&mut self) -> &str {
|
||||||
|
if self.location_name.is_none() {
|
||||||
|
self.location_name = self
|
||||||
|
.stations
|
||||||
|
.get_station(self.location(), |s| s.name().to_owned());
|
||||||
|
}
|
||||||
|
self.location_name
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or(self.location())
|
||||||
|
}
|
||||||
|
pub fn location_name_immut(&self) -> &str {
|
||||||
|
self.location_name
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or(self.location())
|
||||||
|
}
|
||||||
|
pub fn target_name_immut(&self) -> &str {
|
||||||
|
self.target_name
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or(self.target())
|
||||||
|
}
|
||||||
|
pub fn target(&self) -> &str {
|
||||||
|
&self.target
|
||||||
|
}
|
||||||
|
pub fn target_name(&mut self) -> &str {
|
||||||
|
if self.target_name.is_none() {
|
||||||
|
self.target_name = self
|
||||||
|
.stations
|
||||||
|
.get_station(self.target(), |s| s.name().to_owned());
|
||||||
|
}
|
||||||
|
self.target_name
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or(self.target())
|
||||||
|
}
|
||||||
|
pub fn location_activity_count(&self) -> usize {
|
||||||
|
self.location_activity_count
|
||||||
|
}
|
||||||
|
pub fn target_activity_count(&self) -> usize {
|
||||||
|
self.target_activity_count
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_departures(&mut self) {
|
||||||
|
let v = std::mem::replace(&mut self.departures, Err("".into())).or_else(|_| {
|
||||||
|
match self.stations.get_station(&self.location, |location| {
|
||||||
|
location.query_departures(&self.location, 15, self.filter_transports)
|
||||||
|
}) {
|
||||||
|
Some(v) => {
|
||||||
|
if let Ok(v) = &v {
|
||||||
|
self.stations.add_new_from_departures(v);
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
None => Err(
|
||||||
|
"current location no longer exists (this is most likely a server error)".into(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.departures = v;
|
||||||
|
}
|
||||||
|
pub fn departures_cached(&self) -> Result<&Departures, &Box<dyn Error>> {
|
||||||
|
self.departures.as_ref()
|
||||||
|
}
|
||||||
|
/// Err if check failed, Ok(false) if 0 or 1 departures, Ok(true) otherwise
|
||||||
|
pub fn go_to_check(&self, location: &str) -> Result<usize, Box<dyn Error>> {
|
||||||
|
self.stations
|
||||||
|
.requery_station_if_necessary_or_add_new(location);
|
||||||
|
if let Some(act) = self.stations.get_station(location, |s| {
|
||||||
|
s.query_activity_count(location, false, 10, self.filter_transports.only_known())
|
||||||
|
}) {
|
||||||
|
Ok(act?)
|
||||||
|
} else {
|
||||||
|
Err("no such station".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// true if win
|
||||||
|
pub fn go_to_unchecked(&mut self, location: String) -> bool {
|
||||||
|
let ploc = std::mem::replace(&mut self.location, location);
|
||||||
|
let plocn = self.location_name.take();
|
||||||
|
self.history.push((ploc, plocn));
|
||||||
|
self.departures = Err("".into());
|
||||||
|
self.location == self.target
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_hint(&mut self) -> Result<&Arrivals, Box<dyn Error>> {
|
||||||
|
if self
|
||||||
|
.hint
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|v| v.1.elapsed().as_secs_f64() > 60.0)
|
||||||
|
{
|
||||||
|
let o = self
|
||||||
|
.stations
|
||||||
|
.get_station(&self.target, |station| {
|
||||||
|
station.query_arrivals(&self.target, 15, self.filter_transports)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| "failed to find target station");
|
||||||
|
if let Ok(Ok(o)) = o {
|
||||||
|
self.hint = Some((o, Instant::now()));
|
||||||
|
} else if self.hint.is_none() {
|
||||||
|
self.hint = Some((o??, Instant::now()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(&self.hint.as_ref().unwrap().0)
|
||||||
|
}
|
||||||
|
}
|
457
src/main.rs
Normal file
457
src/main.rs
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
io::{BufRead, BufReader, BufWriter, Write},
|
||||||
|
net::{TcpListener, TcpStream},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use game::Game;
|
||||||
|
use stations_list::{FilterTransports, StationsList, ALL_FILTER_TRANSPORTS};
|
||||||
|
|
||||||
|
mod game;
|
||||||
|
mod stations_list;
|
||||||
|
mod stations_thread;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let stations = StationsList::new_from("stations_list".into()).unwrap();
|
||||||
|
let stations = Arc::new(stations);
|
||||||
|
let _thread = stations_thread::spawn(Arc::clone(&stations), 100);
|
||||||
|
let listener = TcpListener::bind("0.0.0.0:26021").unwrap();
|
||||||
|
let mut threads: Vec<std::thread::JoinHandle<()>> = vec![];
|
||||||
|
let max_threads = 8;
|
||||||
|
loop {
|
||||||
|
if let Ok((mut con, _)) = listener.accept() {
|
||||||
|
if let Some(i) = threads
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.rev()
|
||||||
|
.find(|v| v.1.is_finished())
|
||||||
|
.map(|v| v.0)
|
||||||
|
{
|
||||||
|
threads.swap_remove(i);
|
||||||
|
}
|
||||||
|
if threads.len() < max_threads {
|
||||||
|
let stations = Arc::clone(&stations);
|
||||||
|
threads.push(std::thread::spawn(move || {
|
||||||
|
let _ = connection(&mut con, stations);
|
||||||
|
let _ = con.shutdown(std::net::Shutdown::Both);
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
let _ = write!(&mut con, "server busy, try again later");
|
||||||
|
let _ = con.shutdown(std::net::Shutdown::Both);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connection(con: &mut TcpStream, stations: Arc<StationsList>) -> std::io::Result<()> {
|
||||||
|
let mut w = BufWriter::new(con);
|
||||||
|
writeln!(w, "–––=== Bahnhofsreise ===̣–––")?;
|
||||||
|
writeln!(w, "Bekannte Bahnhöfe: {}", stations.station_count())?;
|
||||||
|
let filter_transports: FilterTransports = rand::random();
|
||||||
|
let mut filter_transports = filter_transports.and_unknown();
|
||||||
|
let mut start: Vec<(String, String)> = vec![];
|
||||||
|
let mut ziel: Vec<(String, String)> = vec![];
|
||||||
|
loop {
|
||||||
|
for (i, ft) in ALL_FILTER_TRANSPORTS.iter().enumerate() {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Kategorie {}: [{}] {}",
|
||||||
|
i + 1,
|
||||||
|
if *ft == filter_transports { 'x' } else { ' ' },
|
||||||
|
ft.explined()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
write!(w, " Start: ")?;
|
||||||
|
if start.is_empty() {
|
||||||
|
writeln!(w, "Zufällig")?;
|
||||||
|
} else {
|
||||||
|
for (i, v) in start.iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
write!(w, " / ")?;
|
||||||
|
}
|
||||||
|
write!(w, "{}", v.1)?;
|
||||||
|
}
|
||||||
|
writeln!(w)?;
|
||||||
|
}
|
||||||
|
write!(w, " Ziel: ")?;
|
||||||
|
if ziel.is_empty() {
|
||||||
|
writeln!(w, "Zufällig")?;
|
||||||
|
} else {
|
||||||
|
for (i, v) in ziel.iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
write!(w, " / ")?;
|
||||||
|
}
|
||||||
|
write!(w, "{}", v.1)?;
|
||||||
|
}
|
||||||
|
writeln!(w)?;
|
||||||
|
}
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Enter zum Starten, oder Kategorie <1-6>, oder Start <Bahnhof>, oder Ziel <Bahnhof>"
|
||||||
|
)?;
|
||||||
|
w.flush()?;
|
||||||
|
|
||||||
|
let line = BufReader::new(w.get_mut())
|
||||||
|
.lines()
|
||||||
|
.next()
|
||||||
|
.ok_or(std::io::ErrorKind::BrokenPipe)??
|
||||||
|
.to_lowercase();
|
||||||
|
let line = line.trim();
|
||||||
|
match line {
|
||||||
|
"kategorie 1" => filter_transports = ALL_FILTER_TRANSPORTS[0],
|
||||||
|
"kategorie 2" => filter_transports = ALL_FILTER_TRANSPORTS[1],
|
||||||
|
"kategorie 3" => filter_transports = ALL_FILTER_TRANSPORTS[2],
|
||||||
|
"kategorie 4" => filter_transports = ALL_FILTER_TRANSPORTS[3],
|
||||||
|
"kategorie 5" => filter_transports = ALL_FILTER_TRANSPORTS[4],
|
||||||
|
"kategorie 6" => filter_transports = ALL_FILTER_TRANSPORTS[5],
|
||||||
|
"" => break,
|
||||||
|
line => {
|
||||||
|
if line.starts_with("start ") {
|
||||||
|
start = stations.find_stations(line[6..].trim());
|
||||||
|
} else if line.starts_with("ziel ") {
|
||||||
|
ziel = stations.find_stations(line[5..].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start.is_empty() || ziel.is_empty() {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Suche nach Bahnhöfen, wo was los ist... (kann etwas dauern)",
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
writeln!(w, "Abfahrt . . .",)?;
|
||||||
|
}
|
||||||
|
w.flush()?;
|
||||||
|
start.truncate(1);
|
||||||
|
let start = start.pop();
|
||||||
|
ziel.truncate(1);
|
||||||
|
let ziel = ziel.pop();
|
||||||
|
let custom_ziel = ziel.is_some();
|
||||||
|
let mut game = match Game::new(
|
||||||
|
filter_transports,
|
||||||
|
start.map(|v| v.0),
|
||||||
|
ziel.map(|v| v.0),
|
||||||
|
Arc::clone(&stations),
|
||||||
|
) {
|
||||||
|
Ok(game) => game,
|
||||||
|
Err(e) => {
|
||||||
|
writeln!(w, "\n[FEHLER] Spielvorbereitung hat nicht geklappt :(\n")?;
|
||||||
|
writeln!(w, "{e}")?;
|
||||||
|
w.flush()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w, "→ Dein Startbahnhof: {}.", game.location_name())?;
|
||||||
|
writeln!(w, " → Dein Zielbahnhof: {}.", game.target_name())?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
" → Activity Scores: {} → {}",
|
||||||
|
game.location_activity_count(),
|
||||||
|
game.target_activity_count()
|
||||||
|
)?;
|
||||||
|
if custom_ziel && game.target_activity_count() < 3 {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Geringer Activity Score von {} am Ziel, sicher? [Enter])",
|
||||||
|
game.target_activity_count()
|
||||||
|
)?;
|
||||||
|
w.flush()?;
|
||||||
|
BufReader::new(w.get_mut()).lines().next();
|
||||||
|
}
|
||||||
|
w.flush()?;
|
||||||
|
let mut skip_verbindungen = false;
|
||||||
|
loop {
|
||||||
|
if !skip_verbindungen {
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w, "–– Mögliche Verbindungen ––")?;
|
||||||
|
w.flush()?;
|
||||||
|
game.update_departures();
|
||||||
|
game.location_name();
|
||||||
|
}
|
||||||
|
let departures = match game.departures_cached() {
|
||||||
|
Ok(deps) => deps,
|
||||||
|
Err(e) => {
|
||||||
|
writeln!(w, "\n[FEHLER] Zugsuche hat nicht geklappt :(\n")?;
|
||||||
|
writeln!(w, "{}", e)?;
|
||||||
|
w.flush()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut possible_locations = HashMap::<String, &str>::new();
|
||||||
|
for zug in &departures.entries {
|
||||||
|
for linie in zug {
|
||||||
|
if linie.canceled {
|
||||||
|
if !skip_verbindungen {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"[fällt aus] {} | {} → {}",
|
||||||
|
linie.line_name, linie.stop_place.name, linie.destination.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !skip_verbindungen {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"{} | {} → {}",
|
||||||
|
linie.line_name, linie.stop_place.name, linie.destination.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
possible_locations.insert(
|
||||||
|
linie.destination.name.trim().to_lowercase(),
|
||||||
|
&linie.destination.slug,
|
||||||
|
);
|
||||||
|
if !linie.via_stops.is_empty() {
|
||||||
|
if !skip_verbindungen {
|
||||||
|
write!(w, " via")?;
|
||||||
|
}
|
||||||
|
let mut width = 3;
|
||||||
|
for (istop, stop) in linie.via_stops.iter().enumerate() {
|
||||||
|
if !skip_verbindungen {
|
||||||
|
if istop > 0 {
|
||||||
|
write!(w, ",")?;
|
||||||
|
width += 1;
|
||||||
|
}
|
||||||
|
if width > 0 && width + stop.name.len() > 70 {
|
||||||
|
writeln!(w)?;
|
||||||
|
write!(w, " ")?;
|
||||||
|
width = 0;
|
||||||
|
}
|
||||||
|
if istop == 0 {
|
||||||
|
width += 1 + stop.name.len();
|
||||||
|
write!(w, " {}", stop.name)?;
|
||||||
|
} else {
|
||||||
|
width += 4 + stop.name.len();
|
||||||
|
write!(w, " {}", stop.name)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
possible_locations.insert(stop.name.trim().to_lowercase(), &stop.slug);
|
||||||
|
}
|
||||||
|
if !skip_verbindungen {
|
||||||
|
writeln!(w)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !skip_verbindungen {
|
||||||
|
writeln!(w)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !skip_verbindungen {
|
||||||
|
writeln!(w)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !skip_verbindungen {
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w, "→ Aktueller Bahnhof: {}.", game.location_name_immut())?;
|
||||||
|
writeln!(w, " → Dein Zielbahnhof: {}.", game.target_name_immut())?;
|
||||||
|
}
|
||||||
|
skip_verbindungen = false;
|
||||||
|
let mut r_errs = 0;
|
||||||
|
loop {
|
||||||
|
writeln!(w, "Wohin geht die Reise? (`?` für Hilfe)")?;
|
||||||
|
w.flush()?;
|
||||||
|
match BufReader::new(w.get_mut()).lines().next() {
|
||||||
|
None => return Ok(()),
|
||||||
|
Some(line) => {
|
||||||
|
let line = line?;
|
||||||
|
let line = line.trim();
|
||||||
|
if line == "?" {
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w, "–– Erklärung ––")?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Bahnreise ist ein Spiel basierend auf Live-Daten von `bahnhof.de`."
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Ziel ist es, vom Startbahnhof aus den Zielbahnhof zu erreichen."
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Das Spiel zeigt eine Liste von Verbindungen an, die an dem Bahnhof"
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"abfahren, an dem du gerade bist. Um irgendwohin zu fahren, tippe"
|
||||||
|
)?;
|
||||||
|
writeln!(w, "den Namen eines Bahnhofs eine und bestätige mit Enter.")?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Das wiederholst du, bis du am Zielbahnhof angekommen bist."
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Wenn du nicht weißt, wo der Zielbahnhof ist, kannst du dir"
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"mit `??` einen Tipp anzeigen lassen, der dir verrät, von wo aus"
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"du dein Ziel erreichen kannst. Sonst hilft nur noch eine Landkarte."
|
||||||
|
)?;
|
||||||
|
} else if line == "??" {
|
||||||
|
let target_name = game.target_name_immut().to_owned();
|
||||||
|
match game.get_hint() {
|
||||||
|
Ok(hint) => {
|
||||||
|
let mut arrivals: Vec<(&'_ str, Vec<&'_ str>)> = vec![];
|
||||||
|
for arrival in hint.entries.iter().map(|v| v.iter()).flatten() {
|
||||||
|
if let Some((_, lines)) = arrivals
|
||||||
|
.iter_mut()
|
||||||
|
.find(|v| v.0 == arrival.origin.name.as_str())
|
||||||
|
{
|
||||||
|
if !arrival.canceled {
|
||||||
|
if !lines.contains(&arrival.line_name.as_str()) {
|
||||||
|
lines.push(arrival.line_name.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arrivals.push((
|
||||||
|
arrival.origin.name.as_str(),
|
||||||
|
if !arrival.canceled {
|
||||||
|
vec![arrival.line_name.as_str()]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeln!(w, "\n\n[TIPP] Züge, die in {target_name} ankommen:")?;
|
||||||
|
for (origin, lines) in arrivals {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"- {origin} ({})",
|
||||||
|
if lines.is_empty() {
|
||||||
|
format!("all connections cancelled")
|
||||||
|
} else {
|
||||||
|
lines
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, v)| {
|
||||||
|
format!("{}{v}", if i == 0 { "" } else { ", " })
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
writeln!(w, "ups, das hat nicht geklappt... ({e})")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
skip_verbindungen = true;
|
||||||
|
break;
|
||||||
|
} else if line.is_empty() {
|
||||||
|
r_errs += 1;
|
||||||
|
if r_errs > 25 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
} else if let Some(new) = possible_locations
|
||||||
|
.get(line.to_lowercase().as_str())
|
||||||
|
.map(|v| (*v).to_owned())
|
||||||
|
.or_else(|| {
|
||||||
|
stations
|
||||||
|
.find_stations(line)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(id, _)| {
|
||||||
|
possible_locations
|
||||||
|
.values()
|
||||||
|
.find(|v| **v == id.as_str())
|
||||||
|
.map(|_| id.clone())
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if new.trim().is_empty() {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Diesen Halt gibt es scheinbar, aber er hat keine Bahnhofs-Seite,"
|
||||||
|
)?;
|
||||||
|
writeln!(w, "d.h. es können leider auch keine Infos/Abfahrten erfragt werden :(")?;
|
||||||
|
w.flush()?;
|
||||||
|
} else {
|
||||||
|
match game.go_to_check(new.as_str()) {
|
||||||
|
Ok(0) if game.target() != new.as_str() => {
|
||||||
|
writeln!(w, "sieht dort recht leer aus... (this is softlock protection btw)")?;
|
||||||
|
w.flush()?;
|
||||||
|
}
|
||||||
|
Ok(act) => {
|
||||||
|
let mut go_to_station = true;
|
||||||
|
if act <= 3 && game.target() != new.as_str() {
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"Geringer Activity Score von {act}, sicher? [ja/nein])"
|
||||||
|
)?;
|
||||||
|
w.flush()?;
|
||||||
|
go_to_station = false;
|
||||||
|
if let Some(Ok(v)) =
|
||||||
|
BufReader::new(w.get_mut()).lines().next()
|
||||||
|
{
|
||||||
|
if v.as_str() == "ja" {
|
||||||
|
go_to_station = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if go_to_station {
|
||||||
|
let location = new;
|
||||||
|
if game.go_to_unchecked(location) {
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w)?;
|
||||||
|
writeln!(w, "yay, du bist da :D")?;
|
||||||
|
let ilen = game.history().len().to_string().len();
|
||||||
|
for (i, (id, name)) in game.history().iter().enumerate()
|
||||||
|
{
|
||||||
|
let i = (i + 1).to_string();
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"- {}{i}. {}",
|
||||||
|
" ".repeat(ilen.saturating_sub(i.len())),
|
||||||
|
name.as_ref()
|
||||||
|
.map(|v| v.as_str())
|
||||||
|
.unwrap_or(id.as_str())
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
writeln!(
|
||||||
|
w,
|
||||||
|
"- {} → {}",
|
||||||
|
" ".repeat(ilen.saturating_sub(1)),
|
||||||
|
game.location_name()
|
||||||
|
)?;
|
||||||
|
w.flush()?;
|
||||||
|
return Ok(());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
writeln!(w, "hm, vielleicht lieber nicht dorthin? ({e})")?;
|
||||||
|
w.flush()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeln!(w, "hm, kenne ich nicht... (typo?)")?;
|
||||||
|
r_errs += 1;
|
||||||
|
if r_errs > 25 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
711
src/stations_list.rs
Normal file
711
src/stations_list.rs
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
error::Error,
|
||||||
|
fmt::Display,
|
||||||
|
path::PathBuf,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use rand::{thread_rng, Rng};
|
||||||
|
use rand_derive2::RandGen;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
pub struct StationsList {
|
||||||
|
dir: PathBuf,
|
||||||
|
stations: Mutex<HashMap<String, OptStation>>,
|
||||||
|
}
|
||||||
|
struct OptStation {
|
||||||
|
updated: Option<SystemTime>,
|
||||||
|
station: Option<Station>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StationsList {
|
||||||
|
pub fn new_from(dir: PathBuf) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let _ = std::fs::create_dir_all(&dir);
|
||||||
|
let mut stations: HashMap<String, OptStation> = Default::default();
|
||||||
|
for f in std::fs::read_dir(&dir)? {
|
||||||
|
let f = f?;
|
||||||
|
let fmeta = f.metadata()?;
|
||||||
|
if fmeta.is_file() {
|
||||||
|
let id = f.file_name();
|
||||||
|
let id = id
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| format!("non-utf8 filename {:?}!", f.file_name()))?;
|
||||||
|
let fp = f.path();
|
||||||
|
let save = std::fs::read_to_string(&fp)?;
|
||||||
|
let station = if save.trim().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Station::from_save(id, save)?)
|
||||||
|
};
|
||||||
|
stations.insert(
|
||||||
|
id.trim().to_owned(),
|
||||||
|
OptStation {
|
||||||
|
updated: Some(fmeta.modified().unwrap_or_else(|_| SystemTime::now())),
|
||||||
|
station,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if fmeta.is_dir() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut count_s: usize = 0;
|
||||||
|
let mut count_n: usize = 0;
|
||||||
|
for station in stations.values() {
|
||||||
|
if station.station.is_some() {
|
||||||
|
count_s += 1;
|
||||||
|
} else {
|
||||||
|
count_n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
eprintln!("Loaded {count_s} stations and {count_n} non-stations from {dir:?}");
|
||||||
|
Ok(Self {
|
||||||
|
dir,
|
||||||
|
stations: Mutex::new(stations),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn station_count(&self) -> usize {
|
||||||
|
self.stations
|
||||||
|
.blocking_lock()
|
||||||
|
.values()
|
||||||
|
.filter(|v| v.station.is_some())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_new_from_departures(&self, deps: &Departures) {
|
||||||
|
let mut stations = self.stations.blocking_lock();
|
||||||
|
for dep in deps.entries.iter().map(|v| v.iter()).flatten() {
|
||||||
|
for id in [&dep.stop_place.slug, &dep.destination.slug]
|
||||||
|
.into_iter()
|
||||||
|
.chain(dep.via_stops.iter().map(|v| &v.slug))
|
||||||
|
.map(|v| v.trim())
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
{
|
||||||
|
if !stations.contains_key(id) {
|
||||||
|
stations.insert(
|
||||||
|
id.to_owned(),
|
||||||
|
OptStation {
|
||||||
|
updated: None,
|
||||||
|
station: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_for_new_stations(&self) -> Result<(), Box<dyn Error>> {
|
||||||
|
// maybe find a new station or update an existing one
|
||||||
|
let html = reqwest::blocking::get("https://bahnhof.de/en/search")?
|
||||||
|
.error_for_status()?
|
||||||
|
.text()?;
|
||||||
|
for (match_index, match_str) in html.match_indices(r#"href="/en/"#) {
|
||||||
|
let id = &html[match_index + match_str.len()..];
|
||||||
|
if let Some(end) = id.find(r#"""#) {
|
||||||
|
let id = id[..end].trim();
|
||||||
|
if !id.contains(['/', '%', '&', '?', '#']) {
|
||||||
|
let mut stations = self.stations.blocking_lock();
|
||||||
|
Self::requery_station_if_necessary_or_add_new_int(id, &self.dir, &mut stations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn requery_random_station(&self, cache_count: usize) -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut stations = self.stations.blocking_lock();
|
||||||
|
if !stations.is_empty() {
|
||||||
|
for id in stations
|
||||||
|
.iter()
|
||||||
|
.skip(thread_rng().gen_range(0..stations.len()))
|
||||||
|
.take(cache_count)
|
||||||
|
.map(|v| v.0.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
{
|
||||||
|
if Self::requery_station_if_necessary_or_add_new_int(&id, &self.dir, &mut stations)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn requery_station_if_necessary_or_add_new(&self, id: &str) -> bool {
|
||||||
|
Self::requery_station_if_necessary_or_add_new_int(
|
||||||
|
id,
|
||||||
|
&self.dir,
|
||||||
|
&mut self.stations.blocking_lock(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fn requery_station_if_necessary_or_add_new_int(
|
||||||
|
id: &str,
|
||||||
|
dir: &PathBuf,
|
||||||
|
stations: &mut HashMap<String, OptStation>,
|
||||||
|
) -> bool {
|
||||||
|
if let Some(s) = stations.get_mut(id) {
|
||||||
|
// recheck stations after a day, and recheck non-stations after a week
|
||||||
|
if s.updated.is_none_or(|updated| {
|
||||||
|
updated.elapsed().is_ok_and(|elapsed| {
|
||||||
|
elapsed
|
||||||
|
> Duration::from_secs(
|
||||||
|
if s.station.is_some() { 1 } else { 7 } * 24 * 60 * 60,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
match Station::query_station(id) {
|
||||||
|
Ok(station) => {
|
||||||
|
if let Some(prev) = s.station.take() {
|
||||||
|
if prev == station {
|
||||||
|
eprintln!("Confirmed station {id} (unchanged)");
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Updated station {id} (changed): {prev:?} -> {station:?}"
|
||||||
|
);
|
||||||
|
if let Err(e) = std::fs::write(dir.join(id), station.to_save()) {
|
||||||
|
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Added new station {id}: non-station -> {station:?}");
|
||||||
|
if let Err(e) = std::fs::write(dir.join(id), station.to_save()) {
|
||||||
|
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.station = Some(station);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if s.updated.is_none_or(|updated| {
|
||||||
|
updated.elapsed().is_ok_and(|elapsed| {
|
||||||
|
elapsed > Duration::from_secs(7 * 24 * 60 * 60)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
eprintln!("Error querying station {id} and last updated over a week ago, marking as non-station! Error: {e}");
|
||||||
|
let _ = stations.remove(id);
|
||||||
|
if let Err(e) = std::fs::write(dir.join(id), "") {
|
||||||
|
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"Error querying station {id}, keeping old data for now. Error: {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stations.insert(
|
||||||
|
id.to_owned(),
|
||||||
|
OptStation {
|
||||||
|
updated: Some(SystemTime::now()),
|
||||||
|
station: match Station::query_station(id) {
|
||||||
|
Ok(station) => {
|
||||||
|
eprintln!("Added new station {id}: nothing -> {station:?}");
|
||||||
|
if let Err(e) = std::fs::write(dir.join(id), station.to_save()) {
|
||||||
|
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||||
|
}
|
||||||
|
Some(station)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Marked {id} as not a station. Error: {e}");
|
||||||
|
if let Err(e) = std::fs::write(dir.join(id), "") {
|
||||||
|
eprintln!("[ERR] Couldn't save file {:?}: {e}", dir.join(id));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_station<T>(&self, id: &str, map: impl FnOnce(&Station) -> T) -> Option<T> {
|
||||||
|
self.stations
|
||||||
|
.blocking_lock()
|
||||||
|
.get(id)
|
||||||
|
.and_then(|v| v.station.as_ref())
|
||||||
|
.map(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_random_station<T>(
|
||||||
|
&self,
|
||||||
|
limit_tries_cuberoot: usize,
|
||||||
|
map: impl Fn(&str, &Station) -> Option<T>,
|
||||||
|
) -> Option<T> {
|
||||||
|
let stations = self.stations.blocking_lock();
|
||||||
|
for i in 1..=limit_tries_cuberoot {
|
||||||
|
for i in rand::seq::index::sample(
|
||||||
|
&mut thread_rng(),
|
||||||
|
stations.len(),
|
||||||
|
(i * i).min(stations.len()),
|
||||||
|
) {
|
||||||
|
if let Some((id, opt)) = stations.iter().nth(i) {
|
||||||
|
if let Some(station) = &opt.station {
|
||||||
|
if let Some(v) = map(id, station) {
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_stations(&self, name: &str) -> Vec<(String, String)> {
|
||||||
|
let stations = self.stations.blocking_lock();
|
||||||
|
let name = name.to_lowercase();
|
||||||
|
if let Some(s) = stations.get(&name).and_then(|v| v.station.as_ref()) {
|
||||||
|
vec![(name.to_owned(), s.name.clone())]
|
||||||
|
} else {
|
||||||
|
let mut o = HashMap::<String, (String, usize)>::new();
|
||||||
|
for (id, station) in stations.iter() {
|
||||||
|
if let Some(station) = &station.station {
|
||||||
|
if station.name.to_lowercase() == name {
|
||||||
|
o.insert(id.clone(), (station.name.clone(), 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if o.len() < 3 {
|
||||||
|
for (id, station) in stations.iter() {
|
||||||
|
if let Some(station) = &station.station {
|
||||||
|
if station.name.to_lowercase().starts_with(&name) {
|
||||||
|
o.insert(
|
||||||
|
id.clone(),
|
||||||
|
(
|
||||||
|
station.name.clone(),
|
||||||
|
station.name.len().saturating_sub(name.len()).min(100),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if o.len() < 3 {
|
||||||
|
for (id, station) in stations.iter() {
|
||||||
|
if let Some(station) = &station.station {
|
||||||
|
if let Some(pos) = station.name.to_lowercase().find(&name) {
|
||||||
|
o.insert(id.clone(), (station.name.clone(), (100 + pos).min(200)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut o = o.into_iter().collect::<Vec<_>>();
|
||||||
|
o.sort_unstable_by_key(|v| v.1 .1);
|
||||||
|
o.into_iter().map(|v| (v.0, v.1 .0)).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Station {
|
||||||
|
name: String,
|
||||||
|
eva_numbers: Vec<u128>,
|
||||||
|
}
|
||||||
|
impl Station {
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_save(id: &str, save: String) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let mut name = None;
|
||||||
|
let mut eva_numbers = None;
|
||||||
|
for line in save.lines() {
|
||||||
|
match line
|
||||||
|
.split_once('=')
|
||||||
|
.map_or_else(|| ("", line.trim()), |(a, b)| (a.trim(), b.trim()))
|
||||||
|
{
|
||||||
|
("name", v) => name = Some(v.to_owned()),
|
||||||
|
("evanums", v) => eva_numbers = Some(v.to_owned()),
|
||||||
|
("", line) => match line {
|
||||||
|
line => Err(format!("invalid flag-line in bahnhof {id}: {line}"))?,
|
||||||
|
},
|
||||||
|
(key, value) => Err(format!(
|
||||||
|
"invalid key-value-line in bahnhof {id}: {key}={value}"
|
||||||
|
))?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Station {
|
||||||
|
name: name.ok_or_else(|| format!("in station {id}: missing name"))?,
|
||||||
|
eva_numbers: eva_numbers
|
||||||
|
.ok_or_else(|| format!("in station {id}: missing evanums"))?
|
||||||
|
.split(',')
|
||||||
|
.map(|v| {
|
||||||
|
v.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("eva number {v} could not be parsed: {e}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn to_save(&self) -> String {
|
||||||
|
let mut o = String::new();
|
||||||
|
o.push_str("name=");
|
||||||
|
o.push_str(&self.name);
|
||||||
|
o.push('\n');
|
||||||
|
if !self.eva_numbers.is_empty() {
|
||||||
|
o.push_str("evanums=");
|
||||||
|
for (i, num) in self.eva_numbers.iter().enumerate() {
|
||||||
|
if i != 0 {
|
||||||
|
o.push(',');
|
||||||
|
}
|
||||||
|
o.push_str(&format!("{num}"));
|
||||||
|
}
|
||||||
|
o.push('\n');
|
||||||
|
}
|
||||||
|
o
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query_station(id: &str) -> Result<Self, Box<dyn Error>> {
|
||||||
|
let html =
|
||||||
|
reqwest::blocking::get(format!("https://www.bahnhof.de/{id}/departure"))?.text()?;
|
||||||
|
let start_pat = r#"<title>Abfahrt "#;
|
||||||
|
if let (Some(start), Some(end)) = (html.find(start_pat), html.find("</title>")) {
|
||||||
|
let start = start + start_pat.len();
|
||||||
|
let mut name = if end > start {
|
||||||
|
html[start..end].trim()
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
// skip next char (some UTF8 dash)
|
||||||
|
if !name.is_empty() {
|
||||||
|
name = name[name.chars().next().unwrap().len_utf8()..].trim();
|
||||||
|
}
|
||||||
|
let pat = r#"<meta name="bf:evaNumbers" content=""#;
|
||||||
|
if let Some(index) = html.find(pat) {
|
||||||
|
let rest = &html[index + pat.len()..];
|
||||||
|
if let Some(end) = rest.find('"') {
|
||||||
|
let eva_numbers = rest[..end]
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.map(|v| {
|
||||||
|
v.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| format!("eva number {v} could not be parsed: {e}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
if eva_numbers.is_empty() {
|
||||||
|
Err("no evaNumbers (found empty list)")?;
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
name: name.to_owned(),
|
||||||
|
eva_numbers,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err("missing evaNumbers")?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("missing evaNumbers")?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let start_pat = "<title>";
|
||||||
|
if let (Some(start), Some(end)) = (html.find(start_pat), html.find("</title>")) {
|
||||||
|
if start + start_pat.len() < end {
|
||||||
|
Err(format!(
|
||||||
|
"missing title `Abfahrt - <name>`: title was `{}`",
|
||||||
|
&html[start + start_pat.len()..end]
|
||||||
|
))?
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"missing title `Abfahrt - <name>`: </title> before <title>"
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"missing title `Abfahrt - <name>`: no <title> found in `{html}`"
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query_departures(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
minutes: u8,
|
||||||
|
filter_transports: FilterTransports,
|
||||||
|
) -> Result<Departures, Box<dyn Error>> {
|
||||||
|
let _ = id;
|
||||||
|
if self.eva_numbers.is_empty() {
|
||||||
|
Err("station has no eva numbers")?;
|
||||||
|
}
|
||||||
|
let json =
|
||||||
|
reqwest::blocking::get(self.query_departures_url(false, minutes, filter_transports))?
|
||||||
|
.text()?;
|
||||||
|
Ok(serde_json::from_str(&json).map_err(|e| format!("{e}\nin:\n{json}"))?)
|
||||||
|
}
|
||||||
|
pub fn query_arrivals(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
minutes: u8,
|
||||||
|
filter_transports: FilterTransports,
|
||||||
|
) -> Result<Arrivals, Box<dyn Error>> {
|
||||||
|
let _ = id;
|
||||||
|
if self.eva_numbers.is_empty() {
|
||||||
|
Err("station has no eva numbers")?;
|
||||||
|
}
|
||||||
|
let json =
|
||||||
|
reqwest::blocking::get(self.query_departures_url(true, minutes, filter_transports))?
|
||||||
|
.text()?;
|
||||||
|
Ok(serde_json::from_str(&json).map_err(|e| format!("{e}\nin:\n{json}"))?)
|
||||||
|
}
|
||||||
|
fn query_departures_url(
|
||||||
|
&self,
|
||||||
|
arrivals: bool,
|
||||||
|
minutes: u8,
|
||||||
|
filter_transports: FilterTransports,
|
||||||
|
) -> String {
|
||||||
|
let mut url = format!(
|
||||||
|
"https://www.bahnhof.de/api/boards/{}?",
|
||||||
|
if arrivals { "arrivals" } else { "departures" }
|
||||||
|
);
|
||||||
|
for num in self.eva_numbers.iter() {
|
||||||
|
url.push_str(&format!("evaNumbers={num}&"));
|
||||||
|
}
|
||||||
|
url.push_str(&format!(
|
||||||
|
"duration={minutes}{filter_transports}&locale=de&sortBy=TIME_SCHEDULE"
|
||||||
|
));
|
||||||
|
url
|
||||||
|
}
|
||||||
|
pub fn query_activity_count(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
arrivals: bool,
|
||||||
|
minutes: u8,
|
||||||
|
filter_transports: FilterTransports,
|
||||||
|
) -> Result<usize, Box<dyn Error>> {
|
||||||
|
let _ = id;
|
||||||
|
if self.eva_numbers.is_empty() {
|
||||||
|
Err("station has no eva numbers")?;
|
||||||
|
}
|
||||||
|
let mut url = format!(
|
||||||
|
"https://www.bahnhof.de/api/boards/{}?",
|
||||||
|
if arrivals { "arrivals" } else { "departures" }
|
||||||
|
);
|
||||||
|
for num in self.eva_numbers.iter() {
|
||||||
|
url.push_str(&format!("evaNumbers={num}&"));
|
||||||
|
}
|
||||||
|
url.push_str(&format!(
|
||||||
|
"duration={minutes}{filter_transports}&locale=de&sortBy=TIME_SCHEDULE"
|
||||||
|
));
|
||||||
|
let json = reqwest::blocking::get(url)?.text()?;
|
||||||
|
let arrivals: ArrivalsOrDepartures =
|
||||||
|
serde_json::from_str(&json).map_err(|e| format!("{e}\nin:\n{json}"))?;
|
||||||
|
Ok(arrivals
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.filter(|v| !v.iter().any(|v| v.canceled))
|
||||||
|
.count())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ArrivalsOrDepartures {
|
||||||
|
pub entries: Vec<Vec<ArrivalOrDeparture>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ArrivalOrDeparture {
|
||||||
|
pub canceled: bool,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Departures {
|
||||||
|
/// Vec<Vec<_>> because there may be a departure of multiple trains (usually coupled together, but still different trains/routes/destinations)
|
||||||
|
pub entries: Vec<Vec<Departure>>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Departure {
|
||||||
|
pub canceled: bool,
|
||||||
|
pub line_name: String,
|
||||||
|
pub stop_place: SomeStation,
|
||||||
|
pub destination: SomeStation,
|
||||||
|
pub via_stops: Vec<SomeStation>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SomeStation {
|
||||||
|
// pub eva_number: String,
|
||||||
|
pub name: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub slug: String,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Arrivals {
|
||||||
|
pub entries: Vec<Vec<Arrival>>,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Arrival {
|
||||||
|
pub canceled: bool,
|
||||||
|
pub line_name: String,
|
||||||
|
// pub stop_place: SomeStation,
|
||||||
|
pub origin: SomeStation,
|
||||||
|
// pub via_stops: Vec<SomeStation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, RandGen, PartialEq, Eq)]
|
||||||
|
pub enum FilterTransports {
|
||||||
|
All,
|
||||||
|
AllKnown,
|
||||||
|
AllTrains,
|
||||||
|
AllTrainsKnown,
|
||||||
|
Trains,
|
||||||
|
TrainsKnown,
|
||||||
|
AllRegionalTrains,
|
||||||
|
AllRegionalTrainsKnown,
|
||||||
|
RegionalTrains,
|
||||||
|
RegionalTrainsKnown,
|
||||||
|
HighSpeedTrains,
|
||||||
|
HighSpeedTrainsKnown,
|
||||||
|
}
|
||||||
|
pub const ALL_FILTER_TRANSPORTS: [FilterTransports; 6] = [
|
||||||
|
FilterTransports::All,
|
||||||
|
FilterTransports::AllTrains,
|
||||||
|
FilterTransports::Trains,
|
||||||
|
FilterTransports::AllRegionalTrains,
|
||||||
|
FilterTransports::RegionalTrains,
|
||||||
|
FilterTransports::HighSpeedTrains,
|
||||||
|
];
|
||||||
|
impl Display for FilterTransports {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.allow_high_speed() {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"&filterTransports=HIGH_SPEED_TRAIN&filterTransports=INTERCITY_TRAIN&filterTransports=INTER_REGIONAL_TRAIN"
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if self.allow_regional() {
|
||||||
|
write!(f, "&filterTransports=REGIONAL_TRAIN")?;
|
||||||
|
}
|
||||||
|
if self.allow_local() {
|
||||||
|
write!(f, "&filterTransports=CITY_TRAIN&filterTransports=TRAM")?;
|
||||||
|
}
|
||||||
|
if self.allow_bus() {
|
||||||
|
write!(f, "&filterTransports=BUS")?;
|
||||||
|
}
|
||||||
|
if self.allow_unknown() {
|
||||||
|
write!(f, "&filterTransports=UNKNOWN")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FilterTransports {
|
||||||
|
fn allow_unknown(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::All
|
||||||
|
| Self::AllTrains
|
||||||
|
| Self::Trains
|
||||||
|
| Self::AllRegionalTrains
|
||||||
|
| Self::RegionalTrains
|
||||||
|
| Self::HighSpeedTrains => true,
|
||||||
|
Self::AllKnown
|
||||||
|
| Self::AllTrainsKnown
|
||||||
|
| Self::TrainsKnown
|
||||||
|
| Self::AllRegionalTrainsKnown
|
||||||
|
| Self::RegionalTrainsKnown
|
||||||
|
| Self::HighSpeedTrainsKnown => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn allow_bus(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::All | Self::AllKnown => true,
|
||||||
|
Self::AllTrains
|
||||||
|
| Self::AllTrainsKnown
|
||||||
|
| Self::Trains
|
||||||
|
| Self::TrainsKnown
|
||||||
|
| Self::AllRegionalTrains
|
||||||
|
| Self::AllRegionalTrainsKnown
|
||||||
|
| Self::RegionalTrains
|
||||||
|
| Self::RegionalTrainsKnown
|
||||||
|
| Self::HighSpeedTrains
|
||||||
|
| Self::HighSpeedTrainsKnown => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn allow_local(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::All
|
||||||
|
| Self::AllKnown
|
||||||
|
| Self::AllTrains
|
||||||
|
| Self::AllTrainsKnown
|
||||||
|
| Self::AllRegionalTrains
|
||||||
|
| Self::AllRegionalTrainsKnown => true,
|
||||||
|
Self::Trains
|
||||||
|
| Self::TrainsKnown
|
||||||
|
| Self::RegionalTrains
|
||||||
|
| Self::RegionalTrainsKnown
|
||||||
|
| Self::HighSpeedTrains
|
||||||
|
| Self::HighSpeedTrainsKnown => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn allow_regional(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::All
|
||||||
|
| Self::AllKnown
|
||||||
|
| Self::AllTrains
|
||||||
|
| Self::AllTrainsKnown
|
||||||
|
| Self::Trains
|
||||||
|
| Self::TrainsKnown
|
||||||
|
| Self::AllRegionalTrains
|
||||||
|
| Self::AllRegionalTrainsKnown
|
||||||
|
| Self::RegionalTrains
|
||||||
|
| Self::RegionalTrainsKnown => true,
|
||||||
|
Self::HighSpeedTrains | Self::HighSpeedTrainsKnown => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn allow_high_speed(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::All
|
||||||
|
| Self::AllKnown
|
||||||
|
| Self::AllTrains
|
||||||
|
| Self::AllTrainsKnown
|
||||||
|
| Self::Trains
|
||||||
|
| Self::TrainsKnown
|
||||||
|
| Self::HighSpeedTrains
|
||||||
|
| Self::HighSpeedTrainsKnown => true,
|
||||||
|
Self::AllRegionalTrains
|
||||||
|
| Self::AllRegionalTrainsKnown
|
||||||
|
| Self::RegionalTrains
|
||||||
|
| Self::RegionalTrainsKnown => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn only_known(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::All | Self::AllKnown => Self::AllKnown,
|
||||||
|
Self::AllTrains | Self::AllTrainsKnown => Self::AllTrainsKnown,
|
||||||
|
Self::Trains | Self::TrainsKnown => Self::TrainsKnown,
|
||||||
|
Self::AllRegionalTrains | Self::AllRegionalTrainsKnown => Self::AllRegionalTrainsKnown,
|
||||||
|
Self::RegionalTrains | Self::RegionalTrainsKnown => Self::RegionalTrainsKnown,
|
||||||
|
Self::HighSpeedTrains | Self::HighSpeedTrainsKnown => Self::HighSpeedTrainsKnown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn and_unknown(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::All | Self::AllKnown => Self::All,
|
||||||
|
Self::AllTrains | Self::AllTrainsKnown => Self::AllTrains,
|
||||||
|
Self::Trains | Self::TrainsKnown => Self::Trains,
|
||||||
|
Self::AllRegionalTrains | Self::AllRegionalTrainsKnown => Self::AllRegionalTrains,
|
||||||
|
Self::RegionalTrains | Self::RegionalTrainsKnown => Self::RegionalTrains,
|
||||||
|
Self::HighSpeedTrains | Self::HighSpeedTrainsKnown => Self::HighSpeedTrains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn explined(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::All | Self::AllKnown => "Öffis (Zug, S+U, Tram, Bus)",
|
||||||
|
Self::AllTrains | Self::AllTrainsKnown => "Schienenverkehr (Zug, S+U, Tram)",
|
||||||
|
Self::Trains | Self::TrainsKnown => "Züge (Zug, ohne Tram)",
|
||||||
|
Self::AllRegionalTrains | Self::AllRegionalTrainsKnown => {
|
||||||
|
"Deutschland-Ticket (Regio, S+U, Tram)"
|
||||||
|
}
|
||||||
|
Self::RegionalTrains | Self::RegionalTrainsKnown => "Regionalzüge (Regio, ohne Tram)",
|
||||||
|
Self::HighSpeedTrains | Self::HighSpeedTrainsKnown => "nur Hochgeschwindigkeitszüge",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
58
src/stations_thread.rs
Normal file
58
src/stations_thread.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
use std::{
|
||||||
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
|
thread::{sleep, JoinHandle},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
use crate::stations_list::StationsList;
|
||||||
|
|
||||||
|
pub struct StationsThread {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
min_stations: Arc<AtomicUsize>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
join_handle: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
impl StationsThread {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn turbo(&self, min_stations: usize) {
|
||||||
|
self.min_stations
|
||||||
|
.store(min_stations, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn(stations: Arc<StationsList>, min_stations: usize) -> StationsThread {
|
||||||
|
let min_stations = Arc::new(AtomicUsize::new(min_stations));
|
||||||
|
let min_station_count = Arc::clone(&min_stations);
|
||||||
|
let join_handle = std::thread::spawn(move || loop {
|
||||||
|
let turbo =
|
||||||
|
stations.station_count() < min_station_count.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
sleep(Duration::from_secs(
|
||||||
|
rand::thread_rng().gen_range(10..=80) * if turbo { 1 } else { 60 },
|
||||||
|
));
|
||||||
|
match stations.query_for_new_stations() {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to query for new stations:\n{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _ in 0..10 {
|
||||||
|
sleep(Duration::from_secs(if turbo {
|
||||||
|
rand::thread_rng().gen_range(10..=30)
|
||||||
|
} else {
|
||||||
|
rand::thread_rng().gen_range(30..=120)
|
||||||
|
}));
|
||||||
|
match stations.requery_random_station(10) {
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to requery stations:\n{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
StationsThread {
|
||||||
|
min_stations,
|
||||||
|
join_handle,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user