diff options
-rwxr-xr-x | lib/Travelynx.pm | 266 | ||||
-rw-r--r-- | lib/Travelynx/Command/work.pm | 2 | ||||
-rw-r--r-- | templates/_checked_in.html.ep | 48 | ||||
-rw-r--r-- | templates/_public_status_card.html.ep | 48 |
4 files changed, 360 insertions, 4 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index 19d6edf..3bc9e3c 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -2,10 +2,12 @@ package Travelynx; use Mojo::Base 'Mojolicious'; use Mojo::Pg; +use Mojo::Promise; use Mojolicious::Plugin::Authentication; use Cache::File; use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64); use DateTime; +use DateTime::Format::Strptime; use Encode qw(decode encode); use Geo::Distance; use JSON; @@ -14,6 +16,7 @@ use List::MoreUtils qw(after_incl before_incl); use Travel::Status::DE::IRIS; use Travel::Status::DE::IRIS::Stations; use Travelynx::Helper::Sendmail; +use XML::LibXML; sub check_password { my ( $password, $hash ) = @_; @@ -395,6 +398,8 @@ sub startup { "Checkin($uid): INSERT failed: $@"); return ( undef, 'INSERT failed: ' . $@ ); } + $self->add_route_timestamps( $self->current_user->{id}, + $train ); $self->run_hook( $self->current_user->{id}, 'checkin' ); return ( $train, undef ); } @@ -630,6 +635,7 @@ sub startup { return ( 0, undef ); } $self->run_hook( $uid, 'update' ); + $self->add_route_timestamps( $self->current_user->{id}, $train ); return ( 1, undef ); } ); @@ -1490,6 +1496,223 @@ sub startup { ); $self->helper( + 'get_hafas_json_p' => sub { + my ( $self, $url ) = @_; + + my $cache = $self->app->cache_iris_main; + my $promise = Mojo::Promise->new; + + if ( my $content = $cache->thaw($url) ) { + $promise->resolve($content); + return $promise; + } + + $self->ua->request_timeout(5)->get_p($url)->then( + sub { + my ($tx) = @_; + my $body = decode( 'ISO-8859-15', $tx->res->body ); + + $body =~ s{^TSLs[.]sls = }{}; + $body =~ s{;$}{}; + $body =~ s{(}{(}g; + $body =~ s{)}{)}g; + my $json = JSON->new->decode($body); + $cache->freeze( $url, $json ); + $promise->resolve($json); + } + )->catch( + sub { + my ($err) = @_; + $self->app->log->warning("get($url): $err"); + $promise->reject($err); + } + )->wait; + return $promise; + } + ); + + $self->helper( + 'get_hafas_xml_p' => sub { + my ( $self, $url ) = @_; + + my $cache = $self->app->cache_iris_rt; + my $promise = Mojo::Promise->new; + + if ( my $content = $cache->thaw($url) ) { + $promise->resolve($content); + return $promise; + } + + $self->ua->request_timeout(5)->get_p($url)->then( + sub { + my ($tx) = @_; + my $body = decode( 'ISO-8859-15', $tx->res->body ); + my $tree; + + my $traininfo = { + station => {}, + }; + + # <SDay text="... > ..."> is invalid HTML, but present in + # regardless. As it is the last tag, we just throw it away. + $body =~ s{<SDay .*}{</Journey>}s; + eval { $tree = XML::LibXML->load_xml( string => $body ) }; + if ($@) { + $self->app->log->warning("load_xml($url): $@"); + $cache->freeze( $url, $traininfo ); + $promise->resolve($traininfo); + return; + } + + for my $station ( $tree->findnodes('/Journey/St') ) { + my $name = $station->getAttribute('name'); + my $adelay = $station->getAttribute('adelay'); + my $ddelay = $station->getAttribute('ddelay'); + $traininfo->{station}{$name} = { + adelay => $adelay, + ddelay => $ddelay, + }; + } + + $cache->freeze( $url, $traininfo ); + $promise->resolve($traininfo); + } + )->catch( + sub { + my ($err) = @_; + $self->app->log->warning("get($url): $err"); + $promise->reject($err); + } + )->wait; + return $promise; + } + ); + + $self->helper( + 'add_route_timestamps' => sub { + my ( $self, $uid, $train ) = @_; + + $uid //= $self->current_user->{id}; + + my $db = $self->pg->db; + + my $journey + = $db->select( 'in_transit', ['route'], { user_id => $uid } ) + ->expand->hash; + + if ( not $journey ) { + return; + } + + my $route = $journey->{route}; + + my $base + = 'https://reiseauskunft.bahn.de/bin/trainsearch.exe/dn?L=vs_json.vs_hap&start=yes&rt=1'; + my $date_yy = $train->start->strftime('%d.%m.%y'); + my $date_yyyy = $train->start->strftime('%d.%m.%Y'); + my $train_no = $train->type . ' ' . $train->train_no; + + $self->app->log->debug("add_route_timestamps"); + + my ( $trainlink, $route_data ); + + $self->get_hafas_json_p( + "${base}&date=${date_yy}&trainname=${train_no}")->then( + sub { + my ($trainsearch) = @_; + + # Fallback: Take first result + $trainlink = $trainsearch->{suggestions}[0]{trainLink}; + + # Try finding a result for the current date + for + my $suggestion ( @{ $trainsearch->{suggestions} // [] } ) + { + + # Drunken API, sail with care. Both date formats are used interchangeably + if ( $suggestion->{depDate} eq $date_yy + or $suggestion->{depDate} eq $date_yyyy ) + { + $trainlink = $suggestion->{trainLink}; + last; + } + } + + if ( not $trainlink ) { + $self->app->log->debug("trainlink not found"); + return Mojo::Promise->reject("trainlink not found"); + } + my $base2 + = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn'; + return $self->get_hafas_json_p( +"${base2}/${trainlink}?rt=1&date=${date_yy}&L=vs_json.vs_hap" + ); + } + )->then( + sub { + my ($traininfo) = @_; + + if ( not $traininfo or $traininfo->{error} ) { + $self->app->log->debug("traininfo error"); + return Mojo::Promise->reject("traininfo error"); + } + my $routeinfo + = $traininfo->{suggestions}[0]{locations}; + + my $strp = DateTime::Format::Strptime->new( + pattern => '%d.%m.%y %H:%M', + time_zone => 'Europe/Berlin', + ); + + $route_data = {}; + + for my $station ( @{$routeinfo} ) { + my $arr + = $strp->parse_datetime( + $station->{arrDate} . ' ' . $station->{arrTime} ); + my $dep + = $strp->parse_datetime( + $station->{depDate} . ' ' . $station->{depTime} ); + $route_data->{ $station->{name} } = { + sched_arr => $arr ? $arr->epoch : 0, + sched_dep => $dep ? $dep->epoch : 0, + }; + } + + my $base2 + = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn'; + return $self->get_hafas_xml_p( + "${base2}/${trainlink}?rt=1&date=${date_yy}&L=vs_java3" + ); + } + )->then( + sub { + my ($traininfo2) = @_; + + for my $station ( keys %{$route_data} ) { + for my $key ( + keys %{ $traininfo2->{station}{$station} // {} } ) + { + $route_data->{$station}{$key} + = $traininfo2->{station}{$station}{$key}; + } + } + + for my $station ( @{$route} ) { + $station->[1] + = $route_data->{ $station->[0] }; + } + $db->update( + 'in_transit', + { route => JSON->new->encode($route) }, + { user_id => $uid } + ); + } + )->wait; + } + ); + + $self->helper( 'get_oldest_journey_ts' => sub { my ($self) = @_; @@ -1826,8 +2049,9 @@ sub startup { $uid //= $self->current_user->{id}; - my $db = $self->pg->db; - my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + my $db = $self->pg->db; + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + my $epoch = $now->epoch; my $in_transit = $db->select( 'in_transit_str', '*', { user_id => $uid } ) @@ -1883,12 +2107,45 @@ sub startup { } $ret->{messages} = [ reverse @parsed_messages ]; + for my $station (@route_after) { + if ( @{$station} > 1 ) { + my $times = $station->[1]; + if ( $times->{sched_arr} ) { + $times->{sched_arr} + = epoch_to_dt( $times->{sched_arr} ); + $times->{rt_arr} = $times->{sched_arr}->clone; + if ( $times->{adelay} + and $times->{adelay} =~ m{^\d+$} ) + { + $times->{rt_arr} + ->add( minutes => $times->{adelay} ); + } + $times->{rt_arr_countdown} + = $times->{rt_arr}->epoch - $epoch; + } + if ( $times->{sched_dep} ) { + $times->{sched_dep} + = epoch_to_dt( $times->{sched_dep} ); + $times->{rt_dep} = $times->{sched_dep}->clone; + if ( $times->{ddelay} + and $times->{ddelay} =~ m{^\d+$} ) + { + $times->{rt_dep} + ->add( minutes => $times->{ddelay} ); + } + $times->{rt_dep_countdown} + = $times->{rt_dep}->epoch - $epoch; + } + } + } + $ret->{departure_countdown} = $ret->{real_departure}->epoch - $now->epoch; if ( $in_transit->{real_arr_ts} ) { $ret->{arrival_countdown} = $ret->{real_arrival}->epoch - $now->epoch; - $ret->{journey_duration} = $ret->{real_arrival}->epoch + $ret->{journey_duration} + = $ret->{real_arrival}->epoch - $ret->{real_departure}->epoch; $ret->{journey_completion} = $ret->{journey_duration} @@ -2116,7 +2373,8 @@ sub startup { and $next_departure->epoch - $journey->{rt_arrival}->epoch < ( 60 * 60 ) ) { - if ( $next_departure->epoch - $journey->{rt_arrival}->epoch + if ( + $next_departure->epoch - $journey->{rt_arrival}->epoch < 0 ) { push( @inconsistencies, diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 46f2213..80c584c 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -60,6 +60,7 @@ sub run { }, { user_id => $uid } ); + $self->app->add_route_timestamps( $uid, $train ); } }; if ($@) { @@ -112,6 +113,7 @@ sub run { }, { user_id => $uid } ); + $self->app->add_route_timestamps( $uid, $train ); } elsif ( $entry->{real_arr_ts} ) { $self->app->log->debug(" - checking out"); diff --git a/templates/_checked_in.html.ep b/templates/_checked_in.html.ep index 0683ae5..df5d260 100644 --- a/templates/_checked_in.html.ep +++ b/templates/_checked_in.html.ep @@ -62,8 +62,56 @@ noch nicht bekannt % } </div> + <div class="center-align hide-on-small-only"> + % for my $station (@{$journey->{route_after}}) { + % if ($station->[0] eq $journey->{arr_name}) { + % last; + % } + % if (($station->[1]{rt_arr_countdown} // 0) > 0) { + <%= $station->[0] %><br/><%= $station->[1]{rt_arr}->strftime('%H:%M') %> + % if ($station->[1]{sched_arr}->epoch != $station->[1]{rt_arr}->epoch) { + %= sprintf('(%+d)', ($station->[1]{rt_arr}->epoch - $station->[1]{sched_arr}->epoch ) / 60); + % } + % last; + % } + % if (($station->[1]{rt_dep_countdown} // 0) > 0) { + <%= $station->[0] %><br/> + <%= $station->[1]{rt_arr}->strftime('%H:%M') %> → + <%= $station->[1]{rt_dep}->strftime('%H:%M') %> + % if ($station->[1]{sched_dep}->epoch != $station->[1]{rt_dep}->epoch) { + %= sprintf('(%+d)', ($station->[1]{rt_dep}->epoch - $station->[1]{sched_dep}->epoch ) / 60); + % } + % last; + % } + % } + </div> <div style="clear: both;"> </div> + <div class="hide-on-med-and-up" style="margin-top: 2ex;"> + % for my $station (@{$journey->{route_after}}) { + % if ($station->[0] eq $journey->{arr_name}) { + % last; + % } + % if (($station->[1]{rt_arr_countdown} // 0) > 0) { + Nächster Halt:<br/> + <%= $station->[0] %><br/><%= $station->[1]{rt_arr}->strftime('%H:%M') %> + % if ($station->[1]{sched_arr}->epoch != $station->[1]{rt_arr}->epoch) { + %= sprintf('(%+d)', ($station->[1]{rt_arr}->epoch - $station->[1]{sched_arr}->epoch ) / 60); + % } + % last; + % } + % if (($station->[1]{rt_dep_countdown} // 0) > 0) { + Aktueller Halt:<br/> + <%= $station->[0] %><br/> + <%= $station->[1]{rt_arr}->strftime('%H:%M') %> → + <%= $station->[1]{rt_dep}->strftime('%H:%M') %> + % if ($station->[1]{sched_dep}->epoch != $station->[1]{rt_dep}->epoch) { + %= sprintf('(%+d)', ($station->[1]{rt_dep}->epoch - $station->[1]{sched_dep}->epoch ) / 60); + % } + % last; + % } + % } + </div> </p> % } % if (@{$journey->{messages} // []} > 0 and $journey->{messages}[0]) { diff --git a/templates/_public_status_card.html.ep b/templates/_public_status_card.html.ep index ebf989b..510bf52 100644 --- a/templates/_public_status_card.html.ep +++ b/templates/_public_status_card.html.ep @@ -70,8 +70,56 @@ noch nicht bekannt % } </div> + <div class="center-align hide-on-small-only"> + % for my $station (@{$journey->{route_after}}) { + % if ($station->[0] eq $journey->{arr_name}) { + % last; + % } + % if (($station->[1]{rt_arr_countdown} // 0) > 0) { + <%= $station->[0] %><br/><%= $station->[1]{rt_arr}->strftime('%H:%M') %> + % if ($station->[1]{sched_arr}->epoch != $station->[1]{rt_arr}->epoch) { + %= sprintf('(%+d)', ($station->[1]{rt_arr}->epoch - $station->[1]{sched_arr}->epoch ) / 60); + % } + % last; + % } + % if (($station->[1]{rt_dep_countdown} // 0) > 0) { + <%= $station->[0] %><br/> + <%= $station->[1]{rt_arr}->strftime('%H:%M') %> → + <%= $station->[1]{rt_dep}->strftime('%H:%M') %> + % if ($station->[1]{sched_dep}->epoch != $station->[1]{rt_dep}->epoch) { + %= sprintf('(%+d)', ($station->[1]{rt_dep}->epoch - $station->[1]{sched_dep}->epoch ) / 60); + % } + % last; + % } + % } + </div> <div style="clear: both;"> </div> + <div class="hide-on-med-and-up" style="margin-top: 2ex;"> + % for my $station (@{$journey->{route_after}}) { + % if ($station->[0] eq $journey->{arr_name}) { + % last; + % } + % if (($station->[1]{rt_arr_countdown} // 0) > 0) { + Nächster Halt:<br/> + <%= $station->[0] %><br/><%= $station->[1]{rt_arr}->strftime('%H:%M') %> + % if ($station->[1]{sched_arr}->epoch != $station->[1]{rt_arr}->epoch) { + %= sprintf('(%+d)', ($station->[1]{rt_arr}->epoch - $station->[1]{sched_arr}->epoch ) / 60); + % } + % last; + % } + % if (($station->[1]{rt_dep_countdown} // 0) > 0) { + Aktueller Halt:<br/> + <%= $station->[0] %><br/> + <%= $station->[1]{rt_arr}->strftime('%H:%M') %> → + <%= $station->[1]{rt_dep}->strftime('%H:%M') %> + % if ($station->[1]{sched_dep}->epoch != $station->[1]{rt_dep}->epoch) { + %= sprintf('(%+d)', ($station->[1]{rt_dep}->epoch - $station->[1]{sched_dep}->epoch ) / 60); + % } + % last; + % } + % } + </div> </p> % if (@{$journey->{messages} // []} > 0 and $journey->{messages}[0]) { <p style="margin-bottom: 2ex;"> |