summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rwxr-xr-xlib/Travelynx.pm493
-rw-r--r--lib/Travelynx/Command/database.pm386
-rw-r--r--lib/Travelynx/Command/traewelling.pm4
-rw-r--r--lib/Travelynx/Command/work.pm298
-rw-r--r--lib/Travelynx/Controller/Account.pm99
-rwxr-xr-xlib/Travelynx/Controller/Api.pm14
-rwxr-xr-xlib/Travelynx/Controller/Profile.pm28
-rw-r--r--lib/Travelynx/Controller/Traewelling.pm2
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm347
-rw-r--r--lib/Travelynx/Helper/DBDB.pm21
-rw-r--r--lib/Travelynx/Helper/DBRIS.pm147
-rw-r--r--lib/Travelynx/Helper/HAFAS.pm104
-rw-r--r--lib/Travelynx/Helper/MOTIS.pm158
-rw-r--r--lib/Travelynx/Helper/Traewelling.pm18
-rw-r--r--lib/Travelynx/Model/InTransit.pm475
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm30
-rw-r--r--lib/Travelynx/Model/Stations.pm173
-rw-r--r--lib/Travelynx/Model/Traewelling.pm2
-rw-r--r--lib/Travelynx/Model/Users.pm6
19 files changed, 2546 insertions, 259 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 0c8ceec..75c9b57 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -1,6 +1,7 @@
package Travelynx;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -19,11 +20,13 @@ use JSON;
use List::Util;
use List::UtilsBy qw(uniq_by);
use List::MoreUtils qw(first_index);
-use Travel::Status::DE::DBWagenreihung;
+use Travel::Status::DE::DBRIS::Formation;
use Travelynx::Helper::DBDB;
+use Travelynx::Helper::DBRIS;
use Travelynx::Helper::EFA;
use Travelynx::Helper::HAFAS;
use Travelynx::Helper::IRIS;
+use Travelynx::Helper::MOTIS;
use Travelynx::Helper::Sendmail;
use Travelynx::Helper::Traewelling;
use Travelynx::Model::InTransit;
@@ -185,7 +188,7 @@ sub startup {
$self->attr(
ice_name => sub {
state $id_to_name = {
- Travel::Status::DE::DBWagenreihung::Group::name_to_designation(
+ Travel::Status::DE::DBRIS::Formation::Group::name_to_designation(
)
};
return $id_to_name;
@@ -233,12 +236,26 @@ sub startup {
}
);
+ $self->helper(
+ dbris => sub {
+ my ($self) = @_;
+ state $dbris = Travelynx::Helper::DBRIS->new(
+ log => $self->app->log,
+ service_config => $self->app->config->{dbris},
+ cache => $self->app->cache_iris_rt,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
+ }
+ );
$self->helper(
hafas => sub {
my ($self) = @_;
state $hafas = Travelynx::Helper::HAFAS->new(
log => $self->app->log,
+ service_config => $self->app->config->{hafas},
main_cache => $self->app->cache_iris_main,
realtime_cache => $self->app->cache_iris_rt,
root_url => $self->base_url_for('/')->to_abs,
@@ -262,6 +279,19 @@ sub startup {
);
$self->helper(
+ motis => sub {
+ my ($self) = @_;
+ state $motis = Travelynx::Helper::MOTIS->new(
+ log => $self->app->log,
+ cache => $self->app->cache_iris_rt,
+ user_agent => $self->ua,
+ root_url => $self->base_url_for('/')->to_abs,
+ version => $self->app->config->{version},
+ );
+ }
+ );
+
+ $self->helper(
traewelling => sub {
my ($self) = @_;
state $trwl = Travelynx::Model::Traewelling->new( pg => $self->pg );
@@ -422,6 +452,14 @@ sub startup {
my $first = $load->{FIRST} // 0;
my $second = $load->{SECOND} // 0;
+ # DBRIS
+ if ( $first == 99 ) {
+ $first = 4;
+ }
+ if ( $second == 99 ) {
+ $second = 4;
+ }
+
my @symbols
= (
qw(help_outline person_outline people priority_high not_interested)
@@ -469,6 +507,12 @@ sub startup {
return Mojo::Promise->reject('You are already checked in');
}
+ if ( $opt{motis} ) {
+ return $self->_checkin_motis_p(%opt);
+ }
+ if ( $opt{dbris} ) {
+ return $self->_checkin_dbris_p(%opt);
+ }
if ( $opt{hafas} ) {
return $self->_checkin_hafas_p(%opt);
}
@@ -548,6 +592,300 @@ sub startup {
);
$self->helper(
+ '_checkin_motis_p' => sub {
+ my ( $self, %opt ) = @_;
+
+ my $station = $opt{station};
+ my $train_id = $opt{train_id};
+ my $ts = $opt{ts};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $hafas;
+
+ my $promise = Mojo::Promise->new;
+
+ $self->motis->get_trip_p(
+ service => $opt{motis},
+ trip_id => $train_id,
+ )->then(
+ sub {
+ my ($trip) = @_;
+ my $found_stopover;
+
+ for my $stopover ( $trip->stopovers ) {
+ if ( $stopover->stop->id eq $station ) {
+ $found_stopover = $stopover;
+
+ # Lines may serve the same stop several times.
+ # Keep looking until the scheduled departure
+ # matches the one passed while checking in.
+ if ( $ts
+ and $stopover->scheduled_departure->epoch
+ == $ts )
+ {
+ last;
+ }
+ }
+ }
+
+ if ( not $found_stopover ) {
+ $promise->reject(
+"Did not find stopover at '$station' within trip '$train_id'"
+ );
+ return;
+ }
+
+ for my $stopover ( $trip->stopovers ) {
+ $self->stations->add_or_update(
+ stop => $stopover->stop,
+ db => $db,
+ motis => $opt{motis},
+ );
+ }
+
+ $self->stations->add_or_update(
+ stop => $found_stopover->stop,
+ db => $db,
+ motis => $opt{motis},
+ );
+
+ eval {
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ journey => $trip,
+ stopover => $found_stopover,
+ data => { trip_id => $train_id },
+ backend_id => $self->stations->get_backend_id(
+ motis => $opt{motis}
+ ),
+ );
+ };
+
+ if ($@) {
+ $self->app->log->error(
+ "Checkin($uid): INSERT failed: $@");
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
+
+ my $polyline;
+ if ( $trip->polyline ) {
+ my @station_list;
+ my @coordinate_list;
+ for my $coordinate ( $trip->polyline ) {
+ if ( $coordinate->{stop} ) {
+ if ( not defined $coordinate->{stop}->{eva} ) {
+ die();
+ }
+
+ push(
+ @coordinate_list,
+ [
+ $coordinate->{lon},
+ $coordinate->{lat},
+ $coordinate->{stop}->{eva}
+ ]
+ );
+
+ push( @station_list,
+ $coordinate->{stop}->name );
+ }
+ else {
+ push( @coordinate_list,
+ [ $coordinate->{lon}, $coordinate->{lat} ]
+ );
+ }
+ }
+
+ # equal length → polyline only consists of straight
+ # lines between stops. that's not helpful.
+ if ( @station_list == @coordinate_list ) {
+ $self->log->debug( 'Ignoring polyline for '
+ . $trip->route_name
+ . ' as it only consists of straight lines between stops.'
+ );
+ }
+ else {
+ $polyline = {
+ from_eva =>
+ ( $trip->stopovers )[0]->stop->{eva},
+ to_eva => ( $trip->stopovers )[-1]->stop->{eva},
+ coords => \@coordinate_list,
+ };
+ }
+ }
+
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ );
+ }
+
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkin' );
+ }
+
+ $promise->resolve($trip);
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+ }
+ );
+
+ $self->helper(
+ '_checkin_dbris_p' => sub {
+ my ( $self, %opt ) = @_;
+
+ my $station = $opt{station};
+ my $train_id = $opt{train_id};
+ my $train_suffix = $opt{train_suffix};
+ my $ts = $opt{ts};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $hafas;
+
+ my $promise = Mojo::Promise->new;
+
+ $self->dbris->get_journey_p(
+ trip_id => $train_id,
+ with_polyline => 1
+ )->then(
+ sub {
+ my ($journey) = @_;
+ my $found;
+ for my $stop ( $journey->route ) {
+ if ( $stop->eva eq $station ) {
+ $found = $stop;
+
+ # Lines may serve the same stop several times.
+ # Keep looking until the scheduled departure
+ # matches the one passed while checking in.
+ if ( $ts and $stop->sched_dep->epoch == $ts ) {
+ last;
+ }
+ }
+ }
+ if ( not $found ) {
+ $promise->reject(
+"Did not find stop '$station' within journey '$train_id'"
+ );
+ return;
+ }
+ for my $stop ( $journey->route ) {
+ $self->stations->add_or_update(
+ stop => $stop,
+ db => $db,
+ dbris => 'bahn.de',
+ );
+ }
+ eval {
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ journey => $journey,
+ stop => $found,
+ data => { trip_id => $train_id },
+ backend_id => $self->stations->get_backend_id(
+ dbris => 'bahn.de'
+ ),
+ train_suffix => $train_suffix,
+ );
+ };
+ if ($@) {
+ $self->app->log->error(
+ "Checkin($uid): INSERT failed: $@");
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
+
+ my $polyline;
+ if ( $journey->polyline ) {
+ my @station_list;
+ my @coordinate_list;
+ for my $coord ( $journey->polyline ) {
+ if ( $coord->{stop} ) {
+ push(
+ @coordinate_list,
+ [
+ $coord->{lon}, $coord->{lat},
+ $coord->{stop}->eva
+ ]
+ );
+ push( @station_list, $coord->{stop}->name );
+ }
+ else {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat} ] );
+ }
+ }
+
+ # equal length → polyline only consists of straight
+ # lines between stops. that's not helpful.
+ if ( @station_list == @coordinate_list ) {
+ $self->log->debug( 'Ignoring polyline for '
+ . $journey->train
+ . ' as it only consists of straight lines between stops.'
+ );
+ }
+ else {
+ $polyline = {
+ from_eva => ( $journey->route )[0]->eva,
+ to_eva => ( $journey->route )[-1]->eva,
+ coords => \@coordinate_list,
+ };
+ }
+ }
+
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ );
+ }
+
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkin' );
+ $self->add_wagonorder(
+ uid => $uid,
+ train_id => $train_id,
+ is_departure => 1,
+ eva => $found->eva,
+ datetime => $found->sched_dep,
+ train_type => $journey->type,
+ train_no => $journey->train_no,
+ );
+ $self->add_stationinfo( $uid, 1, $train_id,
+ $found->eva );
+ }
+
+ $promise->resolve($journey);
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+ }
+ );
+
+ $self->helper(
'_checkin_hafas_p' => sub {
my ( $self, %opt ) = @_;
@@ -805,8 +1143,7 @@ sub startup {
}
if ( not $user->{checked_in} and not $user->{cancelled} ) {
- return $promise->resolve( 0,
- 'You are not checked into any train' );
+ return $promise->resolve( 0, 'You are not checked in' );
}
if ( $dep_eva and $dep_eva != $user->{dep_eva} ) {
@@ -816,8 +1153,8 @@ sub startup {
return $promise->resolve( 0, 'race condition' );
}
- if ( $user->{is_hafas} ) {
- return $self->_checkout_hafas_p(%opt);
+ if ( $user->{is_dbris} or $user->{is_hafas} or $user->{is_motis} ) {
+ return $self->_checkout_journey_p(%opt);
}
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
@@ -950,7 +1287,7 @@ sub startup {
if (@unknown_stations) {
$self->app->log->warn(
sprintf(
-'Route of %s %s (%s -> %s) contains unknown stations: %s',
+'IRIS: Route of %s %s (%s -> %s) contains unknown stations: %s',
$train->type,
$train->train_no,
$train->origin,
@@ -1066,7 +1403,7 @@ sub startup {
);
$self->helper(
- '_checkout_hafas_p' => sub {
+ '_checkout_journey_p' => sub {
my ( $self, %opt ) = @_;
my $station = $opt{station};
@@ -1092,7 +1429,7 @@ sub startup {
my $has_arrived;
for my $stop ( @{ $journey->{route_after} } ) {
if ( $station eq $stop->[0] or $station eq $stop->[1] ) {
- $found = 1;
+ $found = $stop;
$self->in_transit->set_arrival_eva(
uid => $uid,
db => $db,
@@ -1160,6 +1497,22 @@ sub startup {
uid => $uid
);
}
+ elsif ( $found and $found->[2]{isCancelled} ) {
+ $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db
+ );
+ $journey->{cancelled} = 1;
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->set_cancelled_destination(
+ uid => $uid,
+ db => $db,
+ cancelled_destination => $found->[0],
+ );
+ }
if ($tx) {
$tx->commit;
@@ -1301,35 +1654,52 @@ sub startup {
my $data = {};
my $user_data = {};
- if ( $opt{is_departure}
+ my $wr;
+ eval {
+ $wr
+ = Travel::Status::DE::DBRIS::Formation->new(
+ json => $wagonorder );
+ };
+
+ if ( $opt{is_departure}
+ and $wr
and not exists $wagonorder->{error} )
{
+ my $dt
+ = $opt{datetime}->clone->set_time_zone('UTC');
$data->{wagonorder_dep} = $wagonorder;
+ $data->{wagonorder_param} = {
+ time => $dt->rfc3339 =~ s{(?=Z)}{.000}r,
+ number => $opt{train_no},
+ evaNumber => $opt{eva},
+ administrationId => 80,
+ date => $dt->strftime('%Y-%m-%d'),
+ category => $opt{train_type},
+ };
$user_data->{wagongroups} = [];
- for my $group ( @{ $wagonorder->{groups} // [] } ) {
+ for my $group ( $wr->groups ) {
my @wagons;
- for my $wagon ( @{ $group->{vehicles} // [] } )
- {
+ for my $wagon ( $group->carriages ) {
push(
@wagons,
{
- id => $wagon->{vehicleID},
- number => $wagon
- ->{wagonIdentificationNumber},
- type =>
- $wagon->{type}{constructionType},
+ id => $wagon->uic_id,
+ number => $wagon->number,
+ type => $wagon->type,
}
);
}
push(
@{ $user_data->{wagongroups} },
{
- name => $group->{name},
- to => $group->{transport}{destination}
- {name},
- type => $group->{transport}{category},
- no => $group->{transport}{number},
- wagons => [@wagons],
+ name => $group->name,
+ desc => $group->desc_short,
+ description => $group->description,
+ designation => $group->designation,
+ to => $group->destination,
+ type => $group->train_type,
+ no => $group->train_no,
+ wagons => [@wagons],
}
);
if ( $group->{name}
@@ -1608,7 +1978,15 @@ sub startup {
$ret =~ s{[{]tt[}]}{$opt{tt}}g;
$ret =~ s{[{]tn[}]}{$opt{tn}}g;
$ret =~ s{[{]id[}]}{$opt{id}}g;
+ $ret =~ s{[{]dbris[}]}{$opt{dbris}}g;
$ret =~ s{[{]hafas[}]}{$opt{hafas}}g;
+
+ if ( $opt{id} and not $opt{is_iris} ) {
+ $ret =~ s{[{]id_or_tttn[}]}{$opt{id}}g;
+ }
+ else {
+ $ret =~ s{[{]id_or_tttn[}]}{$opt{tt}$opt{tn}}g;
+ }
return $ret;
}
);
@@ -1639,8 +2017,8 @@ sub startup {
my $wr;
eval {
$wr
- = Travel::Status::DE::DBWagenreihung->new(
- from_json => $wagonorder );
+ = Travel::Status::DE::DBRIS::Formation->new(
+ json => $wagonorder );
};
if ( $wr
and $wr->sectors
@@ -1709,6 +2087,7 @@ sub startup {
uid => $uid,
db => $db,
with_data => 1,
+ with_polyline => 1,
with_timestamps => 1,
with_visibility => 1,
postprocess => 1,
@@ -1785,8 +2164,8 @@ sub startup {
my $wr;
eval {
$wr
- = Travel::Status::DE::DBWagenreihung->new(
- from_json => $in_transit->{data}{wagonorder_dep} );
+ = Travel::Status::DE::DBRIS::Formation->new(
+ json => $in_transit->{data}{wagonorder_dep} );
};
if ( $wr
and $wr->carriages
@@ -1857,8 +2236,10 @@ sub startup {
cancellation => $latest_cancellation,
backend_id => $latest->{backend_id},
backend_name => $latest->{backend_name},
+ is_dbris => $latest->{is_dbris},
is_iris => $latest->{is_iris},
is_hafas => $latest->{is_hafas},
+ is_motis => $latest->{is_motis},
journey_id => $latest->{journey_id},
timestamp => $action_time,
timestamp_delta => $now->epoch - $action_time->epoch,
@@ -1870,6 +2251,7 @@ sub startup {
real_departure => epoch_to_dt( $latest->{real_dep_ts} ),
dep_ds100 => $latest->{dep_ds100},
dep_eva => $latest->{dep_eva},
+ dep_external_id => $latest->{dep_external_id},
dep_name => $latest->{dep_name},
dep_lat => $latest->{dep_lat},
dep_lon => $latest->{dep_lon},
@@ -1878,6 +2260,7 @@ sub startup {
real_arrival => epoch_to_dt( $latest->{real_arr_ts} ),
arr_ds100 => $latest->{arr_ds100},
arr_eva => $latest->{arr_eva},
+ arr_external_id => $latest->{arr_external_id},
arr_name => $latest->{arr_name},
arr_lat => $latest->{arr_lat},
arr_lon => $latest->{arr_lon},
@@ -1918,8 +2301,11 @@ sub startup {
) ? \1 : \0,
comment => $status->{comment},
backend => {
- id => $status->{backend_id},
- type => $status->{is_hafas} ? 'HAFAS' : 'IRIS-TTS',
+ id => $status->{backend_id},
+ type => $status->{is_dbris} ? 'DBRIS'
+ : $status->{is_hafas} ? 'HAFAS'
+ : $status->{is_motis} ? 'MOTIS'
+ : 'IRIS-TTS',
name => $status->{backend_name},
},
fromStation => {
@@ -2042,8 +2428,7 @@ sub startup {
my $db = $self->pg->db;
my $tx = $db->begin;
- $self->_checkin_hafas_p(
- hafas => 'DB',
+ $self->_checkin_dbris_p(
station => $traewelling->{dep_eva},
train_id => $traewelling->{trip_id},
uid => $uid,
@@ -2052,8 +2437,7 @@ sub startup {
)->then(
sub {
$self->log->debug("... handled origin");
- return $self->_checkout_hafas_p(
- hafas => 'DB',
+ return $self->_checkout_journey_p(
station => $traewelling->{arr_eva},
train_id => $traewelling->{trip_id},
uid => $uid,
@@ -2138,12 +2522,12 @@ sub startup {
my @stations = uniq_by { $_->{name} } map {
{
- name => $_->{to_name},
- latlon => $_->{to_latlon}
+ name => $_->{to_name} // $_->{arr_name},
+ latlon => $_->{to_latlon} // $_->{arr_latlon},
},
{
- name => $_->{from_name},
- latlon => $_->{from_latlon}
+ name => $_->{from_name} // $_->{dep_name},
+ latlon => $_->{from_latlon} // $_->{dep_latlon}
}
} @journeys;
@@ -2168,8 +2552,8 @@ sub startup {
for my $journey (@polyline_journeys) {
my @polyline = @{ $journey->{polyline} };
- my $from_eva = $journey->{from_eva};
- my $to_eva = $journey->{to_eva};
+ my $from_eva = $journey->{from_eva} // $journey->{dep_eva};
+ my $to_eva = $journey->{to_eva} // $journey->{arr_eva};
my $from_index
= first_index { $_->[2] and $_->[2] == $from_eva } @polyline;
@@ -2205,7 +2589,7 @@ sub startup {
or $to_index == -1 )
{
# Fall back to route
- delete $journey->{polyline};
+ push( @beeline_journeys, $journey );
next;
}
@@ -2217,7 +2601,6 @@ sub startup {
if ( $seen{$key} ) {
next;
}
-
$seen{$key} = 1;
# direction does not matter at the moment
@@ -2227,6 +2610,9 @@ sub startup {
. ( $to_index - $from_index );
$seen{$key} = 1;
+ if ( $from_index > $to_index ) {
+ ( $to_index, $from_index ) = ( $from_index, $to_index );
+ }
@polyline = @polyline[ $from_index .. $to_index ];
my @polyline_coords;
for my $coord (@polyline) {
@@ -2240,13 +2626,19 @@ sub startup {
my @route = @{ $journey->{route} };
my $from_index = first_index {
- ( $_->[1] and $_->[1] == $journey->{from_eva} )
- or $_->[0] eq $journey->{from_name}
+ ( $_->[1]
+ and $_->[1]
+ == ( $journey->{from_eva} // $journey->{dep_eva} ) )
+ or $_->[0] eq
+ ( $journey->{from_name} // $journey->{dep_name} )
}
@route;
my $to_index = first_index {
- ( $_->[1] and $_->[1] == $journey->{to_eva} )
- or $_->[0] eq $journey->{to_name}
+ ( $_->[1]
+ and $_->[1]
+ == ( $journey->{to_eva} // $journey->{arr_eva} ) )
+ or $_->[0] eq
+ ( $journey->{to_name} // $journey->{arr_name} )
}
@route;
@@ -2254,15 +2646,15 @@ sub startup {
my $rename = $self->app->renamed_station;
$from_index = first_index {
( $rename->{ $_->[0] } // $_->[0] ) eq
- $journey->{from_name}
+ ( $journey->{from_name} // $journey->{dep_name} )
}
@route;
}
if ( $to_index == -1 ) {
my $rename = $self->app->renamed_station;
$to_index = first_index {
- ( $rename->{ $_->[0] } // $_->[0] ) eq
- $journey->{to_name}
+ ( $rename->{ $_->[0] } // $_->[0] ) eq
+ ( $journey->{to_name} // $journey->{arr_name} )
}
@route;
}
@@ -2285,7 +2677,8 @@ sub startup {
# and entered manually (-> beeline also shown on map, typically
# significantly differs from detailed route) -- unless the user
# sets include_manual, of course.
- if ( $journey->{edited} & 0x0010
+ if ( $journey->{edited}
+ and $journey->{edited} & 0x0010
and @route <= 2
and not $include_manual )
{
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index f9a2f1e..b328040 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -1,6 +1,7 @@
package Travelynx::Command::database;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
@@ -12,6 +13,7 @@ use JSON;
use Travel::Status::DE::EFA;
use Travel::Status::DE::HAFAS;
use Travel::Status::DE::IRIS::Stations;
+use Travel::Status::MOTIS;
has description => 'Initialize or upgrade database layout';
@@ -2692,6 +2694,338 @@ qq{select distinct checkout_station_id from in_transit where backend_id = 0;}
},
# v58 -> v59
+ # DB HAFAS is dead. Default to DB IRIS for now.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table users alter column backend_id set default 0;
+ update schema_version set version = 59;
+ }
+ );
+ },
+
+ # v59 -> v60
+ # Add bahn.de / DBRIS backend
+ sub {
+ my ($db) = @_;
+ $db->insert(
+ 'backends',
+ {
+ iris => 0,
+ hafas => 0,
+ efa => 0,
+ ris => 1,
+ name => 'bahn.de',
+ },
+ );
+ $db->query(
+ qq{
+ update schema_version set version = 60;
+ }
+ );
+ },
+
+ # v60 -> v61
+ # Rename "ris" / "is_ris" to "dbris" / "is_dbris", as it is DB-specific
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view in_transit_str;
+ drop view journeys_str;
+ drop view users_with_backend;
+ drop view follows_in_transit;
+ alter table backends rename column ris to dbris;
+ create view in_transit_str as select
+ user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.name as backend_name, journeys.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source
+ left join backends as backend on journeys.backend_id = backend.id
+ ;
+ create view users_with_backend as select
+ users.id as id, users.name as name, status, public_level,
+ email, password, registered_at, last_seen,
+ deletion_requested, deletion_notified, use_history,
+ accept_follows, notifications, profile, backend_id, iris,
+ hafas, efa, dbris, backend.name as backend_name
+ from users
+ left join backends as backend on users.backend_id = backend.id
+ ;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ order by checkin_time desc
+ ;
+ update schema_version set version = 61;
+ }
+ );
+ },
+
+ # v61 -> v62
+ # Add MOTIS backend type, add RNV and transitous MOTIS backends
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table backends add column motis bool default false;
+ alter table schema_version add column motis varchar(12);
+
+ create table stations_external_ids (
+ eva serial not null primary key,
+ backend_id smallint not null,
+ external_id text not null,
+
+ unique (backend_id, external_id),
+ foreign key (eva, backend_id) references stations (eva, source)
+ );
+
+ create view stations_with_external_ids as select
+ stations.*, stations_external_ids.external_id
+ from stations
+ left join stations_external_ids on
+ stations.eva = stations_external_ids.eva and
+ stations.source = stations_external_ids.backend_id
+ ;
+
+ drop view in_transit_str;
+ drop view journeys_str;
+ drop view users_with_backend;
+ drop view follows_in_transit;
+
+ create view in_transit_str as select
+ user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ dep_station_external_id.external_id as dep_external_id,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ arr_station_external_id.external_id as arr_external_id,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and in_transit.backend_id = dep_station_external_id.backend_id
+ left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and in_transit.backend_id = arr_station_external_id.backend_id
+ left join backends as backend on in_transit.backend_id = backend.id
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, journeys.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ dep_station_external_id.external_id as dep_external_id,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ arr_station_external_id.external_id as arr_external_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source
+ left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and journeys.backend_id = dep_station_external_id.backend_id
+ left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and journeys.backend_id = arr_station_external_id.backend_id
+ left join backends as backend on journeys.backend_id = backend.id
+ ;
+ create view users_with_backend as select
+ users.id as id, users.name as name, status, public_level,
+ email, password, registered_at, last_seen,
+ deletion_requested, deletion_notified, use_history,
+ accept_follows, notifications, profile, backend_id, iris,
+ hafas, efa, dbris, motis, backend.name as backend_name
+ from users
+ left join backends as backend on users.backend_id = backend.id
+ ;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ order by checkin_time desc
+ ;
+ }
+ );
+ $db->query(
+ qq{
+ update schema_version set version = 62;
+ }
+ );
+ },
+
+ # v62 -> v63
# Add EFA backend support
sub {
my ($db) = @_;
@@ -2893,7 +3227,7 @@ sub sync_stations {
}
}
-sub sync_efa {
+sub sync_backends_efa {
my ($db) = @_;
for my $service ( Travel::Status::DE::EFA::get_services() ) {
my $present = $db->select(
@@ -2923,7 +3257,7 @@ sub sync_efa {
{ efa => $Travel::Status::DE::EFA::VERSION } );
}
-sub sync_hafas {
+sub sync_backends_hafas {
my ($db) = @_;
for my $service ( Travel::Status::DE::HAFAS::get_services() ) {
my $present = $db->select(
@@ -2941,7 +3275,7 @@ sub sync_hafas {
iris => 0,
hafas => 1,
efa => 0,
- ris => 0,
+ dbris => 0,
name => $service->{shortname},
},
{ on_conflict => undef }
@@ -2953,6 +3287,37 @@ sub sync_hafas {
{ hafas => $Travel::Status::DE::HAFAS::VERSION } );
}
+sub sync_backends_motis {
+ my ($db) = @_;
+ for my $service ( Travel::Status::MOTIS::get_services() ) {
+ my $present = $db->select(
+ 'backends',
+ 'count(*) as count',
+ {
+ motis => 1,
+ name => $service->{shortname}
+ }
+ )->hash->{count};
+ if ( not $present ) {
+ $db->insert(
+ 'backends',
+ {
+ iris => 0,
+ hafas => 0,
+ efa => 0,
+ dbris => 0,
+ motis => 1,
+ name => $service->{shortname},
+ },
+ { on_conflict => undef }
+ );
+ }
+ }
+
+ $db->update( 'schema_version',
+ { motis => $Travel::Status::MOTIS::VERSION } );
+}
+
sub setup_db {
my ($db) = @_;
my $tx = $db->begin;
@@ -3048,7 +3413,7 @@ sub migrate_db {
else {
say
"Synchronizing with Travel::Status::DE::EFA $Travel::Status::DE::EFA::VERSION";
- sync_efa($db);
+ sync_backends_efa($db);
}
my $hafas_version = get_schema_version( $db, 'hafas' );
@@ -3059,7 +3424,18 @@ sub migrate_db {
else {
say
"Synchronizing with Travel::Status::DE::HAFAS $Travel::Status::DE::HAFAS::VERSION";
- sync_hafas($db);
+ sync_backends_hafas($db);
+ }
+
+ my $motis_version = get_schema_version( $db, 'motis' ) // '0';
+ say "Found backend table for Motis v${motis_version}";
+ if ( $motis_version eq $Travel::Status::MOTIS::VERSION ) {
+ say 'Backend table is up-to-date';
+ }
+ else {
+ say
+"Synchronizing with Travel::Status::MOTIS $Travel::Status::MOTIS::VERSION";
+ sync_backends_motis($db);
}
$db->update( 'schema_version',
diff --git a/lib/Travelynx/Command/traewelling.pm b/lib/Travelynx/Command/traewelling.pm
index ed40371..e4e0134 100644
--- a/lib/Travelynx/Command/traewelling.pm
+++ b/lib/Travelynx/Command/traewelling.pm
@@ -122,12 +122,12 @@ sub push_sync {
my ($status) = @_;
$push_result{ $status->{http} } += 1;
}
- )->catch(
+ )->catch(
sub {
my ($status) = @_;
$push_result{ $status->{http} // 0 } += 1;
}
- )->wait;
+ )->wait;
}
return \%push_result;
diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm
index 98f478a..2b01cb2 100644
--- a/lib/Travelynx/Command/work.pm
+++ b/lib/Travelynx/Command/work.pm
@@ -1,11 +1,14 @@
package Travelynx::Command::work;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
use Mojo::Promise;
+use utf8;
+
use DateTime;
use JSON;
use List::Util;
@@ -33,7 +36,10 @@ sub run {
$self->app->log->debug("Removed ${num_incomplete} incomplete checkins");
}
- my $errors = 0;
+ my $errors = 0;
+ my $backend_issues = 0;
+ my $rate_limit_counts = 0;
+ my $dbris_rate_limited = 0;
for my $entry ( $self->app->in_transit->get_all_active ) {
@@ -47,6 +53,235 @@ sub run {
my $arr = $entry->{arr_eva};
my $train_id = $entry->{train_id};
+ if ( $entry->{is_dbris} ) {
+
+ eval {
+
+ Mojo::Promise->timer( $dbris_rate_limited ? 4.5 : 1.0 )->then(
+ sub {
+ return $self->app->dbris->get_journey_p(
+ trip_id => $train_id );
+ }
+ )->then(
+ sub {
+ my ($journey) = @_;
+
+ $dbris_rate_limited = 0;
+
+ my $found_dep;
+ my $found_arr;
+ for my $stop ( $journey->route ) {
+ if ( $stop->eva == $dep ) {
+ $found_dep = $stop;
+ }
+ if ( $arr and $stop->eva == $arr ) {
+ $found_arr = $stop;
+ last;
+ }
+ }
+ if ( not $found_dep ) {
+ $self->app->log->debug(
+ "Did not find $dep within journey $train_id");
+ return;
+ }
+
+ if ( $found_dep->rt_dep ) {
+ $self->app->in_transit->update_departure_dbris(
+ uid => $uid,
+ journey => $journey,
+ stop => $found_dep,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ train_id => $train_id,
+ );
+ }
+ if ( $found_dep->sched_dep
+ and $found_dep->dep->epoch > $now->epoch )
+ {
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $train_id,
+ is_departure => 1,
+ eva => $dep,
+ datetime => $found_dep->sched_dep,
+ train_type => $journey->type,
+ train_no => $journey->number,
+ );
+ $self->app->add_stationinfo( $uid, 1,
+ $train_id, $found_dep->eva );
+ }
+
+ if ( $found_arr and $found_arr->rt_arr ) {
+ $self->app->in_transit->update_arrival_dbris(
+ uid => $uid,
+ journey => $journey,
+ train_id => $train_id,
+ stop => $found_arr,
+ dep_eva => $dep,
+ arr_eva => $arr
+ );
+ if ( $found_arr->arr->epoch - $now->epoch < 600 ) {
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $train_id,
+ is_arrival => 1,
+ eva => $arr,
+ datetime => $found_arr->sched_dep,
+ train_type => $journey->type,
+ train_no => $journey->number,
+ );
+ $self->app->add_stationinfo( $uid, 0,
+ $train_id, $found_dep->eva,
+ $found_arr->eva );
+ }
+ }
+ if ( $found_arr and $found_arr->is_cancelled ) {
+
+ # check out (adds a cancelled journey and resets journey state
+ # to destination selection)
+ $self->app->checkout_p(
+ station => $arr,
+ force => 0,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->app->log->debug(
+"work($uid) @ DBRIS $entry->{backend_name}: journey: $err"
+ );
+ if ( $err =~ m{HTTP 429} ) {
+ $dbris_rate_limited = 1;
+ $rate_limit_counts += 1;
+ }
+ else {
+ $backend_issues += 1;
+ }
+ }
+ )->wait;
+
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 600 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ };
+ if ($@) {
+ $errors += 1;
+ $self->app->log->error(
+ "work($uid) @ DBRIS $entry->{backend_name}: $@");
+ }
+ next;
+ }
+
+ if ( $entry->{is_motis} ) {
+
+ eval {
+ $self->app->motis->get_trip_p(
+ service => $entry->{backend_name},
+ trip_id => $train_id,
+ )->then(
+ sub {
+ my ($journey) = @_;
+
+ for my $stopover ( $journey->stopovers ) {
+ if ( not defined $stopover->stop->{eva} ) {
+
+ # Looks like MOTIS / transitous station IDs can change after the fact.
+ # So let's be safe rather than sorry, even if this causes way too many calls to the slow path
+ # (Stations::get_by_external_id uses string lookups).
+ # This function call implicitly sets $stopover->stop->{eva} for MOTIS backends.
+ $self->app->stations->add_or_update(
+ stop => $stopover->stop,
+ motis => $entry->{backend_name},
+ );
+ }
+ }
+
+ my $found_departure;
+ my $found_arrival;
+ for my $stopover ( $journey->stopovers ) {
+ if ( $stopover->stop->{eva} == $dep ) {
+ $found_departure = $stopover;
+ }
+
+ if ( $arr and $stopover->stop->{eva} == $arr ) {
+ $found_arrival = $stopover;
+ last;
+ }
+ }
+
+ if ( not $found_departure ) {
+ $self->app->log->debug(
+ "Did not find $dep within trip $train_id");
+ return;
+ }
+
+ if ( $found_departure->realtime_departure ) {
+ $self->app->in_transit->update_departure_motis(
+ uid => $uid,
+ journey => $journey,
+ stopover => $found_departure,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ train_id => $train_id,
+ );
+ }
+
+ if ( $found_arrival
+ and $found_arrival->realtime_arrival )
+ {
+ $self->app->in_transit->update_arrival_motis(
+ uid => $uid,
+ journey => $journey,
+ train_id => $train_id,
+ stopover => $found_arrival,
+ dep_eva => $dep,
+ arr_eva => $arr
+ );
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->app->log->error(
+"work($uid) @ MOTIS $entry->{backend_name}: journey: $err"
+ );
+ }
+ )->wait;
+
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 600 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ };
+ if ($@) {
+ $errors += 1;
+ $self->app->log->error(
+ "work($uid) @ MOTIS $entry->{backend_name}: $@");
+ }
+ next;
+ }
+
if ( $entry->{is_hafas} ) {
eval {
@@ -83,22 +318,27 @@ sub run {
dep_eva => $dep,
arr_eva => $arr
);
- if ( $entry->{backend_id} <= 1
- and $journey->class <= 16
- and $found_dep->rt_dep->epoch > $now->epoch )
- {
- $self->app->add_wagonorder(
- uid => $uid,
- train_id => $journey->id,
- is_departure => 1,
- eva => $dep,
- datetime => $found_dep->sched_dep,
- train_type => $journey->type,
- train_no => $journey->number,
- );
- $self->app->add_stationinfo( $uid, 1,
- $journey->id, $found_dep->loc->eva );
- }
+ }
+ if (
+ $found_dep->sched_dep
+ and ( $entry->{backend_id} <= 1
+ or $entry->{backend_name} eq 'VRN'
+ or $entry->{backend_name} eq 'ÖBB' )
+ and $journey->class <= 16
+ and $found_dep->dep->epoch > $now->epoch
+ )
+ {
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $journey->id,
+ is_departure => 1,
+ eva => $dep,
+ datetime => $found_dep->sched_dep,
+ train_type => $journey->type =~ s{ +$}{}r,
+ train_no => $journey->number,
+ );
+ $self->app->add_stationinfo( $uid, 1,
+ $journey->id, $found_dep->loc->eva );
}
if ( $found_arr and $found_arr->rt_arr ) {
@@ -109,10 +349,15 @@ sub run {
dep_eva => $dep,
arr_eva => $arr
);
- if ( $entry->{backend_id} <= 1
+ if (
+ (
+ $entry->{backend_id} <= 1
+ or $entry->{backend_name} eq 'VRN'
+ or $entry->{backend_name} eq 'ÖBB'
+ )
and $journey->class <= 16
- and $found_arr->rt_arr->epoch - $now->epoch
- < 600 )
+ and $found_arr->arr->epoch - $now->epoch < 600
+ )
{
$self->app->add_wagonorder(
uid => $uid,
@@ -132,10 +377,12 @@ sub run {
)->catch(
sub {
my ($err) = @_;
+ $backend_issues += 1;
if ( $err
- =~ m{svcResL\[0\][.]err is (?:FAIL|PARAMETER)$} )
+ =~ m{svcResL\[0\][.]err is (?:FAIL|PARAMETER)$}
+ or $err =~ m{timeout} )
{
- # HAFAS do be weird. These are not actionable.
+ # These are not actionable.
$self->app->log->debug(
"work($uid) @ HAFAS $entry->{backend_name}: journey: $err"
);
@@ -340,6 +587,7 @@ sub run {
)->catch(
sub {
my ($error) = @_;
+ $backend_issues += 1;
$self->app->log->error(
"work($uid) @ IRIS: arrival: $error");
$errors += 1;
@@ -363,13 +611,13 @@ sub run {
if ( $self->app->mode eq 'development' ) {
$self->app->log->debug( 'POST '
. $self->app->config->{influxdb}->{url}
- . " worker runtime_seconds=${worker_duration},errors=${errors}"
+ . " worker runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}"
);
}
else {
$self->app->ua->post_p( $self->app->config->{influxdb}->{url},
- "worker runtime_seconds=${worker_duration},errors=${errors}" )
- ->wait;
+"worker runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}"
+ )->wait;
}
}
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index 97a3cf6..74c272f 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -1,6 +1,7 @@
package Travelynx::Controller::Account;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
@@ -1066,8 +1067,15 @@ sub backend_form {
if ( $backend->{iris} ) {
$type = 'IRIS-TTS';
$backend->{name} = 'IRIS';
- $backend->{longname} = 'Deutsche Bahn (IRIS-TTS)';
+ $backend->{longname} = 'Deutsche Bahn: IRIS-TTS';
$backend->{homepage} = 'https://www.bahn.de';
+ $backend->{legacy} = 1;
+ }
+ elsif ( $backend->{dbris} ) {
+ $type = 'DBRIS';
+ $backend->{longname} = 'Deutsche Bahn: bahn.de';
+ $backend->{homepage} = 'https://www.bahn.de';
+ $backend->{recommended} = 1;
}
elsif ( $backend->{efa} ) {
if ( my $s = $self->efa->get_service( $backend->{name} ) ) {
@@ -1112,7 +1120,28 @@ sub backend_form {
}
}
elsif ( $backend->{hafas} ) {
- if ( my $s = $self->hafas->get_service( $backend->{name} ) ) {
+
+ # These backends lack a journey endpoint or are no longer
+ # operational and are thus useless for travelynx
+ if ( $backend->{name} eq 'Resrobot'
+ or $backend->{name} eq 'TPG'
+ or $backend->{name} eq 'VRN'
+ or $backend->{name} eq 'DB' )
+ {
+ $type = undef;
+ }
+
+ # PKP is behind a GeoIP filter. Only list it if travelynx.conf
+ # indicates that our IP is allowed or provides a proxy.
+ elsif (
+ $backend->{name} eq 'PKP'
+ and not( $self->app->config->{hafas}{PKP}{geoip_ok}
+ or $self->app->config->{hafas}{PKP}{proxy} )
+ )
+ {
+ $type = undef;
+ }
+ elsif ( my $s = $self->hafas->get_service( $backend->{name} ) ) {
$type = 'HAFAS';
$backend->{longname} = $s->{name};
$backend->{homepage} = $s->{homepage};
@@ -1120,6 +1149,13 @@ sub backend_form {
@{ $s->{coverage}{regions} // [] } ];
$backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+ if ( $backend->{name} eq 'ÖBB' ) {
+ $backend->{recommended} = 1;
+ }
+ else {
+ $backend->{association} = 1;
+ }
+
if (
$s->{coverage}{area}
and $s->{coverage}{area}{type} eq 'Polygon'
@@ -1153,19 +1189,58 @@ sub backend_form {
$type = undef;
}
}
+ elsif ( $backend->{motis} ) {
+ my $s = $self->motis->get_service( $backend->{name} );
+
+ $type = 'MOTIS';
+ $backend->{longname} = $s->{name};
+ $backend->{homepage} = $s->{homepage};
+ $backend->{regions} = [ map { $place_map{$_} // $_ }
+ @{ $s->{coverage}{regions} // [] } ];
+ $backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+ $backend->{experimental} = 1;
+
+ if ( $backend->{name} eq 'transitous' ) {
+ $backend->{regions} = ['Weltweit'];
+ }
+ if ( $backend->{name} eq 'RNV' ) {
+ $backend->{homepage} = 'https://rnv-online.de/';
+ }
+
+ if (
+ $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'Polygon'
+ and $self->lonlat_in_polygon(
+ $s->{coverage}{area}{coordinates},
+ [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ }
+ elsif ( $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'MultiPolygon' )
+ {
+ for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } )
+ {
+ if (
+ $self->lonlat_in_polygon(
+ $s_poly, [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ last;
+ }
+ }
+ }
+ }
$backend->{type} = $type;
}
- # These backends lack a journey endpoint and are useless for travelynx
- @backends
- = grep { $_->{name} ne 'Resrobot' and $_->{name} ne 'TPG' } @backends;
-
- my $iris = shift @backends;
-
- @backends
- = sort { $a->{name} cmp $b->{name} } grep { $_->{type} } @backends;
-
- unshift( @backends, $iris );
+ @backends = map { $_->[1] }
+ sort { $a->[0] cmp $b->[0] }
+ map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends;
$self->render(
'select_backend',
diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm
index f31195a..572d3fa 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -21,6 +21,9 @@ sub sanitize {
if ( not defined $value ) {
return undef;
}
+ if ( not defined $type ) {
+ return $value ? ( '' . $value ) : undef;
+ }
if ( $type eq '' ) {
return '' . $value;
}
@@ -184,8 +187,13 @@ sub travel_v1 {
my $from_station = sanitize( q{}, $payload->{fromStation} );
my $to_station = sanitize( q{}, $payload->{toStation} );
my $train_id;
+ my $dbris = sanitize( undef, $payload->{dbris} );
my $hafas = sanitize( undef, $payload->{hafas} );
- $hafas //= exists $payload->{train}{journeyID} ? 'DB' : undef;
+ my $motis = sanitize( undef, $payload->{motis} );
+
+ if ( not $hafas and exists $payload->{train}{journeyID} ) {
+ $dbris //= 'bahn.de';
+ }
if (
not(
@@ -209,6 +217,7 @@ sub travel_v1 {
}
if ( not $hafas
+ and not $dbris
and not $self->stations->search( $from_station, backend_id => 1 ) )
{
$self->render(
@@ -225,6 +234,7 @@ sub travel_v1 {
if ( $to_station
and not $hafas
+ and not $dbris
and not $self->stations->search( $to_station, backend_id => 1 ) )
{
$self->render(
@@ -288,6 +298,8 @@ sub travel_v1 {
train_id => $train_id,
uid => $uid,
hafas => $hafas,
+ dbris => $dbris,
+ motis => $motis,
);
}
)->then(
diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm
index c35642d..db30d36 100755
--- a/lib/Travelynx/Controller/Profile.pm
+++ b/lib/Travelynx/Controller/Profile.pm
@@ -111,6 +111,13 @@ sub profile {
$status->{arr_name} = undef;
}
+ my $map_data = {};
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
+
my @journeys;
if (
@@ -190,6 +197,8 @@ sub profile {
: 0,
journey => $status,
journeys => [@journeys],
+ with_map => 1,
+ %{$map_data},
}
);
}
@@ -494,6 +503,13 @@ sub user_status {
$og_data{description} = $tw_data{description} = q{};
}
+ my $map_data = {};
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
+
$self->respond_to(
json => {
json => {
@@ -516,7 +532,9 @@ sub user_status {
journey => $status,
twitter => \%tw_data,
opengraph => \%og_data,
- version => $self->app->config->{version} // 'UNKNOWN',
+ with_map => 1,
+ %{$map_data},
+ version => $self->app->config->{version} // 'UNKNOWN',
},
);
}
@@ -555,6 +573,7 @@ sub status_card {
my $status = $self->get_user_status( $user->{id} );
my $visibility;
+ my $map_data = {};
if ( $status->{checked_in} or $status->{arr_name} ) {
my $visibility = $status->{effective_visibility};
if (
@@ -579,12 +598,19 @@ sub status_card {
$status->{arr_name} = undef;
}
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
+
$self->render(
'_public_status_card',
name => $name,
privacy => $user,
journey => $status,
from_profile => $self->param('profile') ? 1 : 0,
+ %{$map_data},
);
}
diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm
index 3cdeff8..6aa789c 100644
--- a/lib/Travelynx/Controller/Traewelling.pm
+++ b/lib/Travelynx/Controller/Traewelling.pm
@@ -29,7 +29,7 @@ sub oauth {
redirect_uri =>
$self->base_url_for('/oauth/traewelling')->to_abs->scheme(
$self->app->mode eq 'development' ? 'http' : 'https'
- )->to_string,
+ )->to_string,
scope => 'read-statuses write-statuses'
}
)->then(
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index 53d28a9..de1a547 100755
--- a/lib/Travelynx/Controller/Traveling.pm
+++ b/lib/Travelynx/Controller/Traveling.pm
@@ -1,6 +1,7 @@
package Travelynx::Controller::Traveling;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
@@ -10,6 +11,7 @@ use DateTime::Format::Strptime;
use List::Util qw(uniq min max);
use List::UtilsBy qw(max_by uniq_by);
use List::MoreUtils qw(first_index);
+use Mojo::UserAgent;
use Mojo::Promise;
use Text::CSV;
use Travel::Status::DE::IRIS::Stations;
@@ -41,6 +43,18 @@ sub get_connecting_trains_p {
my $promise = Mojo::Promise->new;
+ if ( $user->{backend_dbris} ) {
+
+ # We do get a little bit of via information, so this might work in some
+ # cases. But not reliably. Probably best to leave it out entirely then.
+ return $promise->reject;
+ }
+ if ( $user->{backend_motis} ) {
+
+ # FIXME: The following code can't handle external_ids currently
+ return $promise->reject;
+ }
+
if ( $opt{eva} ) {
if ( $use_history & 0x01 ) {
$eva = $opt{eva};
@@ -105,6 +119,8 @@ sub get_connecting_trains_p {
my $iris_promise = Mojo::Promise->new;
my %via_count = map { $_->{name} => 0 } @destinations;
+ my $backend
+ = $self->stations->get_backend( backend_id => $opt{backend_id} );
if ( $opt{backend_id} == 0 ) {
$self->iris->get_departures_p(
station => $eva,
@@ -259,9 +275,11 @@ sub get_connecting_trains_p {
}
)->wait;
}
- else {
- my $hafas_service
- = $self->stations->get_hafas_name( backend_id => $opt{backend_id} );
+ elsif ( $backend->{dbris} ) {
+ return $promise->reject;
+ }
+ elsif ( $backend->{hafas} ) {
+ my $hafas_service = $backend->{name};
$self->hafas->get_departures_p(
service => $hafas_service,
eva => $eva,
@@ -341,6 +359,12 @@ sub homepage {
$self->stash( timeline => [@timeline] );
my @recent_targets;
if ( $status->{checked_in} ) {
+ my $map_data = {};
+ if ( $status->{arr_name} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
my $journey_visibility
= $self->compute_effective_visibility(
$user->{default_visibility_str},
@@ -359,6 +383,8 @@ sub homepage {
journey_visibility => $journey_visibility,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
}
@@ -369,6 +395,8 @@ sub homepage {
user => $user,
user_status => $status,
journey_visibility => $journey_visibility,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
}
@@ -381,13 +409,15 @@ sub homepage {
user => $user,
user_status => $status,
journey_visibility => $journey_visibility,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
return;
}
}
else {
- @recent_targets = uniq_by { $_->{eva} }
+ @recent_targets = uniq_by { $_->{external_id_or_eva} }
$self->journeys->get_latest_checkout_stations( uid => $uid );
}
$self->render(
@@ -419,6 +449,12 @@ sub status_card {
$self->stash( timeline => [@timeline] );
if ( $status->{checked_in} ) {
+ my $map_data = {};
+ if ( $status->{arr_name} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
my $journey_visibility
= $self->compute_effective_visibility(
$self->current_user->{default_visibility_str},
@@ -436,6 +472,7 @@ sub status_card {
journey_visibility => $journey_visibility,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
+ %{$map_data},
);
}
)->catch(
@@ -444,6 +481,7 @@ sub status_card {
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
+ %{$map_data},
);
}
)->wait;
@@ -453,6 +491,7 @@ sub status_card {
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
+ %{$map_data},
);
}
elsif ( $status->{cancellation} ) {
@@ -523,15 +562,74 @@ sub geolocation {
return;
}
- my $hafas_service
- = $self->stations->get_hafas_name( backend_id => $backend_id );
+ my ( $dbris_service, $hafas_service, $motis_service );
+ my $backend = $self->stations->get_backend( backend_id => $backend_id );
+ if ( $backend->{dbris} ) {
+ $dbris_service = $backend->{name};
+ }
+ elsif ( $backend->{hafas} ) {
+ $hafas_service = $backend->{name};
+ }
+ elsif ( $backend->{motis} ) {
+ $motis_service = $backend->{name};
+ }
- if ($hafas_service) {
+ if ($dbris_service) {
$self->render_later;
+ Travel::Status::DE::DBRIS->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ geoSearch => {
+ latitude => $lat,
+ longitude => $lon
+ }
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ my @results = map {
+ {
+ name => $_->name,
+ eva => $_->eva,
+ distance => 0,
+ dbris => $dbris_service,
+ }
+ } $dbris->results;
+ if ( @results > 10 ) {
+ @results = @results[ 0 .. 9 ];
+ }
+ $self->render(
+ json => {
+ candidates => [@results],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ warning => $err,
+ }
+ );
+ }
+ )->wait;
+ return;
+ }
+ elsif ($hafas_service) {
+ $self->render_later;
+
+ my $agent = $self->ua;
+ if ( my $proxy = $self->app->config->{hafas}{$hafas_service}{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
Travel::Status::DE::HAFAS->new_p(
promise => 'Mojo::Promise',
- user_agent => $self->ua,
+ user_agent => $agent,
service => $hafas_service,
geoSearch => {
lat => $lat,
@@ -571,6 +669,54 @@ sub geolocation {
return;
}
+ elsif ($motis_service) {
+ $self->render_later;
+
+ Travel::Status::MOTIS->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => $self->ua,
+
+ service => $motis_service,
+ stops_by_coordinate => {
+ lat => $lat,
+ lon => $lon
+ }
+ )->then(
+ sub {
+ my ($motis) = @_;
+ my @motis = map {
+ {
+ id => $_->id,
+ name => $_->name,
+ distance => 0,
+ motis => $motis_service,
+ }
+ } $motis->results;
+
+ if ( @motis > 10 ) {
+ @motis = @motis[ 0 .. 9 ];
+ }
+
+ $self->render(
+ json => {
+ candidates => [@motis],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ warning => $err,
+ }
+ );
+ }
+ )->wait;
+
+ return;
+ }
my @iris = map {
{
@@ -580,6 +726,7 @@ sub geolocation {
lon => $_->[0][3],
lat => $_->[0][4],
distance => $_->[1],
+ dbris => 0,
hafas => 0,
}
} Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
@@ -648,10 +795,13 @@ sub travel_action {
$promise->then(
sub {
return $self->checkin_p(
- hafas => $params->{hafas},
- station => $params->{station},
- train_id => $params->{train},
- ts => $params->{ts},
+ dbris => $params->{dbris},
+ hafas => $params->{hafas},
+ motis => $params->{motis},
+ station => $params->{station},
+ train_id => $params->{train},
+ train_suffix => $params->{suffix},
+ ts => $params->{ts},
);
}
)->then(
@@ -679,7 +829,10 @@ sub travel_action {
my ( $still_checked_in, undef ) = @_;
if ( my $destination = $params->{dest} ) {
my $station_link = '/s/' . $destination;
- if ( $status->{is_hafas} ) {
+ if ( $status->{is_dbris} ) {
+ $station_link .= '?dbris=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
$station_link .= '?hafas=' . $status->{backend_name};
}
$self->render(
@@ -715,7 +868,10 @@ sub travel_action {
sub {
my ( $still_checked_in, $error ) = @_;
my $station_link = '/s/' . $params->{station};
- if ( $status->{is_hafas} ) {
+ if ( $status->{is_dbris} ) {
+ $station_link .= '?dbris=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
$station_link .= '?hafas=' . $status->{backend_name};
}
@@ -766,13 +922,27 @@ sub travel_action {
else {
my $redir = '/';
if ( $status->{checked_in} or $status->{cancelled} ) {
- if ( $status->{is_hafas} ) {
+ if ( $status->{is_dbris} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_eva}
+ . '?dbris='
+ . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
$redir
= '/s/'
. $status->{dep_eva}
. '?hafas='
. $status->{backend_name};
}
+ elsif ( $status->{is_motis} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_external_id}
+ . '?motis='
+ . $status->{backend_name};
+ }
else {
$redir = '/s/' . $status->{dep_ds100};
}
@@ -788,7 +958,9 @@ sub travel_action {
elsif ( $params->{action} eq 'cancelled_from' ) {
$self->render_later;
$self->checkin_p(
+ dbris => $params->{dbris},
hafas => $params->{hafas},
+ motis => $params->{motis},
station => $params->{station},
train_id => $params->{train},
ts => $params->{ts},
@@ -917,12 +1089,38 @@ sub station {
$timestamp = DateTime->now( time_zone => 'Europe/Berlin' );
}
+ my $dbris_service = $self->param('dbris')
+ // ( $user->{backend_dbris} ? $user->{backend_name} : undef );
my $efa_service = $self->param('efa')
// ( $user->{backend_efa} ? $user->{backend_name} : undef );
my $hafas_service = $self->param('hafas')
// ( $user->{backend_hafas} ? $user->{backend_name} : undef );
+ my $motis_service = $self->param('motis')
+ // ( $user->{backend_motis} ? $user->{backend_name} : undef );
my $promise;
- if ($hafas_service) {
+ if ($dbris_service) {
+ if ( $station !~ m{ [@] L = \d+ }x ) {
+ $self->render_later;
+ $self->dbris->get_station_id_p($station)->then(
+ sub {
+ my ($dbris_station) = @_;
+ $self->redirect_to( '/s/' . $dbris_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
+ return;
+ }
+ $promise = $self->dbris->get_departures_p(
+ station => $station,
+ timestamp => $timestamp,
+ lookbehind => 30,
+ );
+ }
+ elsif ($hafas_service) {
$promise = $self->hafas->get_departures_p(
service => $hafas_service,
eva => $station,
@@ -936,7 +1134,36 @@ sub station {
service => $efa_service,
name => $station,
timestamp => $timestamp,
- lookbehind => 10,
+ lookbehind => 30,
+ lookahead => 30,
+ );
+ }
+ elsif ($motis_service) {
+ if ( $station !~ m/.*_.*/ ) {
+ $self->render_later;
+ $self->motis->get_station_by_query_p(
+ service => $motis_service,
+ query => $station,
+ )->then(
+ sub {
+ my ($motis_station) = @_;
+ $self->redirect_to( '/s/' . $motis_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ say "$err";
+
+ $self->redirect_to('/');
+ }
+ )->wait;
+ return;
+ }
+ $promise = $self->motis->get_departures_p(
+ service => $motis_service,
+ station_id => $station,
+ timestamp => $timestamp,
+ lookbehind => 30,
lookahead => 30,
);
}
@@ -957,7 +1184,22 @@ sub station {
my $now_within_range
= abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0;
- if ($hafas_service) {
+ if ($dbris_service) {
+
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->dep->epoch ] } $status->results;
+
+ $status = {
+ station_eva => $station,
+ related_stations => [],
+ };
+
+ if ( $station =~ m{ [@] O = (?<name> [^@]+ ) [@] }x ) {
+ $status->{station_name} = $+{name};
+ }
+ }
+ elsif ($hafas_service) {
@results = map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
@@ -988,6 +1230,19 @@ sub station {
related_stations => [],
};
}
+ elsif ($motis_service) {
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->stopover->departure->epoch ] }
+ $status->results;
+
+ $status = {
+ station_eva => $station,
+ station_name =>
+ $status->{results}->[0]->stopover->stop->name,
+ related_stations => [],
+ };
+ }
else {
# You can't check into a train which terminates here
@@ -1054,8 +1309,10 @@ sub station {
$self->render(
'departures',
user => $user,
+ dbris => $dbris_service,
efa => $efa_service,
hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
@@ -1074,8 +1331,10 @@ sub station {
$self->render(
'departures',
user => $user,
+ dbris => $dbris_service,
efa => $efa_service,
hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
@@ -1093,8 +1352,10 @@ sub station {
$self->render(
'departures',
user => $user,
+ dbris => $dbris_service,
efa => $efa_service,
hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
@@ -1157,18 +1418,23 @@ sub station {
}
)->wait;
}
- elsif ( $err =~ m{svcRes|connection close} ) {
+ elsif ( $err
+ =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden}
+ )
+ {
$self->render(
'bad_gateway',
- message => $err,
- status => 502
+ message => $err,
+ status => 502,
+ select_new_backend => 1,
);
}
elsif ( $err =~ m{timeout}i ) {
$self->render(
'gateway_timeout',
- message => $err,
- status => 504
+ message => $err,
+ status => 504,
+ select_new_backend => 1,
);
}
else {
@@ -1187,7 +1453,40 @@ sub redirect_to_station {
my ($self) = @_;
my $station = $self->param('station');
- $self->redirect_to("/s/${station}");
+ if ( $self->param('backend_dbris') ) {
+ $self->render_later;
+ $self->dbris->get_station_id_p($station)->then(
+ sub {
+ my ($dbris_station) = @_;
+ $self->redirect_to( '/s/' . $dbris_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
+ }
+ elsif ( $self->param('backend_motis') ) {
+ $self->render_later;
+ $self->motis->get_station_by_query(
+ service => $self->param('backend_motis'),
+ query => $station,
+ )->then(
+ sub {
+ my ($motis_station) = @_;
+ $self->redirect_to( '/s/' . $motis_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
+ }
+ else {
+ $self->redirect_to("/s/${station}");
+ }
}
sub cancelled {
diff --git a/lib/Travelynx/Helper/DBDB.pm b/lib/Travelynx/Helper/DBDB.pm
index 79e9c0b..a310aa3 100644
--- a/lib/Travelynx/Helper/DBDB.pm
+++ b/lib/Travelynx/Helper/DBDB.pm
@@ -61,7 +61,8 @@ sub has_wagonorder_p {
}
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ $self->{user_agent}->request_timeout(5)
+ ->get_p( $url => $self->{header} )
->then(
sub {
my ($tx) = @_;
@@ -81,7 +82,7 @@ sub has_wagonorder_p {
}
return;
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$self->{log}->debug("${debug_prefix}: n ($err)");
@@ -89,7 +90,7 @@ sub has_wagonorder_p {
$promise->reject;
return;
}
- )->wait;
+ )->wait;
return $promise;
}
@@ -120,7 +121,8 @@ sub get_wagonorder_p {
return $promise;
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ $self->{user_agent}->request_timeout(5)
+ ->get_p( $url => $self->{header} )
->then(
sub {
my ($tx) = @_;
@@ -139,14 +141,14 @@ sub get_wagonorder_p {
}
return;
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$self->{log}->debug("${debug_prefix}: error ${err}");
$promise->reject($err);
return;
}
- )->wait;
+ )->wait;
return $promise;
}
@@ -163,7 +165,8 @@ sub get_stationinfo_p {
return $promise->resolve($content);
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ $self->{user_agent}->request_timeout(5)
+ ->get_p( $url => $self->{header} )
->then(
sub {
my ($tx) = @_;
@@ -183,7 +186,7 @@ sub get_stationinfo_p {
$promise->resolve($json);
return;
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$self->{log}->debug("get_stationinfo_p(${eva}): Error ${err}");
@@ -191,7 +194,7 @@ sub get_stationinfo_p {
$promise->reject($err);
return;
}
- )->wait;
+ )->wait;
return $promise;
}
diff --git a/lib/Travelynx/Helper/DBRIS.pm b/lib/Travelynx/Helper/DBRIS.pm
new file mode 100644
index 0000000..0a46758
--- /dev/null
+++ b/lib/Travelynx/Helper/DBRIS.pm
@@ -0,0 +1,147 @@
+package Travelynx::Helper::DBRIS;
+
+# Copyright (C) 2025 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+use utf8;
+
+use DateTime;
+use Encode qw(decode);
+use JSON;
+use Mojo::Promise;
+use Mojo::UserAgent;
+use Travel::Status::DE::DBRIS;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header}
+ = { 'User-Agent' =>
+"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx"
+ };
+
+ return bless( \%opt, $class );
+}
+
+sub get_station_id_p {
+ my ( $self, $station_name ) = @_;
+ my $promise = Mojo::Promise->new;
+ Travel::Status::DE::DBRIS->new_p(
+ locationSearch => $station_name,
+ cache => $self->{cache},
+ lwp_options => {
+ timeout => 10,
+ agent => $self->{header}{'User-Agent'},
+ },
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ my $found;
+ for my $result ( $dbris->results ) {
+ if ( defined $result->eva ) {
+ $promise->resolve($result);
+ return;
+ }
+ }
+ $promise->reject("Unable to find station '$station_name'");
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject("'$err' while trying to look up '$station_name'");
+ return;
+ }
+ )->wait;
+ return $promise;
+}
+
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
+
+ my $agent = $self->{user_agent};
+
+ if ( $opt{station} =~ m{ [@] L = (?<eva> \d+ ) [@] }x ) {
+ $opt{station} = {
+ eva => $+{eva},
+ id => $opt{station},
+ };
+ }
+
+ my $when = (
+ $opt{timestamp}
+ ? $opt{timestamp}->clone
+ : DateTime->now( time_zone => 'Europe/Berlin' )
+ )->subtract( minutes => $opt{lookbehind} );
+ return Travel::Status::DE::DBRIS->new_p(
+ station => $opt{station},
+ datetime => $when,
+ cache => $self->{cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10),
+ );
+}
+
+sub get_journey_p {
+ my ( $self, %opt ) = @_;
+
+ my $promise = Mojo::Promise->new;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ my $agent = $self->{user_agent};
+ my $proxy;
+ if ( my @proxies = @{ $self->{service_config}{'bahn.de'}{proxies} // [] } )
+ {
+ $proxy = $proxies[ int( rand( scalar @proxies ) ) ];
+ }
+ elsif ( my $p = $self->{service_config}{'bahn.de'}{proxy} ) {
+ $proxy = $p;
+ }
+
+ if ($proxy) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
+ Travel::Status::DE::DBRIS->new_p(
+ journey => $opt{trip_id},
+ with_polyline => $opt{with_polyline},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10),
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ my $journey = $dbris->result;
+
+ if ($journey) {
+ $self->{log}->debug("get_journey_p($opt{trip_id}): success");
+ $promise->resolve($journey);
+ return;
+ }
+ $self->{log}->debug("get_journey_p($opt{trip_id}): no journey");
+ $promise->reject('no journey');
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("get_journey_p($opt{trip_id}): error $err");
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm
index 5b1ee81..c35dfdb 100644
--- a/lib/Travelynx/Helper/HAFAS.pm
+++ b/lib/Travelynx/Helper/HAFAS.pm
@@ -7,11 +7,13 @@ package Travelynx::Helper::HAFAS;
use strict;
use warnings;
use 5.020;
+use utf8;
use DateTime;
use Encode qw(decode);
use JSON;
use Mojo::Promise;
+use Mojo::UserAgent;
use Travel::Status::DE::HAFAS;
sub _epoch {
@@ -53,58 +55,18 @@ sub get_service {
return Travel::Status::DE::HAFAS::get_service($service);
}
-sub get_json_p {
- my ( $self, $url, %opt ) = @_;
-
- my $cache = $self->{main_cache};
- my $promise = Mojo::Promise->new;
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
- if ( $opt{realtime} ) {
- $cache = $self->{realtime_cache};
- }
- $opt{encoding} //= 'ISO-8859-15';
+ $opt{service} //= 'ÖBB';
- if ( my $content = $cache->thaw($url) ) {
- return $promise->resolve($content);
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
- ->then(
- sub {
- my ($tx) = @_;
-
- if ( my $err = $tx->error ) {
- $promise->reject(
-"hafas->get_json_p($url) returned HTTP $err->{code} $err->{message}"
- );
- return;
- }
-
- my $body = decode( $opt{encoding}, $tx->res->body );
-
- $body =~ s{^TSLs[.]sls = }{};
- $body =~ s{;$}{};
- $body =~ s{&#x0028;}{(}g;
- $body =~ s{&#x0029;}{)}g;
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->{log}->info("hafas->get_json_p($url): $err");
- $promise->reject("hafas->get_json_p($url): $err");
- return;
- }
- )->wait;
- return $promise;
-}
-
-sub get_departures_p {
- my ( $self, %opt ) = @_;
-
my $when = (
$opt{timestamp}
? $opt{timestamp}->clone
@@ -118,19 +80,28 @@ sub get_departures_p {
results => 300,
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(5),
+ user_agent => $agent->request_timeout(5),
);
}
sub search_location_p {
my ( $self, %opt ) = @_;
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
return Travel::Status::DE::HAFAS->new_p(
service => $opt{service},
locationSearch => $opt{query},
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(5),
+ user_agent => $agent->request_timeout(5),
);
}
@@ -143,13 +114,22 @@ sub get_tripid_p {
my $train_desc = $train->type . ' ' . $train->train_no;
$train_desc =~ s{^- }{};
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
Travel::Status::DE::HAFAS->new_p(
service => $opt{service},
journeyMatch => $train_desc,
datetime => $train->start,
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(10),
+ user_agent => $agent->request_timeout(10),
)->then(
sub {
my ($hafas) = @_;
@@ -195,6 +175,15 @@ sub get_journey_p {
my $promise = Mojo::Promise->new;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
Travel::Status::DE::HAFAS->new_p(
service => $opt{service},
journey => {
@@ -203,7 +192,7 @@ sub get_journey_p {
with_polyline => $opt{with_polyline},
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(10),
+ user_agent => $agent->request_timeout(10),
)->then(
sub {
my ($hafas) = @_;
@@ -236,6 +225,15 @@ sub get_route_p {
my $promise = Mojo::Promise->new;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
Travel::Status::DE::HAFAS->new_p(
service => $opt{service},
journey => {
@@ -246,7 +244,7 @@ sub get_route_p {
with_polyline => $opt{with_polyline},
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(10),
+ user_agent => $agent->request_timeout(10),
)->then(
sub {
my ($hafas) = @_;
diff --git a/lib/Travelynx/Helper/MOTIS.pm b/lib/Travelynx/Helper/MOTIS.pm
new file mode 100644
index 0000000..d4e1777
--- /dev/null
+++ b/lib/Travelynx/Helper/MOTIS.pm
@@ -0,0 +1,158 @@
+package Travelynx::Helper::MOTIS;
+
+# Copyright (C) 2025 networkException <git@nwex.de>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+use utf8;
+
+use DateTime;
+use Encode qw(decode);
+use JSON;
+use Mojo::Promise;
+use Mojo::UserAgent;
+
+use Travel::Status::MOTIS;
+
+sub _epoch {
+ my ($dt) = @_;
+
+ return $dt ? $dt->epoch : 0;
+}
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header}
+ = { 'User-Agent' =>
+"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx"
+ };
+
+ return bless( \%opt, $class );
+}
+
+sub get_service {
+ my ( $self, $service ) = @_;
+
+ return Travel::Status::MOTIS::get_service($service);
+}
+
+sub get_station_by_query_p {
+ my ( $self, %opt ) = @_;
+
+ $opt{service} //= 'transitous';
+
+ my $promise = Mojo::Promise->new;
+
+ Travel::Status::MOTIS->new_p(
+ cache => $self->{cache},
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ lwp_options => {
+ timeout => 10,
+ agent => $self->{header}{'User-Agent'},
+ },
+
+ service => $opt{service},
+ stops_by_query => $opt{query},
+ )->then(
+ sub {
+ my ($motis) = @_;
+ my $found;
+
+ for my $result ( $motis->results ) {
+ if ( defined $result->id ) {
+ $promise->resolve($result);
+ return;
+ }
+ }
+
+ $promise->reject("Unable to find station '$opt{query}'");
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject("'$err' while trying to look up '$opt{query}'");
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
+
+ $opt{service} //= 'transitous';
+
+ my $timestamp = (
+ $opt{timestamp}
+ ? $opt{timestamp}->clone
+ : DateTime->now
+ )->subtract( minutes => $opt{lookbehind} );
+
+ return Travel::Status::MOTIS->new_p(
+ cache => $self->{cache},
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ lwp_options => {
+ timeout => 10,
+ agent => $self->{header}{'User-Agent'},
+ },
+
+ service => $opt{service},
+ timestamp => $timestamp,
+ stop_id => $opt{station_id},
+ results => 60,
+ );
+}
+
+sub get_trip_p {
+ my ( $self, %opt ) = @_;
+
+ $opt{service} //= 'transitous';
+
+ my $promise = Mojo::Promise->new;
+
+ Travel::Status::MOTIS->new_p(
+ with_polyline => $opt{with_polyline},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+
+ service => $opt{service},
+ trip_id => $opt{trip_id},
+ )->then(
+ sub {
+ my ($motis) = @_;
+ my $journey = $motis->result;
+
+ if ($journey) {
+ $self->{log}->debug("get_trip_p($opt{trip_id}): success");
+ $promise->resolve($journey);
+ return;
+ }
+
+ $self->{log}->debug("get_trip_p($opt{trip_id}): no journey");
+ $promise->reject('no journey');
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("get_trip_p($opt{trip_id}): error $err");
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm
index 100a799..66f2a29 100644
--- a/lib/Travelynx/Helper/Traewelling.pm
+++ b/lib/Travelynx/Helper/Traewelling.pm
@@ -78,7 +78,8 @@ sub get_status_p {
$self->{user_agent}->request_timeout(20)
->get_p(
"https://traewelling.de/api/v1/user/${username}/statuses?limit=1" =>
- $header )->then(
+ $header )
+ ->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
@@ -150,13 +151,13 @@ sub get_status_p {
}
}
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$promise->reject( { text => "v1/${username}/statuses: $err" } );
return;
}
- )->wait;
+ )->wait;
return $promise;
}
@@ -238,13 +239,13 @@ sub logout_p {
return;
}
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$promise->reject("v1/auth/logout: $err");
return;
}
- )->wait;
+ )->wait;
return $promise;
}
@@ -322,7 +323,8 @@ sub checkin_p {
$self->{user_agent}->request_timeout(20)
->post_p(
"https://traewelling.de/api/v1/trains/checkin" => $header => json =>
- $request )->then(
+ $request )
+ ->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
@@ -368,7 +370,7 @@ sub checkin_p {
# on the user status page
return;
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$self->{log}->debug("... $debug_prefix error: $err");
@@ -381,7 +383,7 @@ sub checkin_p {
$promise->reject( { connection => $err } );
return;
}
- )->wait;
+ )->wait;
return $promise;
}
diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm
index 62e60f1..cc943b3 100644
--- a/lib/Travelynx/Model/InTransit.pm
+++ b/lib/Travelynx/Model/InTransit.pm
@@ -1,6 +1,7 @@
package Travelynx::Model::InTransit;
-# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2020-2025 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -12,11 +13,12 @@ use DateTime;
use JSON;
my %visibility_itoa = (
- 100 => 'public',
- 80 => 'travelynx',
- 60 => 'followers',
- 30 => 'unlisted',
- 10 => 'private',
+ 100 => 'public',
+ 80 => 'travelynx',
+ 60 => 'followers',
+ 30 => 'unlisted',
+ 10 => 'private',
+ default => 'default',
);
my %visibility_atoi = (
@@ -30,7 +32,7 @@ my %visibility_atoi = (
sub _epoch {
my ($dt) = @_;
- return $dt ? $dt->epoch : 0;
+ return $dt ? $dt->epoch : undef;
}
sub epoch_to_dt {
@@ -95,15 +97,21 @@ sub add {
my $db = $opt{db} // $self->{pg}->db;
my $backend_id = $opt{backend_id};
my $train = $opt{train};
+ my $train_suffix = $opt{train_suffix};
my $journey = $opt{journey};
my $stop = $opt{stop};
+ my $stopover = $opt{stopover};
my $checkin_station_id = $opt{departure_eva};
my $route = $opt{route};
my $data = $opt{data};
+ my $persistent_data;
my $json = JSON->new;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
if ($train) {
+
+ # IRIS
$db->insert(
'in_transit',
{
@@ -111,16 +119,16 @@ sub add {
cancelled => $train->departure_is_cancelled ? 1
: 0,
checkin_station_id => $checkin_station_id,
- checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
- dep_platform => $train->platform,
- train_type => $train->type,
- train_line => $train->line_no,
- train_no => $train->train_no,
- train_id => $train->train_id,
- sched_departure => $train->sched_departure,
- real_departure => $train->departure,
- route => $json->encode($route),
- messages => $json->encode(
+ checkin_time => $now,
+ dep_platform => $train->platform,
+ train_type => $train->type,
+ train_line => $train->line_no,
+ train_no => $train->train_no,
+ train_id => $train->train_id,
+ sched_departure => $train->sched_departure,
+ real_departure => $train->departure,
+ route => $json->encode($route),
+ messages => $json->encode(
[ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
),
data => JSON->new->encode(
@@ -134,7 +142,9 @@ sub add {
}
);
}
- elsif ( $journey and $stop ) {
+ elsif ( $journey and $stop and $journey->can('product') ) {
+
+ # HAFAS
my @route;
my $product = $journey->product_at( $stop->loc->eva )
// $journey->product;
@@ -169,18 +179,159 @@ sub add {
? 1
: 0,
checkin_station_id => $stop->loc->eva,
+ checkin_time => $now,
+ dep_platform => $stop->{platform},
+ train_type => $product->type // q{},
+ train_line => $product->line_no,
+ train_no => $product->number // q{},
+ train_id => $journey->id,
+ sched_departure => $stop->{sched_dep},
+ real_departure => $stop->{rt_dep} // $stop->{sched_dep},
+ route => $json->encode( \@route ),
+ data => JSON->new->encode(
+ {
+ rt => $stop->{rt_dep} ? 1 : 0,
+ %{ $data // {} }
+ }
+ ),
+ backend_id => $backend_id,
+ }
+ );
+ }
+ elsif ( $journey and $stop ) {
+
+ # DBRIS
+ my $number = $journey->train_no // $journey->number // $train_suffix;
+
+ my $line;
+ if ( defined $journey->line_no and $journey->line_no ne $number ) {
+ $line = $journey->line_no;
+ }
+ elsif ( defined $train_suffix and $train_suffix ne $number ) {
+ $line = $train_suffix;
+ }
+
+ my @route;
+ for my $j_stop ( $journey->route ) {
+ push(
+ @route,
+ [
+ $j_stop->name,
+ $j_stop->eva,
+ {
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ isCancelled => $j_stop->is_cancelled,
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ load => {
+ FIRST => $j_stop->occupancy_first,
+ SECOND => $j_stop->occupancy_second
+ },
+ lat => $j_stop->lat,
+ lon => $j_stop->lon,
+ }
+ ]
+ );
+ }
+ my @messages;
+ for my $msg ( $journey->messages ) {
+ if ( not $msg->{ueberschrift} ) {
+ push(
+ @{ $data->{him_msg} },
+ {
+ header => q{},
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ push(
+ @{ $persistent_data->{him_msg} },
+ {
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ }
+ }
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => $stop->is_cancelled
+ ? 1
+ : 0,
+ checkin_station_id => $stop->eva,
+ checkin_time => $now,
+ dep_platform => $stop->platform,
+ train_type => $journey->type // q{},
+ train_line => $line,
+ train_no => $number,
+ train_id => $data->{trip_id},
+ sched_departure => $stop->sched_dep,
+ real_departure => $stop->rt_dep // $stop->sched_dep,
+ route => $json->encode( \@route ),
+ data => JSON->new->encode(
+ {
+ rt => $stop->{rt_dep} ? 1 : 0,
+ %{ $data // {} }
+ }
+ ),
+ user_data => JSON->new->encode($persistent_data),
+ backend_id => $backend_id,
+ }
+ );
+ }
+ elsif ( $journey and $stopover ) {
+
+ # MOTIS
+ my @route;
+ for my $journey_stopover ( $journey->stopovers ) {
+ push(
+ @route,
+ [
+ $journey_stopover->stop->name,
+ $journey_stopover->stop->{eva}
+ // die('eva not set for stopover'),
+ {
+ sched_arr =>
+ _epoch( $journey_stopover->scheduled_arrival ),
+ sched_dep =>
+ _epoch( $journey_stopover->scheduled_departure ),
+ rt_arr => _epoch( $journey_stopover->realtime_arrival ),
+ rt_dep =>
+ _epoch( $journey_stopover->realtime_departure ),
+ arr_delay => $journey_stopover->arrival_delay,
+ dep_delay => $journey_stopover->departure_delay,
+ lat => $journey_stopover->stop->lat,
+ lon => $journey_stopover->stop->lon,
+ }
+ ]
+ );
+ }
+
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => $stopover->{is_cancelled}
+ ? 1
+ : 0,
+ checkin_station_id => $stopover->stop->{eva},
checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
- dep_platform => $stop->{platform},
- train_type => $product->type // q{},
- train_line => $product->line_no,
- train_no => $product->number // q{},
+ dep_platform => $stopover->track,
+ train_type => $journey->mode,
+ train_no => q{},
train_id => $journey->id,
- sched_departure => $stop->{sched_dep},
- real_departure => $stop->{rt_dep} // $stop->{sched_dep},
+ train_line => $journey->route_name,
+ sched_departure => $stopover->scheduled_departure,
+ real_departure => $stopover->departure,
route => $json->encode( \@route ),
data => JSON->new->encode(
{
- rt => $stop->{rt_dep} ? 1 : 0,
+ rt => $stopover->{is_realtime} ? 1 : 0,
%{ $data // {} }
}
),
@@ -264,18 +415,17 @@ sub postprocess {
$ret->{route_after} = \@route_after;
$ret->{extra_data} = $ret->{data};
$ret->{comment} = $ret->{user_data}{comment};
+ $ret->{wagongroups} = $ret->{user_data}{wagongroups};
$ret->{platform_type} = 'Gleis';
- if ( $ret->{train_type} =~ m{ ast | bus | ruf }ix ) {
+ if ( $ret->{train_type} and $ret->{train_type} =~ m{ ast | bus | ruf }ix ) {
$ret->{platform_type} = 'Steig';
}
$ret->{visibility_str}
- = $ret->{visibility}
- ? $visibility_itoa{ $ret->{visibility} }
- : 'default';
+ = $visibility_itoa{ $ret->{visibility} // 'default' };
$ret->{effective_visibility_str}
- = $visibility_itoa{ $ret->{effective_visibility} };
+ = $visibility_itoa{ $ret->{effective_visibility} // 'default' };
my @parsed_messages;
for my $message ( @{ $ret->{messages} // [] } ) {
@@ -299,7 +449,7 @@ sub postprocess {
= $dep_info->{rt_arr}->epoch - $epoch;
}
- for my $station (@route_after) {
+ for my $station (@route) {
if ( @{$station} > 1 ) {
# Note: $station->[2]{sched_arr} may already have been
@@ -367,7 +517,7 @@ sub get {
my $table = 'in_transit';
- if ( $opt{with_timestamps} ) {
+ if ( $opt{with_timestamps} or $opt{with_polyline} ) {
$table = 'in_transit_str';
}
@@ -381,13 +531,16 @@ sub get {
$ret = $res->hash;
}
+ if ( $opt{with_polyline} and $ret ) {
+ $ret->{dep_latlon} = [ $ret->{dep_lat}, $ret->{dep_lon} ];
+ $ret->{arr_latlon} = [ $ret->{arr_lat}, $ret->{arr_lon} ];
+ }
+
if ( $opt{with_visibility} and $ret ) {
$ret->{visibility_str}
- = $ret->{visibility}
- ? $visibility_itoa{ $ret->{visibility} }
- : 'default';
+ = $visibility_itoa{ $ret->{visibility} // 'default' };
$ret->{effective_visibility_str}
- = $visibility_itoa{ $ret->{effective_visibility} };
+ = $visibility_itoa{ $ret->{effective_visibility} // 'default' };
}
if ( $opt{postprocess} and $ret ) {
@@ -731,6 +884,93 @@ sub update_departure_cancelled {
return $rows;
}
+sub update_departure_dbris {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $json = JSON->new;
+
+ my $res_h = $db->select( 'in_transit', [ 'data', 'user_data' ],
+ { user_id => $uid } )->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ my $persistent_data = $res_h ? $res_h->{user_data} : {};
+
+ if ( $stop->{rt_dep} ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
+ $ephemeral_data->{him_msg} = [];
+ $persistent_data->{him_msg} = [];
+ for my $msg ( $journey->messages ) {
+ if ( not $msg->{ueberschrift} ) {
+ push(
+ @{ $ephemeral_data->{him_msg} },
+ {
+ header => q{},
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ push(
+ @{ $persistent_data->{him_msg} },
+ {
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ }
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_departure => $stop->{rt_dep},
+ data => $json->encode($ephemeral_data),
+ user_data => $json->encode($persistent_data),
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{train_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_departure_motis {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stopover = $opt{stopover};
+ my $json = JSON->new;
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_departure => $stopover->{realtime_departure},
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{train_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
sub update_departure_hafas {
my ( $self, %opt ) = @_;
my $uid = $opt{uid};
@@ -741,12 +981,20 @@ sub update_departure_hafas {
my $stop = $opt{stop};
my $json = JSON->new;
+ my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
+ ->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ if ( $stop->{rt_dep} ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
# selecting on user_id and train_no avoids a race condition if a user checks
# into a new train while we are fetching data for their previous journey. In
# this case, the new train would receive data from the previous journey.
$db->update(
'in_transit',
{
+ data => $json->encode($ephemeral_data),
real_departure => $stop->{rt_dep},
},
{
@@ -800,6 +1048,146 @@ sub update_arrival {
return $rows;
}
+sub update_arrival_dbris {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $json = JSON->new;
+
+ my $res_h = $db->select( 'in_transit', [ 'data', 'user_data' ],
+ { user_id => $uid } )->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ my $persistent_data = $res_h ? $res_h->{user_data} : {};
+
+ if ( $stop->{rt_arr} ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
+ $ephemeral_data->{him_msg} = [];
+ $persistent_data->{him_msg} = [];
+ for my $msg ( $journey->messages ) {
+ if ( not $msg->{ueberschrift} ) {
+ push(
+ @{ $ephemeral_data->{him_msg} },
+ {
+ header => q{},
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ push(
+ @{ $persistent_data->{him_msg} },
+ {
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ }
+ }
+
+ my @route;
+ for my $j_stop ( $journey->route ) {
+ push(
+ @route,
+ [
+ $j_stop->name,
+ $j_stop->eva,
+ {
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ platform => $j_stop->platform,
+ isCancelled => $j_stop->is_cancelled,
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ load => {
+ FIRST => $j_stop->occupancy_first,
+ SECOND => $j_stop->occupancy_second
+ },
+ lat => $j_stop->lat,
+ lon => $j_stop->lon,
+ }
+ ]
+ );
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_arrival => $stop->{rt_arr},
+ arr_platform => $stop->{platform},
+ route => $json->encode( [@route] ),
+ data => $json->encode($ephemeral_data),
+ user_data => $json->encode($persistent_data),
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{train_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_arrival_motis {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stopover = $opt{stopover};
+ my $json = JSON->new;
+
+ my @route;
+ for my $journey_stopover ( $journey->stopovers ) {
+ push(
+ @route,
+ [
+ $journey_stopover->stop->name,
+ $journey_stopover->stop->{eva}
+ // die('eva not set for stopover'),
+ {
+ sched_arr => _epoch( $journey_stopover->scheduled_arrival ),
+ sched_dep =>
+ _epoch( $journey_stopover->scheduled_departure ),
+ rt_arr => _epoch( $journey_stopover->realtime_arrival ),
+ rt_dep => _epoch( $journey_stopover->realtime_departure ),
+ arr_delay => $journey_stopover->arrival_delay,
+ dep_delay => $journey_stopover->departure_delay,
+ lat => $journey_stopover->stop->lat,
+ lon => $journey_stopover->stop->lon,
+ }
+ ]
+ );
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_arrival => $stopover->{realtime_arrival},
+ route => $json->encode( [@route] ),
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{train_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
sub update_arrival_hafas {
my ( $self, %opt ) = @_;
my $uid = $opt{uid};
@@ -810,6 +1198,16 @@ sub update_arrival_hafas {
my $stop = $opt{stop};
my $json = JSON->new;
+ my $res_h
+ = $db->select( 'in_transit', [ 'data', 'route' ], { user_id => $uid } )
+ ->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ my $old_route = $res_h ? $res_h->{route} : [];
+
+ if ( $stop->{rt_arr} ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
my @route;
for my $j_stop ( $journey->route ) {
push(
@@ -835,10 +1233,6 @@ sub update_arrival_hafas {
}
}
- my $res_h = $db->select( 'in_transit', ['route'], { user_id => $uid } )
- ->expand->hash;
- my $old_route = $res_h ? $res_h->{route} : [];
-
for my $i ( 0 .. $#route ) {
if ( $old_route->[$i] and $old_route->[$i][1] == $route[$i][1] ) {
for my $k (qw(rt_arr rt_dep arr_delay dep_delay)) {
@@ -853,6 +1247,7 @@ sub update_arrival_hafas {
$db->update(
'in_transit',
{
+ data => $json->encode($ephemeral_data),
real_arrival => $stop->{rt_arr},
route => $json->encode( [@route] ),
},
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
index 93e59e7..1662787 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -549,7 +549,7 @@ sub get {
my @select
= (
- qw(journey_id is_iris is_hafas backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility)
+ qw(journey_id is_dbris is_iris is_hafas is_motis backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility)
);
my %where = (
user_id => $uid,
@@ -607,8 +607,10 @@ sub get {
my $ref = {
id => $entry->{journey_id},
+ is_dbris => $entry->{is_dbris},
is_iris => $entry->{is_iris},
is_hafas => $entry->{is_hafas},
+ is_motis => $entry->{is_motis},
backend_name => $entry->{backend_name},
backend_id => $entry->{backend_id},
type => $entry->{train_type},
@@ -870,8 +872,11 @@ sub get_latest_checkout_stations {
my $res = $db->select(
'journeys_str',
[
- 'arr_name', 'arr_eva', 'train_id', 'backend_id',
- 'backend_name', 'is_hafas'
+ 'arr_name', 'arr_eva',
+ 'arr_external_id', 'train_id',
+ 'backend_id', 'backend_name',
+ 'is_dbris', 'is_hafas',
+ 'is_motis'
],
{
user_id => $uid,
@@ -893,9 +898,13 @@ sub get_latest_checkout_stations {
push(
@ret,
{
- name => $row->{arr_name},
- eva => $row->{arr_eva},
+ name => $row->{arr_name},
+ eva => $row->{arr_eva},
+ external_id_or_eva => $row->{arr_external_id}
+ // $row->{arr_eva},
+ dbris => $row->{is_dbris} ? $row->{backend_name} : 0,
hafas => $row->{is_hafas} ? $row->{backend_name} : 0,
+ motis => $row->{is_motis} ? $row->{backend_name} : 0,
backend_id => $row->{backend_id},
}
);
@@ -1663,7 +1672,10 @@ sub compute_stats {
@inconsistencies,
{
conflict => {
- train => $journey->{type} . ' '
+ train => (
+ $journey->{is_motis} ? '' : $journey->{type}
+ )
+ . ' '
. ( $journey->{line} // $journey->{no} ),
arr => epoch_to_dt( $journey->{rt_arr_ts} )
->strftime('%d.%m.%Y %H:%M'),
@@ -1689,7 +1701,8 @@ sub compute_stats {
$next_departure = $journey->{rt_dep_ts};
$next_id = $journey->{id};
$next_train
- = $journey->{type} . ' ' . ( $journey->{line} // $journey->{no} ),;
+ = ( $journey->{is_motis} ? '' : $journey->{type} ) . ' '
+ . ( $journey->{line} // $journey->{no} ),;
}
my $ret = {
km_route => $km_route,
@@ -1888,7 +1901,8 @@ sub get_connection_targets {
);
my @destinations
= $res->hashes->grep( sub { shift->{count} >= $min_count } )
- ->map( sub { shift->{dest} } )->each;
+ ->map( sub { shift->{dest} } )
+ ->each;
@destinations = $self->{stations}->get_by_evas(
backend_id => $opt{backend_id},
evas => [@destinations]
diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm
index f4eb096..44618ac 100644
--- a/lib/Travelynx/Model/Stations.pm
+++ b/lib/Travelynx/Model/Stations.pm
@@ -1,6 +1,7 @@
package Travelynx::Model::Stations;
# Copyright (C) 2022 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -25,11 +26,28 @@ sub get_backend_id {
if ( $opt{hafas} and $self->{backend_id}{hafas}{ $opt{hafas} } ) {
return $self->{backend_id}{hafas}{ $opt{hafas} };
}
+ if ( $opt{dbris} and $self->{backend_id}{dbris}{ $opt{dbris} } ) {
+ return $self->{backend_id}{dbris}{ $opt{dbris} };
+ }
+ if ( $opt{motis} and $self->{backend_id}{motis}{ $opt{motis} } ) {
+ return $self->{backend_id}{motis}{ $opt{motis} };
+ }
my $db = $opt{db} // $self->{pg}->db;
my $backend_id = 0;
- if ( $opt{hafas} ) {
+ if ( $opt{dbris} ) {
+ $backend_id = $db->select(
+ 'backends',
+ ['id'],
+ {
+ dbris => 1,
+ name => $opt{dbris}
+ }
+ )->hash->{id};
+ $self->{backend_id}{dbris}{ $opt{dbris} } = $backend_id;
+ }
+ elsif ( $opt{hafas} ) {
$backend_id = $db->select(
'backends',
['id'],
@@ -40,35 +58,40 @@ sub get_backend_id {
)->hash->{id};
$self->{backend_id}{hafas}{ $opt{hafas} } = $backend_id;
}
+ elsif ( $opt{motis} ) {
+ $backend_id = $db->select(
+ 'backends',
+ ['id'],
+ {
+ motis => 1,
+ name => $opt{motis}
+ }
+ )->hash->{id};
+ $self->{backend_id}{motis}{ $opt{motis} } = $backend_id;
+ }
return $backend_id;
}
-sub get_hafas_name {
+sub get_backend {
my ( $self, %opt ) = @_;
- if ( exists $self->{hafas_name}{ $opt{backend_id} } ) {
- return $self->{hafas_name}{ $opt{backend_id} };
+ if ( $self->{backend_cache}{ $opt{backend_id} } ) {
+ return $self->{backend_cache}{ $opt{backend_id} };
}
- my $db = $opt{db} // $self->{pg}->db;
- my $hafas_name;
+ my $db = $opt{db} // $self->{pg}->db;
my $ret = $db->select(
'backends',
- ['name'],
+ '*',
{
- hafas => 1,
- id => $opt{backend_id},
+ id => $opt{backend_id},
}
)->hash;
- if ($ret) {
- $hafas_name = $ret->{name};
- }
-
- $self->{hafas_name}{ $opt{backend_id} } = $hafas_name;
+ $self->{backend_cache}{ $opt{backend_id} } = $ret;
- return $hafas_name;
+ return $ret;
}
sub get_backends {
@@ -76,7 +99,8 @@ sub get_backends {
$opt{db} //= $self->{pg}->db;
- my $res = $opt{db}->select( 'backends', [ 'id', 'name', 'efa', 'hafas', 'iris' ] );
+ my $res = $opt{db}->select( 'backends',
+ [ 'id', 'name', 'dbris', 'efa', 'hafas', 'iris', 'motis' ] );
my @ret;
while ( my $row = $res->hash ) {
@@ -85,9 +109,11 @@ sub get_backends {
{
id => $row->{id},
name => $row->{name},
+ dbris => $row->{dbris},
efa => $row->{efa},
hafas => $row->{hafas},
iris => $row->{iris},
+ motis => $row->{motis},
}
);
}
@@ -95,14 +121,104 @@ sub get_backends {
return @ret;
}
+# Slow for MOTIS backends
sub add_or_update {
my ( $self, %opt ) = @_;
my $stop = $opt{stop};
- my $loc = $stop->loc;
$opt{db} //= $self->{pg}->db;
$opt{backend_id} //= $self->get_backend_id(%opt);
+ if ( $opt{dbris} ) {
+ if (
+ my $s = $self->get_by_eva(
+ $stop->eva,
+ db => $opt{db},
+ backend_id => $opt{backend_id}
+ )
+ )
+ {
+ $opt{db}->update(
+ 'stations',
+ {
+ name => $stop->name,
+ lat => $stop->lat,
+ lon => $stop->lon,
+ archived => 0
+ },
+ {
+ eva => $stop->eva,
+ source => $opt{backend_id}
+ }
+ );
+ return;
+ }
+ $opt{db}->insert(
+ 'stations',
+ {
+ eva => $stop->eva,
+ name => $stop->name,
+ lat => $stop->lat,
+ lon => $stop->lon,
+ source => $opt{backend_id},
+ archived => 0
+ }
+ );
+ return;
+ }
+
+ if ( $opt{motis} ) {
+ if (
+ my $s = $self->get_by_external_id(
+ external_id => $stop->id,
+ db => $opt{db},
+ backend_id => $opt{backend_id}
+ )
+ )
+ {
+ $opt{db}->update(
+ 'stations',
+ {
+ name => $stop->name,
+ lat => $stop->lat,
+ lon => $stop->lon,
+ archived => 0
+ },
+ {
+ eva => $s->{eva},
+ source => $opt{backend_id}
+ }
+ );
+
+ # MOTIS backends do not provide a numeric ID, so we set our ID here.
+ $stop->{eva} = $s->{eva};
+ return;
+ }
+
+ my $s = $opt{db}->query(
+ qq {
+ with new_station as (
+ insert into stations_external_ids (backend_id, external_id)
+ values (?, ?)
+ returning eva, backend_id
+ )
+
+ insert into stations (eva, name, lat, lon, source, archived)
+ values ((select eva from new_station), ?, ?, ?, (select backend_id from new_station), ?)
+ returning *
+ },
+ (
+ $opt{backend_id}, $stop->id, $stop->name,
+ $stop->lat, $stop->lon, 0,
+ )
+ );
+
+ # MOTIS backends do not provide a numeric ID, so we set our ID here.
+ $stop->{eva} = $s->hash->{eva};
+ return;
+ }
+
+ my $loc = $stop->loc;
if (
my $s = $self->get_by_eva(
$loc->eva,
@@ -137,6 +253,8 @@ sub add_or_update {
archived => 0
}
);
+
+ return;
}
sub add_meta {
@@ -229,6 +347,27 @@ sub get_by_eva {
)->hash;
}
+# Slow
+sub get_by_external_id {
+ my ( $self, %opt ) = @_;
+
+ if ( not $opt{external_id} ) {
+ return;
+ }
+
+ $opt{db} //= $self->{pg}->db;
+ $opt{backend_id} //= $self->get_backend_id(%opt);
+
+ return $opt{db}->select(
+ 'stations_with_external_ids',
+ '*',
+ {
+ external_id => $opt{external_id},
+ source => $opt{backend_id},
+ }
+ )->hash;
+}
+
# Fast
sub get_by_evas {
my ( $self, %opt ) = @_;
diff --git a/lib/Travelynx/Model/Traewelling.pm b/lib/Travelynx/Model/Traewelling.pm
index c460b1a..608da15 100644
--- a/lib/Travelynx/Model/Traewelling.pm
+++ b/lib/Travelynx/Model/Traewelling.pm
@@ -224,7 +224,7 @@ sub get_pushable_accounts {
join in_transit_str as i on t.user_id = i.user_id
where t.push_sync = True
and i.arr_eva is not null
- and i.backend_id <= 1
+ and i.backend_id = (select id from backends where dbris = true and name = 'bahn.de')
and i.cancelled = False
}
);
diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm
index a672a38..a552633 100644
--- a/lib/Travelynx/Model/Users.pm
+++ b/lib/Travelynx/Model/Users.pm
@@ -418,7 +418,7 @@ sub get {
. 'extract(epoch from registered_at) as registered_at_ts, '
. 'extract(epoch from last_seen) as last_seen_ts, '
. 'extract(epoch from deletion_requested) as deletion_requested_ts, '
- . 'backend_id, backend_name, efa, hafas',
+ . 'backend_id, backend_name, dbris, efa, hafas, motis',
{ id => $uid }
)->hash;
if ($user) {
@@ -440,7 +440,7 @@ sub get {
past_all => $user->{public_level} & 0x10000 ? 1 : 0,
email => $user->{email},
sb_template =>
- 'https://dbf.finalrewind.org/{name}?rt=1&hafas={hafas}#{tt}{tn}',
+'https://dbf.finalrewind.org/{name}?rt=1&dbris={dbris}&hafas={hafas}#{id_or_tttn}',
registered_at => DateTime->from_epoch(
epoch => $user->{registered_at_ts},
time_zone => 'Europe/Berlin'
@@ -457,8 +457,10 @@ sub get {
: undef,
backend_id => $user->{backend_id},
backend_name => $user->{backend_name},
+ backend_dbris => $user->{dbris},
backend_efa => $user->{efa},
backend_hafas => $user->{hafas},
+ backend_motis => $user->{motis},
};
}
return undef;