""" Combine GWR Bristol→Paddington trains with Eurostar St Pancras→destination trains. """ from flask import Flask, render_template, redirect, url_for, request from datetime import date, timedelta import os from cache import get_cached, set_cached import scraper.eurostar as eurostar_scraper import scraper.realtime_trains as rtt_scraper from trip_planner import combine_trips, find_unreachable_morning_eurostars from scraper.eurostar import fetch_prices as fetch_eurostar_prices RTT_PADDINGTON_URL = ( "https://www.realtimetrains.co.uk/search/detailed/" "gb-nr:PAD/from/gb-nr:BRI/{date}/0000-2359" "?stp=WVS&show=pax-calls&order=wtt" ) RTT_BRISTOL_URL = ( "https://www.realtimetrains.co.uk/search/detailed/" "gb-nr:BRI/to/gb-nr:PAD/{date}/0000-2359" "?stp=WVS&show=pax-calls&order=wtt" ) app = Flask(__name__, instance_relative_config=False) app.config.from_object('config.default') _local = os.path.join(os.path.dirname(__file__), 'config', 'local.py') if os.path.exists(_local): app.config.from_pyfile(_local) import cache import circle_line cache.CACHE_DIR = app.config['CACHE_DIR'] circle_line._TXC_XML = app.config['CIRCLE_LINE_XML'] DESTINATIONS = { 'paris': 'Paris Gare du Nord', 'brussels': 'Brussels Midi', 'lille': 'Lille Europe', 'amsterdam': 'Amsterdam Centraal', 'rotterdam': 'Rotterdam Centraal', } @app.route('/') def index(): today = date.today().isoformat() return render_template( 'index.html', destinations=DESTINATIONS, today=today, valid_min_connections=sorted(VALID_MIN_CONNECTIONS), valid_max_connections=sorted(VALID_MAX_CONNECTIONS), ) VALID_MIN_CONNECTIONS = {45, 50, 60, 70, 80, 90, 100, 110, 120} VALID_MAX_CONNECTIONS = {60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180} @app.route('/search') def search(): slug = request.args.get('destination', '') travel_date = request.args.get('travel_date', '') try: min_conn = int(request.args.get('min_connection', 50)) except ValueError: min_conn = 50 if min_conn not in VALID_MIN_CONNECTIONS: min_conn = 50 try: max_conn = int(request.args.get('max_connection', 110)) except ValueError: max_conn = 110 if max_conn not in VALID_MAX_CONNECTIONS: max_conn = 110 if slug in DESTINATIONS and travel_date: return redirect(url_for('results', slug=slug, travel_date=travel_date, min_connection=min_conn, max_connection=max_conn)) return redirect(url_for('index')) @app.route('/results//') def results(slug, travel_date): destination = DESTINATIONS.get(slug) if not destination or not travel_date: return redirect(url_for('index')) try: min_connection = int(request.args.get('min_connection', 50)) except ValueError: min_connection = 50 if min_connection not in VALID_MIN_CONNECTIONS: min_connection = 50 try: max_connection = int(request.args.get('max_connection', 110)) except ValueError: max_connection = 110 if max_connection not in VALID_MAX_CONNECTIONS: max_connection = 110 user_agent = request.headers.get('User-Agent', rtt_scraper.DEFAULT_UA) rtt_cache_key = f"rtt_{travel_date}" es_cache_key = f"eurostar_{travel_date}_{destination}" prices_cache_key = f"eurostar_prices_{travel_date}_{destination}" cached_rtt = get_cached(rtt_cache_key) cached_es = get_cached(es_cache_key) cached_prices = get_cached(prices_cache_key, ttl=24 * 3600) from_cache = bool(cached_rtt and cached_es and cached_prices) error = None if cached_rtt: gwr_trains = cached_rtt else: try: gwr_trains = rtt_scraper.fetch(travel_date, user_agent) set_cached(rtt_cache_key, gwr_trains) except Exception as e: gwr_trains = [] error = f"Could not fetch GWR trains: {e}" if cached_es: eurostar_trains = cached_es else: try: eurostar_trains = eurostar_scraper.fetch(destination, travel_date, user_agent) set_cached(es_cache_key, eurostar_trains) except Exception as e: eurostar_trains = [] msg = f"Could not fetch Eurostar times: {e}" error = f"{error}; {msg}" if error else msg if cached_prices: eurostar_prices = cached_prices else: try: eurostar_prices = fetch_eurostar_prices(destination, travel_date) set_cached(prices_cache_key, eurostar_prices) except Exception as e: eurostar_prices = {} msg = f"Could not fetch Eurostar prices: {e}" error = f"{error}; {msg}" if error else msg trips = combine_trips(gwr_trains, eurostar_trains, travel_date, min_connection, max_connection) # Annotate each trip with Eurostar Standard price, seats, and total cost for trip in trips: es = eurostar_prices.get(trip['depart_st_pancras'], {}) es_price = es.get('price') trip['eurostar_price'] = es_price trip['eurostar_seats'] = es.get('seats') trip['total_price'] = trip['ticket_price'] + es_price if es_price is not None else None # If the API returned journeys but every price is None, tickets aren't on sale yet no_prices_note = None if eurostar_prices and all(v.get('price') is None for v in eurostar_prices.values()): no_prices_note = 'Eurostar prices not yet available — tickets may not be on sale yet.' unreachable_morning_services = find_unreachable_morning_eurostars( gwr_trains, eurostar_trains, travel_date, min_connection, max_connection, ) for svc in unreachable_morning_services: es = eurostar_prices.get(svc['depart_st_pancras'], {}) svc['eurostar_price'] = es.get('price') svc['eurostar_seats'] = es.get('seats') result_rows = sorted( [{'row_type': 'trip', **trip} for trip in trips] + [{'row_type': 'unreachable', **service} for service in unreachable_morning_services], key=lambda row: row['depart_st_pancras'], ) dt = date.fromisoformat(travel_date) prev_date = (dt - timedelta(days=1)).isoformat() next_date = (dt + timedelta(days=1)).isoformat() travel_date_display = dt.strftime('%A %-d %B %Y') eurostar_url = eurostar_scraper.search_url(destination, travel_date) rtt_url = RTT_PADDINGTON_URL.format(date=travel_date) rtt_bristol_url = RTT_BRISTOL_URL.format(date=travel_date) return render_template( 'results.html', trips=trips, result_rows=result_rows, unreachable_morning_services=unreachable_morning_services, destinations=DESTINATIONS, destination=destination, travel_date=travel_date, slug=slug, prev_date=prev_date, next_date=next_date, travel_date_display=travel_date_display, gwr_count=len(gwr_trains), eurostar_count=len(eurostar_trains), from_cache=from_cache, error=error, no_prices_note=no_prices_note, eurostar_url=eurostar_url, rtt_url=rtt_url, rtt_bristol_url=rtt_bristol_url, min_connection=min_connection, max_connection=max_connection, valid_min_connections=sorted(VALID_MIN_CONNECTIONS), valid_max_connections=sorted(VALID_MAX_CONNECTIONS), ) if __name__ == '__main__': app.run(debug=True)