Commit 9bdae29a authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files

Move HAFAS helpers to a separate helper class

parent c8383c69
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ use Mojo::Base 'Mojolicious';
# License: 2-Clause BSD

use Cache::File;
use DBInfoscreen::Helper::HAFAS;
use File::Slurp qw(read_file);
use JSON;
use Travel::Status::DE::HAFAS;
@@ -94,6 +95,19 @@ sub startup {
		}
	);

	$self->helper(
		hafas => sub {
			my ($self) = @_;
			state $hafas = DBInfoscreen::Helper::HAFAS->new(
				log            => $self->app->log,
				main_cache     => $self->app->cache_iris_main,
				realtime_cache => $self->app->cache_iris_rt,
				user_agent     => $self->ua,
				version        => $VERSION,
			);
		}
	);

	$self->helper(
		'handle_no_results' => sub {
			my ( $self, $backend, $station, $errstr ) = @_;
+3 −280
Original line number Diff line number Diff line
@@ -120,44 +120,6 @@ sub check_wagonorder_with_wings {
	return;
}

sub get_hafas_trip_id {
	my ( $ua, $cache, $train ) = @_;

	my $eva    = $train->station_uic;
	my $dep_ts = DateTime->now( time_zone => 'Europe/Berlin' );
	my $url
	  = "https://2.db.transport.rest/stations/${eva}/departures?duration=5&when=$dep_ts";
	if ( $train->sched_departure ) {
		$dep_ts = $train->sched_departure->epoch;
		$url
		  = "https://2.db.transport.rest/stations/${eva}/departures?duration=5&when=$dep_ts";
	}
	elsif ( $train->sched_arrival ) {
		$dep_ts = $train->sched_arrival->epoch;
		$url
		  = "https://2.db.transport.rest/stations/${eva}/arrivals?duration=5&when=$dep_ts";
	}

	my $json = hafas_rest_req( $ua, $cache, $url );

	#say "looking for " . $train->train_no . " in $url";
	for my $result ( @{ $json // [] } ) {
		my $trip_id = $result->{tripId};
		my $fahrt   = $result->{line}{fahrtNr};

		#say "checking $fahrt";
		if ( $result->{line} and $result->{line}{fahrtNr} == $train->train_no )
		{
			#say "Trip ID is $trip_id";
			return $trip_id;
		}
		else {
			#say "unmatched Trip ID $trip_id";
		}
	}
	return;
}

sub check_wagonorder {
	my ( $ua, $cache, $train_no, $wr_link ) = @_;

@@ -184,240 +146,6 @@ sub check_wagonorder {
	}
}

sub hafas_rest_req {
	my ( $ua, $cache, $url ) = @_;

	if ( my $content = $cache->thaw($url) ) {
		return $content;
	}

	my $res = eval {
		$ua->get(
			$url => { 'User-Agent' => "dbf.finalrewind.org/${dbf_version}" } )
		  ->result;
	};

	if ($@) {
		return;
	}
	if ( $res->is_error ) {
		return;
	}

	my $json = decode_json( $res->body );

	$cache->freeze( $url, $json );

	return $json;
}

sub hafas_json_req {
	my ( $ua, $cache, $url ) = @_;

	if ( my $content = $cache->thaw($url) ) {
		return $content;
	}

	my $res = eval { $ua->get($url)->result };

	if ($@) {
		return;
	}
	if ( $res->is_error ) {
		return;
	}

	my $body = encode( 'utf-8', decode( 'ISO-8859-15', $res->body ) );

	$body =~ s{^TSLs[.]sls = }{};
	$body =~ s{;$}{};
	$body =~ s{(}{(}g;
	$body =~ s{)}{)}g;

	my $json = decode_json($body);

	$cache->freeze( $url, $json );

	return $json;
}

sub hafas_xml_req {
	my ( $ua, $cache, $url ) = @_;

	if ( my $content = $cache->thaw($url) ) {
		return $content;
	}

	my $res = eval { $ua->get($url)->result };

	if ($@) {
		return;
	}
	if ( $res->is_error ) {
		$cache->freeze( $url, {} );
		return;
	}

	my $body = decode( 'ISO-8859-15', $res->body );

	# <SDay text="... &gt; ..."> is invalid HTML, but present
	# regardless. As it is the last tag, we just throw it away.
	$body =~ s{<SDay [^>]*/>}{}s;

	my $tree;

	eval { $tree = XML::LibXML->load_xml( string => $body ) };

	if ($@) {
		$cache->freeze( $url, {} );
		return;
	}

	my $ret = {
		station  => {},
		stations => [],
		messages => [],
	};

	for my $station ( $tree->findnodes('/Journey/St') ) {
		my $name   = $station->getAttribute('name');
		my $adelay = $station->getAttribute('adelay');
		my $ddelay = $station->getAttribute('ddelay');
		push( @{ $ret->{stations} }, $name );
		$ret->{station}{$name} = {
			adelay => $adelay,
			ddelay => $ddelay,
		};
	}

	for my $message ( $tree->findnodes('/Journey/HIMMessage') ) {
		my $header  = $message->getAttribute('header');
		my $lead    = $message->getAttribute('lead');
		my $display = $message->getAttribute('display');
		push(
			@{ $ret->{messages} },
			{
				header  => $header,
				lead    => $lead,
				display => $display
			}
		);
	}

	$cache->freeze( $url, $ret );

	return $ret;
}

# quick&dirty, will be cleaned up later
sub get_route_timestamps {
	my ( $ua, $cache_main, $cache_rt, $opt ) = @_;

	$ua->request_timeout(3);

	my $base
	  = 'https://reiseauskunft.bahn.de/bin/trainsearch.exe/dn?L=vs_json&start=yes&rt=1';
	my ( $date_yy, $date_yyyy, $train_no, $train_origin );

	if ( $opt->{train} ) {
		$date_yy      = $opt->{train}->start->strftime('%d.%m.%y');
		$date_yyyy    = $opt->{train}->start->strftime('%d.%m.%Y');
		$train_no     = $opt->{train}->type . ' ' . $opt->{train}->train_no;
		$train_origin = $opt->{train}->origin;
	}
	else {
		my $now = DateTime->now( time_zone => 'Europe/Berlin' );
		$date_yy   = $now->strftime('%d.%m.%y');
		$date_yyyy = $now->strftime('%d.%m.%Y');
		$train_no  = $opt->{train_no};
	}

	my $trainsearch = hafas_json_req( $ua, $cache_main,
		"${base}&date=${date_yy}&trainname=${train_no}" );

	if ( not $trainsearch ) {
		return;
	}

	# Fallback: Take first result
	my $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 (
			exists $suggestion->{depDate}
			and (  $suggestion->{depDate} eq $date_yy
				or $suggestion->{depDate} eq $date_yyyy )
		  )
		{
			# Train numbers are not unique, e.g. IC 149 refers both to the
			# InterCity service Amsterdam -> Berlin and to the InterCity service
			# Koebenhavns Lufthavn st -> Aarhus.  One workaround is making
			# requests with the stationFilter=80 parameter.  Checking the origin
			# station seems to be the more generic solution, so we do that
			# instead.
			if ( $train_origin and $suggestion->{dep} eq $train_origin ) {
				$trainlink = $suggestion->{trainLink};
				last;
			}
		}
	}

	if ( not $trainlink ) {
		return;
	}

	$base = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn';

	my $traininfo = hafas_json_req( $ua, $cache_rt,
		"${base}/${trainlink}?rt=1&date=${date_yy}&L=vs_json" );

	if ( not $traininfo or $traininfo->{error} ) {
		return;
	}

	my $traindelay = hafas_xml_req( $ua, $cache_rt,
		"${base}/${trainlink}?rt=1&date=${date_yy}&L=vs_java3" );

	my $ret = {};

	my $strp = DateTime::Format::Strptime->new(
		pattern   => '%d.%m.%y %H:%M',
		time_zone => 'Europe/Berlin',
	);

	for my $station ( @{ $traininfo->{suggestions}[0]{locations} // [] } ) {
		my $name = $station->{name};
		my $arr  = $station->{arrDate} . ' ' . $station->{arrTime};
		my $dep  = $station->{depDate} . ' ' . $station->{depTime};
		$ret->{$name} = {
			sched_arr => scalar $strp->parse_datetime($arr),
			sched_dep => scalar $strp->parse_datetime($dep),
		};
		if ( exists $traindelay->{station}{$name} ) {
			my $delay = $traindelay->{station}{$name};
			if (    $ret->{$name}{sched_arr}
				and $delay->{adelay}
				and $delay->{adelay} =~ m{^\d+$} )
			{
				$ret->{$name}{rt_arr} = $ret->{$name}{sched_arr}
				  ->clone->add( minutes => $delay->{adelay} );
			}
			if (    $ret->{$name}{sched_dep}
				and $delay->{ddelay}
				and $delay->{ddelay} =~ m{^\d+$} )
			{
				$ret->{$name}{rt_dep} = $ret->{$name}{sched_dep}
				  ->clone->add( minutes => $delay->{ddelay} );
			}
		}
	}

	return ( $ret, $traindelay // {} );
}

sub get_results_for {
	my ( $backend, $station, %opt ) = @_;
	my $data;
@@ -725,8 +453,7 @@ sub render_train {
		)
	];

	$departure->{trip_id}
	  = get_hafas_trip_id( $self->ua, $self->app->cache_iris_main, $result );
	$departure->{trip_id} = $self->hafas->get_tripid($result);

	if (
		$departure->{wr_link}
@@ -739,12 +466,8 @@ sub render_train {
		$departure->{wr_link} = undef;
	}

	my ( $route_ts, $route_info ) = get_route_timestamps(
		$self->ua,
		$self->app->cache_iris_main,
		$self->app->cache_iris_rt,
		{ train => $result }
	);
	my ( $route_ts, $route_info )
	  = $self->hafas->get_route_timestamps( train => $result );

	# If a train number changes on the way, IRIS routes are incomplete,
	# whereas HAFAS data has all stops -> merge HAFAS stops into IRIS
+293 −0
Original line number Diff line number Diff line
package DBInfoscreen::Helper::HAFAS;

use strict;
use warnings;
use 5.020;

use DateTime;
use Encode qw(decode encode);
use Mojo::JSON qw(decode_json);
use XML::LibXML;

sub new {
	my ( $class, %opt ) = @_;

	my $version = $opt{version};

	$opt{header}
	  = { 'User-Agent' =>
		  "dbf/${version} +https://finalrewind.org/projects/db-fakedisplay" };

	return bless( \%opt, $class );

}

sub hafas_rest_req {
	my ( $self, $cache, $url ) = @_;

	if ( my $content = $cache->thaw($url) ) {
		return $content;
	}

	my $res = eval { $self->{user_agent}->get($url)->result; };

	if ($@) {
		return;
	}
	if ( $res->is_error ) {
		return;
	}

	my $json = decode_json( $res->body );

	$cache->freeze( $url, $json );

	return $json;
}

sub hafas_json_req {
	my ( $self, $cache, $url ) = @_;

	if ( my $content = $cache->thaw($url) ) {
		return $content;
	}

	my $res = eval { $self->{user_agent}->get($url)->result };

	if ($@) {
		return;
	}
	if ( $res->is_error ) {
		return;
	}

	my $body = encode( 'utf-8', decode( 'ISO-8859-15', $res->body ) );

	$body =~ s{^TSLs[.]sls = }{};
	$body =~ s{;$}{};
	$body =~ s{&#x0028;}{(}g;
	$body =~ s{&#x0029;}{)}g;

	my $json = decode_json($body);

	$cache->freeze( $url, $json );

	return $json;
}

sub hafas_xml_req {
	my ( $self, $cache, $url ) = @_;

	if ( my $content = $cache->thaw($url) ) {
		return $content;
	}

	my $res = eval { $self->{user_agent}->get($url)->result };

	if ($@) {
		return;
	}
	if ( $res->is_error ) {
		$cache->freeze( $url, {} );
		return;
	}

	my $body = decode( 'ISO-8859-15', $res->body );

	# <SDay text="... &gt; ..."> is invalid HTML, but present
	# regardless. As it is the last tag, we just throw it away.
	$body =~ s{<SDay [^>]*/>}{}s;

	my $tree;

	eval { $tree = XML::LibXML->load_xml( string => $body ) };

	if ($@) {
		$cache->freeze( $url, {} );
		return;
	}

	my $ret = {
		station  => {},
		stations => [],
		messages => [],
	};

	for my $station ( $tree->findnodes('/Journey/St') ) {
		my $name   = $station->getAttribute('name');
		my $adelay = $station->getAttribute('adelay');
		my $ddelay = $station->getAttribute('ddelay');
		push( @{ $ret->{stations} }, $name );
		$ret->{station}{$name} = {
			adelay => $adelay,
			ddelay => $ddelay,
		};
	}

	for my $message ( $tree->findnodes('/Journey/HIMMessage') ) {
		my $header  = $message->getAttribute('header');
		my $lead    = $message->getAttribute('lead');
		my $display = $message->getAttribute('display');
		push(
			@{ $ret->{messages} },
			{
				header  => $header,
				lead    => $lead,
				display => $display
			}
		);
	}

	$cache->freeze( $url, $ret );

	return $ret;
}

sub get_route_timestamps {
	my ( $self, %opt ) = @_;

	my $base
	  = 'https://reiseauskunft.bahn.de/bin/trainsearch.exe/dn?L=vs_json&start=yes&rt=1';
	my ( $date_yy, $date_yyyy, $train_no, $train_origin );

	if ( $opt{train} ) {
		$date_yy      = $opt{train}->start->strftime('%d.%m.%y');
		$date_yyyy    = $opt{train}->start->strftime('%d.%m.%Y');
		$train_no     = $opt{train}->type . ' ' . $opt{train}->train_no;
		$train_origin = $opt{train}->origin;
	}
	else {
		my $now = DateTime->now( time_zone => 'Europe/Berlin' );
		$date_yy   = $now->strftime('%d.%m.%y');
		$date_yyyy = $now->strftime('%d.%m.%Y');
		$train_no  = $opt{train_no};
	}

	my $trainsearch = $self->hafas_json_req( $self->{main_cache},
		"${base}&date=${date_yy}&trainname=${train_no}" );

	if ( not $trainsearch ) {
		return;
	}

	# Fallback: Take first result
	my $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 (
			exists $suggestion->{depDate}
			and (  $suggestion->{depDate} eq $date_yy
				or $suggestion->{depDate} eq $date_yyyy )
		  )
		{
			# Train numbers are not unique, e.g. IC 149 refers both to the
			# InterCity service Amsterdam -> Berlin and to the InterCity service
			# Koebenhavns Lufthavn st -> Aarhus.  One workaround is making
			# requests with the stationFilter=80 parameter.  Checking the origin
			# station seems to be the more generic solution, so we do that
			# instead.
			if ( $train_origin and $suggestion->{dep} eq $train_origin ) {
				$trainlink = $suggestion->{trainLink};
				last;
			}
		}
	}

	if ( not $trainlink ) {
		return;
	}

	$base = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn';

	my $traininfo = $self->hafas_json_req( $self->{realtime_cache},
		"${base}/${trainlink}?rt=1&date=${date_yy}&L=vs_json" );

	if ( not $traininfo or $traininfo->{error} ) {
		return;
	}

	my $traindelay = $self->hafas_xml_req( $self->{realtime_cache},
		"${base}/${trainlink}?rt=1&date=${date_yy}&L=vs_java3" );

	my $ret = {};

	my $strp = DateTime::Format::Strptime->new(
		pattern   => '%d.%m.%y %H:%M',
		time_zone => 'Europe/Berlin',
	);

	for my $station ( @{ $traininfo->{suggestions}[0]{locations} // [] } ) {
		my $name = $station->{name};
		my $arr  = $station->{arrDate} . ' ' . $station->{arrTime};
		my $dep  = $station->{depDate} . ' ' . $station->{depTime};
		$ret->{$name} = {
			sched_arr => scalar $strp->parse_datetime($arr),
			sched_dep => scalar $strp->parse_datetime($dep),
		};
		if ( exists $traindelay->{station}{$name} ) {
			my $delay = $traindelay->{station}{$name};
			if (    $ret->{$name}{sched_arr}
				and $delay->{adelay}
				and $delay->{adelay} =~ m{^\d+$} )
			{
				$ret->{$name}{rt_arr} = $ret->{$name}{sched_arr}
				  ->clone->add( minutes => $delay->{adelay} );
			}
			if (    $ret->{$name}{sched_dep}
				and $delay->{ddelay}
				and $delay->{ddelay} =~ m{^\d+$} )
			{
				$ret->{$name}{rt_dep} = $ret->{$name}{sched_dep}
				  ->clone->add( minutes => $delay->{ddelay} );
			}
		}
	}

	return ( $ret, $traindelay // {} );
}

sub get_tripid {
	my ( $self, $train ) = @_;

	my $cache = $self->{main_cache};
	my $eva   = $train->station_uic;

	my $dep_ts = DateTime->now( time_zone => 'Europe/Berlin' );
	my $url
	  = "https://2.db.transport.rest/stations/${eva}/departures?duration=5&when=$dep_ts";

	if ( $train->sched_departure ) {
		$dep_ts = $train->sched_departure->epoch;
		$url
		  = "https://2.db.transport.rest/stations/${eva}/departures?duration=5&when=$dep_ts";
	}
	elsif ( $train->sched_arrival ) {
		$dep_ts = $train->sched_arrival->epoch;
		$url
		  = "https://2.db.transport.rest/stations/${eva}/arrivals?duration=5&when=$dep_ts";
	}

	my $json = $self->hafas_rest_req( $cache, $url );

	#say "looking for " . $train->train_no . " in $url";
	for my $result ( @{ $json // [] } ) {
		my $trip_id = $result->{tripId};
		my $fahrt   = $result->{line}{fahrtNr};

		#say "checking $fahrt";
		if ( $result->{line} and $result->{line}{fahrtNr} == $train->train_no )
		{
			#say "Trip ID is $trip_id";
			return $trip_id;
		}
		else {
			#say "unmatched Trip ID $trip_id";
		}
	}
	return;
}

1;