Various improvements

This commit is contained in:
Edward Betts 2026-03-31 12:59:44 +01:00
parent 876eb6a759
commit 1fa2e68b31
5 changed files with 101 additions and 8 deletions

37
app.py
View file

@ -31,12 +31,28 @@ def index():
return render_template('index.html', destinations=DESTINATIONS, today=today) return render_template('index.html', destinations=DESTINATIONS, today=today)
VALID_MIN_CONNECTIONS = {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') @app.route('/search')
def search(): def search():
slug = request.args.get('destination', '') slug = request.args.get('destination', '')
travel_date = request.args.get('travel_date', '') 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: if slug in DESTINATIONS and travel_date:
return redirect(url_for('results', slug=slug, travel_date=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')) return redirect(url_for('index'))
@ -46,6 +62,19 @@ def results(slug, travel_date):
if not destination or not travel_date: if not destination or not travel_date:
return redirect(url_for('index')) 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) user_agent = request.headers.get('User-Agent', rtt_scraper.DEFAULT_UA)
rtt_cache_key = f"rtt_{travel_date}" rtt_cache_key = f"rtt_{travel_date}"
@ -78,7 +107,7 @@ def results(slug, travel_date):
msg = f"Could not fetch Eurostar times: {e}" msg = f"Could not fetch Eurostar times: {e}"
error = f"{error}; {msg}" if error else msg error = f"{error}; {msg}" if error else msg
trips = combine_trips(gwr_trains, eurostar_trains, travel_date) trips = combine_trips(gwr_trains, eurostar_trains, travel_date, min_connection, max_connection)
dt = date.fromisoformat(travel_date) dt = date.fromisoformat(travel_date)
prev_date = (dt - timedelta(days=1)).isoformat() prev_date = (dt - timedelta(days=1)).isoformat()
@ -103,6 +132,10 @@ def results(slug, travel_date):
error=error, error=error,
eurostar_url=eurostar_url, eurostar_url=eurostar_url,
rtt_url=rtt_url, rtt_url=rtt_url,
min_connection=min_connection,
max_connection=max_connection,
valid_min_connections=sorted(VALID_MIN_CONNECTIONS),
valid_max_connections=sorted(VALID_MAX_CONNECTIONS),
) )

View file

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bristol to Europe via Eurostar</title> <title>Bristol to Europe via Eurostar</title>
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<style> <style>
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }

View file

@ -25,6 +25,30 @@
style="width:100%;padding:0.6rem 0.8rem;font-size:1rem;border:1px solid #cbd5e0;border-radius:4px"> style="width:100%;padding:0.6rem 0.8rem;font-size:1rem;border:1px solid #cbd5e0;border-radius:4px">
</div> </div>
<div style="margin-bottom:1.2rem">
<label for="min_connection" style="display:block;font-weight:600;margin-bottom:0.4rem">
Minimum connection time (Paddington &rarr; St&nbsp;Pancras)
</label>
<select id="min_connection" name="min_connection"
style="width:100%;padding:0.6rem 0.8rem;font-size:1rem;border:1px solid #cbd5e0;border-radius:4px">
{% for mins in [50, 60, 70, 80, 90, 100, 110, 120] %}
<option value="{{ mins }}" {% if mins == 50 %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
</div>
<div style="margin-bottom:1.5rem">
<label for="max_connection" style="display:block;font-weight:600;margin-bottom:0.4rem">
Maximum connection time (Paddington &rarr; St&nbsp;Pancras)
</label>
<select id="max_connection" name="max_connection"
style="width:100%;padding:0.6rem 0.8rem;font-size:1rem;border:1px solid #cbd5e0;border-radius:4px">
{% for mins in [60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180] %}
<option value="{{ mins }}" {% if mins == 110 %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
</div>
<button type="submit" <button type="submit"
style="background:#00539f;color:#fff;border:none;padding:0.75rem 2rem; style="background:#00539f;color:#fff;border:none;padding:0.75rem 2rem;
font-size:1rem;font-weight:600;border-radius:4px;cursor:pointer"> font-size:1rem;font-weight:600;border-radius:4px;cursor:pointer">

View file

@ -10,14 +10,47 @@
Bristol Temple Meads &rarr; {{ destination }} Bristol Temple Meads &rarr; {{ destination }}
</h2> </h2>
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem"> <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem">
<a href="{{ url_for('results', slug=slug, travel_date=prev_date) }}" <a href="{{ url_for('results', slug=slug, travel_date=prev_date, min_connection=min_connection, max_connection=max_connection) }}"
style="padding:0.3rem 0.75rem;border:1px solid #cbd5e0;border-radius:4px; style="padding:0.3rem 0.75rem;border:1px solid #cbd5e0;border-radius:4px;
text-decoration:none;color:#00539f;font-size:0.9rem">&larr; Prev</a> text-decoration:none;color:#00539f;font-size:0.9rem">&larr; Prev</a>
<strong>{{ travel_date_display }}</strong> <strong>{{ travel_date_display }}</strong>
<a href="{{ url_for('results', slug=slug, travel_date=next_date) }}" <a href="{{ url_for('results', slug=slug, travel_date=next_date, min_connection=min_connection, max_connection=max_connection) }}"
style="padding:0.3rem 0.75rem;border:1px solid #cbd5e0;border-radius:4px; style="padding:0.3rem 0.75rem;border:1px solid #cbd5e0;border-radius:4px;
text-decoration:none;color:#00539f;font-size:0.9rem">Next &rarr;</a> text-decoration:none;color:#00539f;font-size:0.9rem">Next &rarr;</a>
</div> </div>
<div style="margin-top:0.75rem;display:flex;gap:1.5rem;align-items:center">
<div>
<label for="min_conn_select" style="font-size:0.9rem;font-weight:600;margin-right:0.5rem">
Min connection:
</label>
<select id="min_conn_select"
onchange="applyConnectionFilter()"
style="padding:0.3rem 0.6rem;font-size:0.9rem;border:1px solid #cbd5e0;border-radius:4px">
{% for mins in valid_min_connections %}
<option value="{{ mins }}" {% if mins == min_connection %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
</div>
<div>
<label for="max_conn_select" style="font-size:0.9rem;font-weight:600;margin-right:0.5rem">
Max connection:
</label>
<select id="max_conn_select"
onchange="applyConnectionFilter()"
style="padding:0.3rem 0.6rem;font-size:0.9rem;border:1px solid #cbd5e0;border-radius:4px">
{% for mins in valid_max_connections %}
<option value="{{ mins }}" {% if mins == max_connection %}selected{% endif %}>{{ mins }} min</option>
{% endfor %}
</select>
</div>
</div>
<script>
function applyConnectionFilter() {
var min = document.getElementById('min_conn_select').value;
var max = document.getElementById('max_conn_select').value;
window.location = '{{ url_for('results', slug=slug, travel_date=travel_date) }}?min_connection=' + min + '&max_connection=' + max;
}
</script>
<p style="color:#4a5568;margin:0"> <p style="color:#4a5568;margin:0">
{{ gwr_count }} GWR service{{ 's' if gwr_count != 1 }} {{ gwr_count }} GWR service{{ 's' if gwr_count != 1 }}
&nbsp;&middot;&nbsp; &nbsp;&middot;&nbsp;
@ -96,7 +129,7 @@
</div> </div>
<p style="margin-top:1rem;font-size:0.82rem;color:#718096"> <p style="margin-top:1rem;font-size:0.82rem;color:#718096">
Paddington &rarr; St&nbsp;Pancras connection: 60&nbsp;min minimum, 2h maximum. Paddington &rarr; St&nbsp;Pancras connection: {{ min_connection }}&ndash;{{ max_connection }}&nbsp;min.
Eurostar times are from the general timetable and may vary; always check Eurostar times are from the general timetable and may vary; always check
<a href="{{ eurostar_url }}" target="_blank" rel="noopener">eurostar.com</a> to book. <a href="{{ eurostar_url }}" target="_blank" rel="noopener">eurostar.com</a> to book.
&nbsp;&middot;&nbsp; &nbsp;&middot;&nbsp;
@ -114,7 +147,7 @@
{% elif eurostar_count == 0 %} {% elif eurostar_count == 0 %}
No Eurostar services found for {{ destination }} on this date. No Eurostar services found for {{ destination }} on this date.
{% else %} {% else %}
No GWR&nbsp;+&nbsp;Eurostar combination allows an 80-minute connection at Paddington/St&nbsp;Pancras. No GWR&nbsp;+&nbsp;Eurostar combination has at least a {{ min_connection }}-minute connection at Paddington/St&nbsp;Pancras.
{% endif %} {% endif %}
</p> </p>
</div> </div>

View file

@ -27,6 +27,8 @@ def combine_trips(
gwr_trains: list[dict], gwr_trains: list[dict],
eurostar_trains: list[dict], eurostar_trains: list[dict],
travel_date: str, travel_date: str,
min_connection_minutes: int = MIN_CONNECTION_MINUTES,
max_connection_minutes: int = MAX_CONNECTION_MINUTES,
) -> list[dict]: ) -> list[dict]:
""" """
Return a list of valid combined trips, sorted by Bristol departure time. Return a list of valid combined trips, sorted by Bristol departure time.
@ -53,7 +55,7 @@ def combine_trips(
if int((arr_pad - dep_bri).total_seconds() / 60) > MAX_GWR_MINUTES: if int((arr_pad - dep_bri).total_seconds() / 60) > MAX_GWR_MINUTES:
continue continue
earliest_eurostar = arr_pad + timedelta(minutes=MIN_CONNECTION_MINUTES) earliest_eurostar = arr_pad + timedelta(minutes=min_connection_minutes)
# Find only the earliest viable Eurostar for this GWR departure # Find only the earliest viable Eurostar for this GWR departure
for es in eurostar_trains: for es in eurostar_trains:
@ -69,7 +71,7 @@ def combine_trips(
if dep_stp < earliest_eurostar: if dep_stp < earliest_eurostar:
continue continue
if (dep_stp - arr_pad).total_seconds() / 60 > MAX_CONNECTION_MINUTES: if (dep_stp - arr_pad).total_seconds() / 60 > max_connection_minutes:
continue continue
total_mins = int((arr_dest - dep_bri).total_seconds() / 60) total_mins = int((arr_dest - dep_bri).total_seconds() / 60)