Use qty dropdown for availability count, not just scarcity text

The 'We have X left' scarcity indicator only appears when availability is
low (typically ≤5). The quantity <select> dropdown in the booking form
always shows 0..N where N = actual available qty (capped at 10).

Extract max option value from the <select> in the last table cell per rate
plan row, falling back to regex parsing of the cell text if the select
isn't rendered, then to the scarcity text as a last resort.

Stored in both rooms_left and available_qty on RateData.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-09 23:20:15 +00:00
parent 5462773dd7
commit f2e11bf585

View file

@ -73,15 +73,39 @@ _EXTRACT_RATES_JS = """
const cancelMatch = condCell.match(/free cancellation before ([\\w\\s]+)/i);
const freeCancelText = cancelMatch ? cancelMatch[1].trim() : null;
// Persons: first <td> ("Max persons: 2" or "Only for 1 guest")
// Persons: first <td> ("Max persons: 2" or "Only for 1 guest")
const personsCell = cells.length >= 1 ? cells[0].innerText || '' : '';
const personsMatch = personsCell.match(/\\d+/);
const maxPersons = personsMatch ? parseInt(personsMatch[0]) : null;
// Quantity dropdown: last <td> has a <select> with options 0..N
// where N = actual available qty (capped at 10 by Booking.com).
// This is more reliable than the "X left" scarcity text which only
// appears when availability is low (typically 5).
let qtyAvailable = availCount; // fallback to scarcity text
const qtyCell = cells.length >= 4 ? cells[cells.length - 1] : null;
if (qtyCell) {
const sel = qtyCell.querySelector('select');
if (sel) {
const vals = Array.from(sel.options)
.map(o => parseInt(o.value))
.filter(v => !isNaN(v) && v > 0);
if (vals.length > 0) qtyAvailable = Math.max(...vals);
} else {
// Fallback: parse max number from cell text
// "Select rooms\\n0\\n1 (£208)\\n2 (£416)" [1,2] max 2
const nums = (qtyCell.innerText || '').match(/^(\\d+)/gm);
if (nums && nums.length > 0) {
const parsed = nums.map(n => parseInt(n)).filter(v => v > 0);
if (parsed.length > 0) qtyAvailable = Math.max(...parsed);
}
}
}
results.push({
room_id: roomId,
room_name: roomName,
avail_count: availCount,
avail_count: qtyAvailable,
block_id: blockId,
price: priceRaw ? parseInt(priceRaw) : null,
breakfast_included: breakfastIncluded,
@ -282,7 +306,8 @@ class PlaywrightHotelPageBackend(ScraperBackend):
room_type=plan['room_name'] or None,
breakfast_included=plan['breakfast_included'],
free_cancellation=not plan['non_refundable'],
rooms_left=plan['avail_count'],
rooms_left=plan['avail_count'], # qty from dropdown (max 10) or scarcity text
available_qty=plan['avail_count'],
rate_plan_id=plan['block_id'] or None,
max_persons=plan['max_persons'],
))