Various improvements
This commit is contained in:
parent
876eb6a759
commit
1fa2e68b31
5 changed files with 101 additions and 8 deletions
37
app.py
37
app.py
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 → St 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 → St 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">
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,47 @@
|
||||||
Bristol Temple Meads → {{ destination }}
|
Bristol Temple Meads → {{ 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">← Prev</a>
|
text-decoration:none;color:#00539f;font-size:0.9rem">← 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 →</a>
|
text-decoration:none;color:#00539f;font-size:0.9rem">Next →</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 }}
|
||||||
·
|
·
|
||||||
|
|
@ -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 → St Pancras connection: 60 min minimum, 2h maximum.
|
Paddington → St Pancras connection: {{ min_connection }}–{{ max_connection }} 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.
|
||||||
·
|
·
|
||||||
|
|
@ -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 + Eurostar combination allows an 80-minute connection at Paddington/St Pancras.
|
No GWR + Eurostar combination has at least a {{ min_connection }}-minute connection at Paddington/St Pancras.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue