hotelzero 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +521 -0
- package/dist/browser.d.ts +119 -0
- package/dist/browser.js +731 -0
- package/dist/debug-filters.d.ts +1 -0
- package/dist/debug-filters.js +72 -0
- package/dist/debug-sponsored.d.ts +1 -0
- package/dist/debug-sponsored.js +87 -0
- package/dist/debug.d.ts +1 -0
- package/dist/debug.js +37 -0
- package/dist/extract-filters.d.ts +1 -0
- package/dist/extract-filters.js +96 -0
- package/dist/final-test.d.ts +1 -0
- package/dist/final-test.js +46 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +494 -0
- package/dist/test-mcp.d.ts +1 -0
- package/dist/test-mcp.js +61 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +37 -0
- package/package.json +54 -0
package/dist/browser.js
ADDED
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
// Booking.com filter code mappings
|
|
3
|
+
const FILTER_CODES = {
|
|
4
|
+
// Property Types
|
|
5
|
+
propertyType: {
|
|
6
|
+
hotel: "ht_id=204",
|
|
7
|
+
apartment: "ht_id=201",
|
|
8
|
+
resort: "ht_id=206",
|
|
9
|
+
villa: "ht_id=213",
|
|
10
|
+
vacation_home: "ht_id=220",
|
|
11
|
+
hostel: "ht_id=203",
|
|
12
|
+
bnb: "ht_id=208",
|
|
13
|
+
guesthouse: "ht_id=216",
|
|
14
|
+
homestay: "ht_id=222",
|
|
15
|
+
motel: "ht_id=205",
|
|
16
|
+
inn: "ht_id=207",
|
|
17
|
+
lodge: "ht_id=221",
|
|
18
|
+
chalet: "ht_id=228",
|
|
19
|
+
campground: "ht_id=214",
|
|
20
|
+
glamping: "ht_id=224",
|
|
21
|
+
boat: "ht_id=215",
|
|
22
|
+
capsule: "ht_id=225",
|
|
23
|
+
ryokan: "ht_id=218",
|
|
24
|
+
riad: "ht_id=226",
|
|
25
|
+
country_house: "ht_id=223",
|
|
26
|
+
farm_stay: "ht_id=210",
|
|
27
|
+
},
|
|
28
|
+
// Star ratings
|
|
29
|
+
starRating: {
|
|
30
|
+
1: "class=1",
|
|
31
|
+
2: "class=2",
|
|
32
|
+
3: "class=3",
|
|
33
|
+
4: "class=4",
|
|
34
|
+
5: "class=5",
|
|
35
|
+
},
|
|
36
|
+
// Hotel facilities
|
|
37
|
+
hotelfacility: {
|
|
38
|
+
freeWifi: 107,
|
|
39
|
+
pool: 433,
|
|
40
|
+
spa: 54,
|
|
41
|
+
parking: 2,
|
|
42
|
+
restaurant: 3,
|
|
43
|
+
bar: 8,
|
|
44
|
+
roomService: 5,
|
|
45
|
+
airportShuttle: 17,
|
|
46
|
+
hotTub: 63,
|
|
47
|
+
sauna: 79,
|
|
48
|
+
garden: 104,
|
|
49
|
+
terrace: 6,
|
|
50
|
+
nonSmokingRooms: 16,
|
|
51
|
+
familyRooms: 28,
|
|
52
|
+
disabledFacilities: 25,
|
|
53
|
+
evCharging: 182,
|
|
54
|
+
tennis: 90,
|
|
55
|
+
casino: 78,
|
|
56
|
+
bbqFacilities: 106,
|
|
57
|
+
laundry: 52,
|
|
58
|
+
concierge: 118,
|
|
59
|
+
currencyExchange: 91,
|
|
60
|
+
businessCenter: 10,
|
|
61
|
+
meetingRooms: 126,
|
|
62
|
+
},
|
|
63
|
+
// Room facilities
|
|
64
|
+
roomfacility: {
|
|
65
|
+
airConditioning: 11,
|
|
66
|
+
kitchen: 999,
|
|
67
|
+
balcony: 17,
|
|
68
|
+
privatePool: 93,
|
|
69
|
+
privateBathroom: 38,
|
|
70
|
+
bath: 67,
|
|
71
|
+
tv: 75,
|
|
72
|
+
minibar: 6,
|
|
73
|
+
safe: 14,
|
|
74
|
+
washingMachine: 98,
|
|
75
|
+
soundproofing: 133,
|
|
76
|
+
oceanView: 108,
|
|
77
|
+
},
|
|
78
|
+
// Bed types
|
|
79
|
+
bedType: {
|
|
80
|
+
king: "tdb=6",
|
|
81
|
+
queen: "tdb=5",
|
|
82
|
+
double: "tdb=3",
|
|
83
|
+
twin: "tdb=2",
|
|
84
|
+
single: "tdb=1",
|
|
85
|
+
},
|
|
86
|
+
// Meal plans
|
|
87
|
+
mealplan: {
|
|
88
|
+
breakfast: 1,
|
|
89
|
+
allInclusive: 4,
|
|
90
|
+
selfCatering: 999,
|
|
91
|
+
},
|
|
92
|
+
// Stay type
|
|
93
|
+
stayType: {
|
|
94
|
+
petFriendly: 1,
|
|
95
|
+
adultsOnly: 2,
|
|
96
|
+
lgbtqFriendly: 4,
|
|
97
|
+
},
|
|
98
|
+
// Cancellation
|
|
99
|
+
fc: {
|
|
100
|
+
freeCancellation: 2,
|
|
101
|
+
noPrepayment: 5,
|
|
102
|
+
noBookingFee: 4,
|
|
103
|
+
},
|
|
104
|
+
// Distance from center (in meters)
|
|
105
|
+
distance: {
|
|
106
|
+
half_mile: 805,
|
|
107
|
+
"1_mile": 1610,
|
|
108
|
+
"2_miles": 3220,
|
|
109
|
+
},
|
|
110
|
+
// Popular activities
|
|
111
|
+
popularActivities: {
|
|
112
|
+
fitness: 11,
|
|
113
|
+
beach: 302,
|
|
114
|
+
golf: 12,
|
|
115
|
+
snorkeling: 90,
|
|
116
|
+
diving: 86,
|
|
117
|
+
fishing: 19,
|
|
118
|
+
hiking: 253,
|
|
119
|
+
cycling: 255,
|
|
120
|
+
skiing: 13,
|
|
121
|
+
waterSports: 410,
|
|
122
|
+
horseRiding: 407,
|
|
123
|
+
},
|
|
124
|
+
// Accessibility - hotel level
|
|
125
|
+
accessibleFacilities: {
|
|
126
|
+
grabRails: 186,
|
|
127
|
+
raisedToilet: 187,
|
|
128
|
+
loweredSink: 188,
|
|
129
|
+
emergencyCord: 189,
|
|
130
|
+
braille: 211,
|
|
131
|
+
tactileSigns: 212,
|
|
132
|
+
auditoryGuidance: 213,
|
|
133
|
+
},
|
|
134
|
+
// Accessibility - room level
|
|
135
|
+
accessibleRoomFacilities: {
|
|
136
|
+
wheelchairAccessible: 134,
|
|
137
|
+
groundFloor: 131,
|
|
138
|
+
elevatorAccess: 132,
|
|
139
|
+
walkInShower: 150,
|
|
140
|
+
rollInShower: 149,
|
|
141
|
+
showerChair: 154,
|
|
142
|
+
adaptedBath: 148,
|
|
143
|
+
roomGrabRails: 147,
|
|
144
|
+
roomLoweredSink: 152,
|
|
145
|
+
roomRaisedToilet: 151,
|
|
146
|
+
roomEmergencyCord: 153,
|
|
147
|
+
},
|
|
148
|
+
// Hotel chains
|
|
149
|
+
chaincode: {
|
|
150
|
+
marriott: 1080,
|
|
151
|
+
hilton: 1078,
|
|
152
|
+
hyatt: 3632,
|
|
153
|
+
ihg: 1072,
|
|
154
|
+
wyndham: 1048,
|
|
155
|
+
best_western: 1035,
|
|
156
|
+
accor: 1025,
|
|
157
|
+
choice: 1040,
|
|
158
|
+
radisson: 1084,
|
|
159
|
+
ritz_carlton: 1094,
|
|
160
|
+
four_seasons: 1061,
|
|
161
|
+
fairmont: 1060,
|
|
162
|
+
sheraton: 1085,
|
|
163
|
+
westin: 1096,
|
|
164
|
+
w_hotels: 1097,
|
|
165
|
+
courtyard: 1093,
|
|
166
|
+
residence_inn: 1098,
|
|
167
|
+
hampton: 1075,
|
|
168
|
+
embassy_suites: 1983,
|
|
169
|
+
doubletree: 1044,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
export class HotelBrowser {
|
|
173
|
+
browser = null;
|
|
174
|
+
page = null;
|
|
175
|
+
async init(headless = true) {
|
|
176
|
+
this.browser = await chromium.launch({
|
|
177
|
+
headless,
|
|
178
|
+
args: ["--disable-blink-features=AutomationControlled"],
|
|
179
|
+
});
|
|
180
|
+
const context = await this.browser.newContext({
|
|
181
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
182
|
+
viewport: { width: 1280, height: 900 },
|
|
183
|
+
});
|
|
184
|
+
this.page = await context.newPage();
|
|
185
|
+
}
|
|
186
|
+
async close() {
|
|
187
|
+
if (this.browser) {
|
|
188
|
+
await this.browser.close();
|
|
189
|
+
this.browser = null;
|
|
190
|
+
this.page = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
buildBookingUrl(params, filters) {
|
|
194
|
+
const { destination, checkIn, checkOut, guests, rooms } = params;
|
|
195
|
+
const url = new URL("https://www.booking.com/searchresults.html");
|
|
196
|
+
url.searchParams.set("ss", destination);
|
|
197
|
+
url.searchParams.set("checkin", checkIn);
|
|
198
|
+
url.searchParams.set("checkout", checkOut);
|
|
199
|
+
url.searchParams.set("group_adults", guests.toString());
|
|
200
|
+
url.searchParams.set("no_rooms", rooms.toString());
|
|
201
|
+
url.searchParams.set("selected_currency", "USD");
|
|
202
|
+
if (!filters)
|
|
203
|
+
return url.toString();
|
|
204
|
+
const nfltParts = [];
|
|
205
|
+
// === Rating ===
|
|
206
|
+
if (filters.minRating) {
|
|
207
|
+
const scoreFilter = Math.floor(filters.minRating) * 10;
|
|
208
|
+
nfltParts.push(`review_score=${scoreFilter}`);
|
|
209
|
+
}
|
|
210
|
+
// === Property Type ===
|
|
211
|
+
if (filters.propertyType) {
|
|
212
|
+
const code = FILTER_CODES.propertyType[filters.propertyType];
|
|
213
|
+
if (code)
|
|
214
|
+
nfltParts.push(code);
|
|
215
|
+
}
|
|
216
|
+
// === Star Rating ===
|
|
217
|
+
if (filters.starRating) {
|
|
218
|
+
const code = FILTER_CODES.starRating[filters.starRating];
|
|
219
|
+
if (code)
|
|
220
|
+
nfltParts.push(code);
|
|
221
|
+
}
|
|
222
|
+
// === Beach & Location ===
|
|
223
|
+
if (filters.beachfront)
|
|
224
|
+
nfltParts.push("ht_beach=1");
|
|
225
|
+
if (filters.beachAccess)
|
|
226
|
+
nfltParts.push("popular_activities=302");
|
|
227
|
+
// === Hotel Facilities ===
|
|
228
|
+
const hotelFacilities = FILTER_CODES.hotelfacility;
|
|
229
|
+
if (filters.freeWifi)
|
|
230
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.freeWifi}`);
|
|
231
|
+
if (filters.pool)
|
|
232
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.pool}`);
|
|
233
|
+
if (filters.spa)
|
|
234
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.spa}`);
|
|
235
|
+
if (filters.parking || filters.freeParking)
|
|
236
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.parking}`);
|
|
237
|
+
if (filters.restaurant)
|
|
238
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.restaurant}`);
|
|
239
|
+
if (filters.bar)
|
|
240
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.bar}`);
|
|
241
|
+
if (filters.roomService)
|
|
242
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.roomService}`);
|
|
243
|
+
if (filters.airportShuttle)
|
|
244
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.airportShuttle}`);
|
|
245
|
+
if (filters.hotTub)
|
|
246
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.hotTub}`);
|
|
247
|
+
if (filters.sauna)
|
|
248
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.sauna}`);
|
|
249
|
+
if (filters.garden)
|
|
250
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.garden}`);
|
|
251
|
+
if (filters.terrace)
|
|
252
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.terrace}`);
|
|
253
|
+
if (filters.nonSmokingRooms)
|
|
254
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.nonSmokingRooms}`);
|
|
255
|
+
if (filters.familyRooms)
|
|
256
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.familyRooms}`);
|
|
257
|
+
if (filters.disabledFacilities)
|
|
258
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.disabledFacilities}`);
|
|
259
|
+
if (filters.evCharging)
|
|
260
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.evCharging}`);
|
|
261
|
+
if (filters.tennis)
|
|
262
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.tennis}`);
|
|
263
|
+
if (filters.casino)
|
|
264
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.casino}`);
|
|
265
|
+
if (filters.bbqFacilities)
|
|
266
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.bbqFacilities}`);
|
|
267
|
+
if (filters.laundry)
|
|
268
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.laundry}`);
|
|
269
|
+
if (filters.concierge)
|
|
270
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.concierge}`);
|
|
271
|
+
if (filters.currencyExchange)
|
|
272
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.currencyExchange}`);
|
|
273
|
+
if (filters.businessCenter)
|
|
274
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.businessCenter}`);
|
|
275
|
+
if (filters.meetingRooms)
|
|
276
|
+
nfltParts.push(`hotelfacility=${hotelFacilities.meetingRooms}`);
|
|
277
|
+
// === Room Facilities ===
|
|
278
|
+
const roomFacilities = FILTER_CODES.roomfacility;
|
|
279
|
+
if (filters.airConditioning)
|
|
280
|
+
nfltParts.push(`roomfacility=${roomFacilities.airConditioning}`);
|
|
281
|
+
if (filters.kitchen)
|
|
282
|
+
nfltParts.push(`roomfacility=${roomFacilities.kitchen}`);
|
|
283
|
+
if (filters.balcony)
|
|
284
|
+
nfltParts.push(`roomfacility=${roomFacilities.balcony}`);
|
|
285
|
+
if (filters.privatePool)
|
|
286
|
+
nfltParts.push(`roomfacility=${roomFacilities.privatePool}`);
|
|
287
|
+
if (filters.privateBathroom)
|
|
288
|
+
nfltParts.push(`roomfacility=${roomFacilities.privateBathroom}`);
|
|
289
|
+
if (filters.bath)
|
|
290
|
+
nfltParts.push(`roomfacility=${roomFacilities.bath}`);
|
|
291
|
+
if (filters.tv)
|
|
292
|
+
nfltParts.push(`roomfacility=${roomFacilities.tv}`);
|
|
293
|
+
if (filters.minibar)
|
|
294
|
+
nfltParts.push(`roomfacility=${roomFacilities.minibar}`);
|
|
295
|
+
if (filters.safe)
|
|
296
|
+
nfltParts.push(`roomfacility=${roomFacilities.safe}`);
|
|
297
|
+
if (filters.washingMachine)
|
|
298
|
+
nfltParts.push(`roomfacility=${roomFacilities.washingMachine}`);
|
|
299
|
+
if (filters.soundproofing)
|
|
300
|
+
nfltParts.push(`roomfacility=${roomFacilities.soundproofing}`);
|
|
301
|
+
if (filters.oceanView)
|
|
302
|
+
nfltParts.push(`roomfacility=${roomFacilities.oceanView}`);
|
|
303
|
+
// === Bed Type ===
|
|
304
|
+
if (filters.bedType) {
|
|
305
|
+
const code = FILTER_CODES.bedType[filters.bedType];
|
|
306
|
+
if (code)
|
|
307
|
+
nfltParts.push(code);
|
|
308
|
+
}
|
|
309
|
+
// === Meal Plans ===
|
|
310
|
+
if (filters.breakfast)
|
|
311
|
+
nfltParts.push(`mealplan=${FILTER_CODES.mealplan.breakfast}`);
|
|
312
|
+
if (filters.allInclusive)
|
|
313
|
+
nfltParts.push(`mealplan=${FILTER_CODES.mealplan.allInclusive}`);
|
|
314
|
+
if (filters.selfCatering)
|
|
315
|
+
nfltParts.push(`mealplan=${FILTER_CODES.mealplan.selfCatering}`);
|
|
316
|
+
// === Stay Type ===
|
|
317
|
+
if (filters.petFriendly)
|
|
318
|
+
nfltParts.push(`stay_type=${FILTER_CODES.stayType.petFriendly}`);
|
|
319
|
+
if (filters.adultsOnly)
|
|
320
|
+
nfltParts.push(`stay_type=${FILTER_CODES.stayType.adultsOnly}`);
|
|
321
|
+
if (filters.lgbtqFriendly)
|
|
322
|
+
nfltParts.push(`stay_type=${FILTER_CODES.stayType.lgbtqFriendly}`);
|
|
323
|
+
// === Cancellation & Payment ===
|
|
324
|
+
if (filters.freeCancellation)
|
|
325
|
+
nfltParts.push(`fc=${FILTER_CODES.fc.freeCancellation}`);
|
|
326
|
+
if (filters.noPrepayment)
|
|
327
|
+
nfltParts.push(`fc=${FILTER_CODES.fc.noPrepayment}`);
|
|
328
|
+
if (filters.noBookingFee)
|
|
329
|
+
nfltParts.push(`fc=${FILTER_CODES.fc.noBookingFee}`);
|
|
330
|
+
// === Sustainability ===
|
|
331
|
+
if (filters.sustainabilityCertified)
|
|
332
|
+
nfltParts.push("SustainablePropertyLevelFilter=4");
|
|
333
|
+
// === Distance from center ===
|
|
334
|
+
if (filters.maxDistanceFromCenter) {
|
|
335
|
+
const distance = FILTER_CODES.distance[filters.maxDistanceFromCenter];
|
|
336
|
+
if (distance)
|
|
337
|
+
nfltParts.push(`distance=${distance}`);
|
|
338
|
+
}
|
|
339
|
+
// === Popular Activities ===
|
|
340
|
+
const activities = FILTER_CODES.popularActivities;
|
|
341
|
+
if (filters.fitness)
|
|
342
|
+
nfltParts.push(`popular_activities=${activities.fitness}`);
|
|
343
|
+
if (filters.golf)
|
|
344
|
+
nfltParts.push(`popular_activities=${activities.golf}`);
|
|
345
|
+
if (filters.snorkeling)
|
|
346
|
+
nfltParts.push(`popular_activities=${activities.snorkeling}`);
|
|
347
|
+
if (filters.diving)
|
|
348
|
+
nfltParts.push(`popular_activities=${activities.diving}`);
|
|
349
|
+
if (filters.fishing)
|
|
350
|
+
nfltParts.push(`popular_activities=${activities.fishing}`);
|
|
351
|
+
if (filters.hiking)
|
|
352
|
+
nfltParts.push(`popular_activities=${activities.hiking}`);
|
|
353
|
+
if (filters.cycling)
|
|
354
|
+
nfltParts.push(`popular_activities=${activities.cycling}`);
|
|
355
|
+
if (filters.skiing)
|
|
356
|
+
nfltParts.push(`popular_activities=${activities.skiing}`);
|
|
357
|
+
if (filters.waterSports)
|
|
358
|
+
nfltParts.push(`popular_activities=${activities.waterSports}`);
|
|
359
|
+
if (filters.horseRiding)
|
|
360
|
+
nfltParts.push(`popular_activities=${activities.horseRiding}`);
|
|
361
|
+
// === Accessibility - Hotel ===
|
|
362
|
+
const accessFac = FILTER_CODES.accessibleFacilities;
|
|
363
|
+
if (filters.grabRails)
|
|
364
|
+
nfltParts.push(`accessible_facilities=${accessFac.grabRails}`);
|
|
365
|
+
if (filters.raisedToilet)
|
|
366
|
+
nfltParts.push(`accessible_facilities=${accessFac.raisedToilet}`);
|
|
367
|
+
if (filters.loweredSink)
|
|
368
|
+
nfltParts.push(`accessible_facilities=${accessFac.loweredSink}`);
|
|
369
|
+
if (filters.emergencyCord)
|
|
370
|
+
nfltParts.push(`accessible_facilities=${accessFac.emergencyCord}`);
|
|
371
|
+
if (filters.braille)
|
|
372
|
+
nfltParts.push(`accessible_facilities=${accessFac.braille}`);
|
|
373
|
+
if (filters.tactileSigns)
|
|
374
|
+
nfltParts.push(`accessible_facilities=${accessFac.tactileSigns}`);
|
|
375
|
+
if (filters.auditoryGuidance)
|
|
376
|
+
nfltParts.push(`accessible_facilities=${accessFac.auditoryGuidance}`);
|
|
377
|
+
// === Accessibility - Room ===
|
|
378
|
+
const accessRoom = FILTER_CODES.accessibleRoomFacilities;
|
|
379
|
+
if (filters.wheelchairAccessible)
|
|
380
|
+
nfltParts.push(`accessible_room_facilities=${accessRoom.wheelchairAccessible}`);
|
|
381
|
+
if (filters.groundFloor)
|
|
382
|
+
nfltParts.push(`accessible_room_facilities=${accessRoom.groundFloor}`);
|
|
383
|
+
if (filters.elevatorAccess)
|
|
384
|
+
nfltParts.push(`accessible_room_facilities=${accessRoom.elevatorAccess}`);
|
|
385
|
+
if (filters.walkInShower)
|
|
386
|
+
nfltParts.push(`accessible_room_facilities=${accessRoom.walkInShower}`);
|
|
387
|
+
if (filters.rollInShower)
|
|
388
|
+
nfltParts.push(`accessible_room_facilities=${accessRoom.rollInShower}`);
|
|
389
|
+
if (filters.showerChair)
|
|
390
|
+
nfltParts.push(`accessible_room_facilities=${accessRoom.showerChair}`);
|
|
391
|
+
// === Hotel Chain ===
|
|
392
|
+
if (filters.hotelChain) {
|
|
393
|
+
const chainCode = FILTER_CODES.chaincode[filters.hotelChain];
|
|
394
|
+
if (chainCode)
|
|
395
|
+
nfltParts.push(`chaincode=${chainCode}`);
|
|
396
|
+
}
|
|
397
|
+
if (nfltParts.length > 0) {
|
|
398
|
+
url.searchParams.set("nflt", nfltParts.join(";"));
|
|
399
|
+
}
|
|
400
|
+
return url.toString();
|
|
401
|
+
}
|
|
402
|
+
async searchHotels(params, filters) {
|
|
403
|
+
if (!this.page)
|
|
404
|
+
throw new Error("Browser not initialized");
|
|
405
|
+
const url = this.buildBookingUrl(params, filters);
|
|
406
|
+
await this.page.goto(url, { waitUntil: "networkidle" });
|
|
407
|
+
await this.page.waitForTimeout(2000);
|
|
408
|
+
// Close any popups/modals
|
|
409
|
+
await this.dismissPopups();
|
|
410
|
+
// Scroll to load more results
|
|
411
|
+
await this.scrollToLoadMore();
|
|
412
|
+
// Extract detailed hotel info
|
|
413
|
+
const hotels = await this.extractHotelDetails();
|
|
414
|
+
// Apply client-side filtering and scoring if we have preferences
|
|
415
|
+
if (filters) {
|
|
416
|
+
return this.scoreAndFilterHotels(hotels, filters);
|
|
417
|
+
}
|
|
418
|
+
return hotels;
|
|
419
|
+
}
|
|
420
|
+
async dismissPopups() {
|
|
421
|
+
if (!this.page)
|
|
422
|
+
return;
|
|
423
|
+
const popupSelectors = [
|
|
424
|
+
'#onetrust-accept-btn-handler',
|
|
425
|
+
'[data-testid="accept-btn"]',
|
|
426
|
+
'button[aria-label="Dismiss sign-in info."]',
|
|
427
|
+
'[aria-label="Dismiss sign in information."]',
|
|
428
|
+
'.modal-mask button',
|
|
429
|
+
'[data-testid="close-button"]',
|
|
430
|
+
];
|
|
431
|
+
for (const selector of popupSelectors) {
|
|
432
|
+
try {
|
|
433
|
+
const btn = await this.page.$(selector);
|
|
434
|
+
if (btn) {
|
|
435
|
+
await btn.click();
|
|
436
|
+
await this.page.waitForTimeout(500);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// Ignore popup dismissal errors
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async scrollToLoadMore() {
|
|
445
|
+
if (!this.page)
|
|
446
|
+
return;
|
|
447
|
+
// Scroll down a few times to load more results
|
|
448
|
+
for (let i = 0; i < 3; i++) {
|
|
449
|
+
await this.page.evaluate(() => {
|
|
450
|
+
window.scrollBy(0, window.innerHeight);
|
|
451
|
+
});
|
|
452
|
+
await this.page.waitForTimeout(1000);
|
|
453
|
+
}
|
|
454
|
+
// Scroll back to top
|
|
455
|
+
await this.page.evaluate(() => {
|
|
456
|
+
window.scrollTo(0, 0);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
async extractHotelDetails() {
|
|
460
|
+
if (!this.page)
|
|
461
|
+
return [];
|
|
462
|
+
return await this.page.evaluate(() => {
|
|
463
|
+
const results = [];
|
|
464
|
+
const cards = document.querySelectorAll('[data-testid="property-card"]');
|
|
465
|
+
cards.forEach((card) => {
|
|
466
|
+
// Check if this is a paid/sponsored listing
|
|
467
|
+
// Key indicator: "nad_" in the link means "native ad" - a PAID placement
|
|
468
|
+
const cardHtml = card.outerHTML;
|
|
469
|
+
const linkEl = card.querySelector('a[data-testid="title-link"]');
|
|
470
|
+
const link = linkEl?.href || "";
|
|
471
|
+
const isNativeAd = cardHtml.includes("nad_") || link.includes("nad_");
|
|
472
|
+
// Also check for explicit "Ad" label text
|
|
473
|
+
const hasAdLabel = Array.from(card.querySelectorAll('span, div')).some(el => {
|
|
474
|
+
const text = el.textContent?.trim().toLowerCase();
|
|
475
|
+
return text === "ad" || text === "sponsored" || text === "promoted";
|
|
476
|
+
});
|
|
477
|
+
// Skip sponsored/paid listings
|
|
478
|
+
if (isNativeAd || hasAdLabel) {
|
|
479
|
+
return; // Skip this card
|
|
480
|
+
}
|
|
481
|
+
// Name
|
|
482
|
+
const nameEl = card.querySelector('[data-testid="title"]');
|
|
483
|
+
const name = nameEl?.textContent?.trim() || "Unknown";
|
|
484
|
+
// Price - look for per night price first, then total
|
|
485
|
+
const allPriceEls = card.querySelectorAll('[data-testid="price-and-discounted-price"]');
|
|
486
|
+
let priceText = "";
|
|
487
|
+
let price = null;
|
|
488
|
+
// First price element is usually per night
|
|
489
|
+
if (allPriceEls.length > 0) {
|
|
490
|
+
priceText = allPriceEls[0].textContent?.trim() || "";
|
|
491
|
+
const priceMatch = priceText.match(/\$?([\d,]+)/);
|
|
492
|
+
price = priceMatch ? parseInt(priceMatch[1].replace(",", "")) : null;
|
|
493
|
+
}
|
|
494
|
+
// Rating - look for the numeric score
|
|
495
|
+
const ratingScoreEl = card.querySelector('[data-testid="review-score"] .dff2e52086');
|
|
496
|
+
const ratingText = ratingScoreEl?.textContent?.trim() || "";
|
|
497
|
+
const rating = ratingText ? parseFloat(ratingText) : null;
|
|
498
|
+
// Rating description (e.g., "Excellent", "Very Good")
|
|
499
|
+
const ratingDescEl = card.querySelector('[data-testid="review-score"] .f546354b44');
|
|
500
|
+
const ratingDesc = ratingDescEl?.textContent?.trim() || "";
|
|
501
|
+
// Review count
|
|
502
|
+
const reviewCountEl = card.querySelector('[data-testid="review-score"] .fb14de7f14');
|
|
503
|
+
const reviewText = reviewCountEl?.textContent || "";
|
|
504
|
+
const reviewMatch = reviewText.match(/([\d,]+)/);
|
|
505
|
+
const reviewCount = reviewMatch ? parseInt(reviewMatch[1].replace(",", "")) : null;
|
|
506
|
+
// Distance to center
|
|
507
|
+
const distanceEl = card.querySelector('[data-testid="distance"]');
|
|
508
|
+
const distanceToCenter = distanceEl?.textContent?.trim() || "";
|
|
509
|
+
// Address/neighborhood
|
|
510
|
+
const addressEl = card.querySelector('[data-testid="address-link"] span:first-child');
|
|
511
|
+
const location = addressEl?.textContent?.trim() || "";
|
|
512
|
+
// Collect all text for amenity detection
|
|
513
|
+
const cardText = card.textContent?.toLowerCase() || "";
|
|
514
|
+
const amenities = [];
|
|
515
|
+
const highlights = [];
|
|
516
|
+
// Extract beach info
|
|
517
|
+
if (cardText.includes("beachfront")) {
|
|
518
|
+
amenities.push("Beachfront");
|
|
519
|
+
}
|
|
520
|
+
else if (cardText.includes("beach nearby")) {
|
|
521
|
+
amenities.push("Beach Nearby");
|
|
522
|
+
}
|
|
523
|
+
else if (cardText.includes("from beach")) {
|
|
524
|
+
const beachMatch = cardText.match(/([\d.]+)\s*(miles?|feet|km|meters?)\s*from\s*beach/i);
|
|
525
|
+
if (beachMatch) {
|
|
526
|
+
amenities.push(`${beachMatch[1]} ${beachMatch[2]} from beach`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// Check for other amenities
|
|
530
|
+
if (cardText.includes("pool"))
|
|
531
|
+
amenities.push("Pool");
|
|
532
|
+
if (cardText.includes("free wifi") || cardText.includes("free wi-fi"))
|
|
533
|
+
amenities.push("Free WiFi");
|
|
534
|
+
if (cardText.includes("parking"))
|
|
535
|
+
amenities.push("Parking");
|
|
536
|
+
if (cardText.includes("breakfast included") || cardText.includes("includes breakfast"))
|
|
537
|
+
amenities.push("Breakfast Included");
|
|
538
|
+
if (cardText.includes("spa") || cardText.includes("wellness"))
|
|
539
|
+
amenities.push("Spa");
|
|
540
|
+
if (cardText.includes("fitness") || cardText.includes("gym"))
|
|
541
|
+
amenities.push("Fitness Center");
|
|
542
|
+
if (cardText.includes("restaurant"))
|
|
543
|
+
amenities.push("Restaurant");
|
|
544
|
+
if (cardText.includes("air condition"))
|
|
545
|
+
amenities.push("A/C");
|
|
546
|
+
if (cardText.includes("pet friendly") || cardText.includes("pets allowed"))
|
|
547
|
+
amenities.push("Pet Friendly");
|
|
548
|
+
if (cardText.includes("hot tub") || cardText.includes("jacuzzi"))
|
|
549
|
+
amenities.push("Hot Tub");
|
|
550
|
+
if (cardText.includes("kitchen"))
|
|
551
|
+
amenities.push("Kitchen");
|
|
552
|
+
if (cardText.includes("balcony"))
|
|
553
|
+
amenities.push("Balcony");
|
|
554
|
+
if (cardText.includes("sea view") || cardText.includes("ocean view"))
|
|
555
|
+
amenities.push("Ocean View");
|
|
556
|
+
if (cardText.includes("free cancellation"))
|
|
557
|
+
highlights.push("Free Cancellation");
|
|
558
|
+
if (cardText.includes("no prepayment"))
|
|
559
|
+
highlights.push("No Prepayment");
|
|
560
|
+
results.push({
|
|
561
|
+
name,
|
|
562
|
+
price,
|
|
563
|
+
priceDisplay: priceText || "Price not shown",
|
|
564
|
+
rating,
|
|
565
|
+
ratingText: ratingDesc || ratingText,
|
|
566
|
+
reviewCount,
|
|
567
|
+
location,
|
|
568
|
+
distanceToCenter,
|
|
569
|
+
amenities,
|
|
570
|
+
highlights,
|
|
571
|
+
link,
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
return results;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
scoreAndFilterHotels(hotels, filters) {
|
|
578
|
+
return hotels
|
|
579
|
+
.map((hotel) => {
|
|
580
|
+
let score = 0;
|
|
581
|
+
const matchReasons = [];
|
|
582
|
+
const amenitiesLower = hotel.amenities.map((a) => a.toLowerCase());
|
|
583
|
+
const highlightsLower = hotel.highlights.map((h) => h.toLowerCase());
|
|
584
|
+
const allText = [...amenitiesLower, ...highlightsLower].join(" ");
|
|
585
|
+
// Score based on requested filters
|
|
586
|
+
if (filters.beachfront || filters.beachAccess) {
|
|
587
|
+
if (allText.includes("beach")) {
|
|
588
|
+
score += 20;
|
|
589
|
+
matchReasons.push("Near beach");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (filters.freeWifi) {
|
|
593
|
+
if (allText.includes("wifi") || allText.includes("wi-fi")) {
|
|
594
|
+
score += 10;
|
|
595
|
+
matchReasons.push("Has WiFi");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (filters.pool) {
|
|
599
|
+
if (allText.includes("pool")) {
|
|
600
|
+
score += 10;
|
|
601
|
+
matchReasons.push("Has pool");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (filters.breakfast) {
|
|
605
|
+
if (allText.includes("breakfast")) {
|
|
606
|
+
score += 10;
|
|
607
|
+
matchReasons.push("Breakfast included");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (filters.parking || filters.freeParking) {
|
|
611
|
+
if (allText.includes("parking")) {
|
|
612
|
+
score += 10;
|
|
613
|
+
matchReasons.push("Has parking");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (filters.spa) {
|
|
617
|
+
if (allText.includes("spa") || allText.includes("wellness")) {
|
|
618
|
+
score += 10;
|
|
619
|
+
matchReasons.push("Has spa");
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (filters.fitness) {
|
|
623
|
+
if (allText.includes("gym") || allText.includes("fitness")) {
|
|
624
|
+
score += 10;
|
|
625
|
+
matchReasons.push("Has gym");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (filters.hotTub) {
|
|
629
|
+
if (allText.includes("hot tub") || allText.includes("jacuzzi")) {
|
|
630
|
+
score += 10;
|
|
631
|
+
matchReasons.push("Has hot tub");
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (filters.kitchen) {
|
|
635
|
+
if (allText.includes("kitchen")) {
|
|
636
|
+
score += 10;
|
|
637
|
+
matchReasons.push("Has kitchen");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (filters.balcony) {
|
|
641
|
+
if (allText.includes("balcony")) {
|
|
642
|
+
score += 10;
|
|
643
|
+
matchReasons.push("Has balcony");
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (filters.oceanView) {
|
|
647
|
+
if (allText.includes("ocean view") || allText.includes("sea view")) {
|
|
648
|
+
score += 15;
|
|
649
|
+
matchReasons.push("Ocean view");
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (filters.freeCancellation) {
|
|
653
|
+
if (allText.includes("free cancellation")) {
|
|
654
|
+
score += 5;
|
|
655
|
+
matchReasons.push("Free cancellation");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// Bonus for high ratings
|
|
659
|
+
if (hotel.rating) {
|
|
660
|
+
if (hotel.rating >= 9) {
|
|
661
|
+
score += 15;
|
|
662
|
+
matchReasons.push(`Excellent rating (${hotel.rating})`);
|
|
663
|
+
}
|
|
664
|
+
else if (hotel.rating >= 8) {
|
|
665
|
+
score += 10;
|
|
666
|
+
matchReasons.push(`Great rating (${hotel.rating})`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Bonus for many reviews (more trustworthy)
|
|
670
|
+
if (hotel.reviewCount && hotel.reviewCount > 500) {
|
|
671
|
+
score += 5;
|
|
672
|
+
matchReasons.push(`${hotel.reviewCount} reviews`);
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
...hotel,
|
|
676
|
+
matchScore: score,
|
|
677
|
+
matchReasons,
|
|
678
|
+
};
|
|
679
|
+
})
|
|
680
|
+
.filter((hotel) => {
|
|
681
|
+
// Filter out hotels that don't meet minimum criteria
|
|
682
|
+
if (filters.minRating && hotel.rating && hotel.rating < filters.minRating) {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
if (filters.maxPrice && hotel.price && hotel.price > filters.maxPrice) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
return true;
|
|
689
|
+
})
|
|
690
|
+
.sort((a, b) => (b.matchScore || 0) - (a.matchScore || 0));
|
|
691
|
+
}
|
|
692
|
+
async getHotelDetails(hotelUrl) {
|
|
693
|
+
if (!this.page)
|
|
694
|
+
throw new Error("Browser not initialized");
|
|
695
|
+
await this.page.goto(hotelUrl, { waitUntil: "networkidle" });
|
|
696
|
+
await this.page.waitForTimeout(2000);
|
|
697
|
+
await this.dismissPopups();
|
|
698
|
+
// Extract detailed information from hotel page
|
|
699
|
+
return await this.page.evaluate(() => {
|
|
700
|
+
const details = {};
|
|
701
|
+
// Hotel name
|
|
702
|
+
const nameEl = document.querySelector('h2[class*="pp-header"]');
|
|
703
|
+
details.name = nameEl?.textContent?.trim();
|
|
704
|
+
// Description
|
|
705
|
+
const descEl = document.querySelector('[data-testid="property-description"]');
|
|
706
|
+
details.description = descEl?.textContent?.trim();
|
|
707
|
+
// All facilities
|
|
708
|
+
const facilityEls = document.querySelectorAll('[data-testid="property-section-facilities"] li');
|
|
709
|
+
details.facilities = Array.from(facilityEls).map((el) => el.textContent?.trim());
|
|
710
|
+
// Photos
|
|
711
|
+
const photoEls = document.querySelectorAll('[data-testid="gallery-image"] img');
|
|
712
|
+
details.photos = Array.from(photoEls)
|
|
713
|
+
.slice(0, 5)
|
|
714
|
+
.map((el) => el.src);
|
|
715
|
+
// Popular facilities highlighted
|
|
716
|
+
const popularFacilities = document.querySelectorAll('[data-testid="property-most-popular-facilities"] span');
|
|
717
|
+
details.popularFacilities = Array.from(popularFacilities).map((el) => el.textContent?.trim());
|
|
718
|
+
return details;
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
async takeScreenshot(path) {
|
|
722
|
+
if (!this.page)
|
|
723
|
+
throw new Error("Browser not initialized");
|
|
724
|
+
await this.page.screenshot({ path, fullPage: false });
|
|
725
|
+
}
|
|
726
|
+
async getPageContent() {
|
|
727
|
+
if (!this.page)
|
|
728
|
+
throw new Error("Browser not initialized");
|
|
729
|
+
return await this.page.content();
|
|
730
|
+
}
|
|
731
|
+
}
|