Add Eurostar Plus prices and NR advance fare support
- Eurostar scraper now fetches both Standard and Plus (PLUS class code) prices/seats in a single API call; each service dict gains plus_price and plus_seats fields - GWR fares scraper gains fetch_advance() which makes two sets of paginated calls (standard advance + first-class advance) and returns cheapest per departure; shared _run_pages() generator reduces duplication in fetch() - New /api/advance_fares/<station_crs>/<travel_date> endpoint returns advance fares as JSON, cached for 24 hours - Results page gains NR ticket selector (Walk-on / Std Advance / 1st Advance) and Eurostar selector (Standard / Plus); total column is JS-computed from the selected combination with cheapest/priciest highlighting - Load advance prices button fetches the API lazily; if advance fares are already cached they are embedded in the page and applied on load so the button is hidden automatically Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5583a20143
commit
89a536dfd3
8 changed files with 515 additions and 83 deletions
|
|
@ -68,6 +68,9 @@ _GQL_QUERY = (
|
|||
"}"
|
||||
)
|
||||
|
||||
_STANDARD = 'STANDARD'
|
||||
_STANDARD_PLUS = 'PLUS'
|
||||
|
||||
|
||||
def search_url(destination: str, travel_date: str) -> str:
|
||||
dest_id = DESTINATION_STATION_IDS[destination]
|
||||
|
|
@ -87,7 +90,7 @@ def _parse_graphql(data: dict, destination: str) -> list[dict]:
|
|||
Parse a NewBookingSearch GraphQL response into a list of service dicts.
|
||||
|
||||
Each dict contains: depart_st_pancras, arrive_destination, destination,
|
||||
train_number, price (float or None), seats (int or None).
|
||||
train_number, price/seats (Standard), plus_price/plus_seats (Standard Premier).
|
||||
|
||||
The same St Pancras departure can appear multiple times (different
|
||||
connecting trains); we keep the entry with the earliest arrival.
|
||||
|
|
@ -98,26 +101,34 @@ def _parse_graphql(data: dict, destination: str) -> list[dict]:
|
|||
for journey in journeys:
|
||||
dep = journey['timing']['departureTime']
|
||||
arr = journey['timing']['arrivalTime']
|
||||
for fare in journey['fares']:
|
||||
if fare['classOfService']['code'] == 'STANDARD':
|
||||
p = fare.get('prices')
|
||||
price = float(p['displayPrice']) if p and p.get('displayPrice') else None
|
||||
seats = fare.get('seats')
|
||||
std_price = std_seats = plus_price = plus_seats = None
|
||||
train_number = ''
|
||||
for fare in (journey.get('fares') or []):
|
||||
cos = fare['classOfService']['code']
|
||||
p = fare.get('prices')
|
||||
price = float(p['displayPrice']) if p and p.get('displayPrice') else None
|
||||
seats = fare.get('seats')
|
||||
if not train_number:
|
||||
legs = fare.get('legs') or []
|
||||
train_number = ' + '.join(
|
||||
f"{(leg.get('serviceType') or {}).get('code', 'ES')} {leg['serviceName']}"
|
||||
for leg in legs if leg.get('serviceName')
|
||||
)
|
||||
if dep not in best or arr < best[dep]['arrive_destination']:
|
||||
best[dep] = {
|
||||
'depart_st_pancras': dep,
|
||||
'arrive_destination': arr,
|
||||
'destination': destination,
|
||||
'train_number': train_number,
|
||||
'price': price,
|
||||
'seats': seats,
|
||||
}
|
||||
break
|
||||
if cos == _STANDARD:
|
||||
std_price, std_seats = price, seats
|
||||
elif cos == _STANDARD_PLUS:
|
||||
plus_price, plus_seats = price, seats
|
||||
if dep not in best or arr < best[dep]['arrive_destination']:
|
||||
best[dep] = {
|
||||
'depart_st_pancras': dep,
|
||||
'arrive_destination': arr,
|
||||
'destination': destination,
|
||||
'train_number': train_number,
|
||||
'price': std_price,
|
||||
'seats': std_seats,
|
||||
'plus_price': plus_price,
|
||||
'plus_seats': plus_seats,
|
||||
}
|
||||
return sorted(best.values(), key=lambda s: s['depart_st_pancras'])
|
||||
|
||||
|
||||
|
|
@ -148,7 +159,7 @@ def fetch(destination: str, travel_date: str) -> list[dict]:
|
|||
'outbound': travel_date,
|
||||
'currency': 'GBP',
|
||||
'adult': 1,
|
||||
'filteredClassesOfService': ['STANDARD'],
|
||||
'filteredClassesOfService': [_STANDARD, _STANDARD_PLUS],
|
||||
},
|
||||
'query': _GQL_QUERY,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue