This commit is contained in:
Mark 2025-03-02 02:53:37 +01:00
commit 073e7d979d
7 changed files with 3038 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/stations_list

1560
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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,
}
}