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.
@@ -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
+ }