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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Debug: Check what filters Booking.com uses
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
async function debug() {
|
|
4
|
+
const browser = await chromium.launch({ headless: false });
|
|
5
|
+
const context = await browser.newContext({
|
|
6
|
+
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",
|
|
7
|
+
});
|
|
8
|
+
const page = await context.newPage();
|
|
9
|
+
// Go to Booking.com search for Puerto Rico
|
|
10
|
+
await page.goto("https://www.booking.com/searchresults.html?ss=Puerto+Rico&checkin=2026-03-07&checkout=2026-03-14&group_adults=2&no_rooms=1");
|
|
11
|
+
await page.waitForTimeout(3000);
|
|
12
|
+
// Take screenshot of filters sidebar
|
|
13
|
+
await page.screenshot({ path: "filters-debug.png", fullPage: false });
|
|
14
|
+
// Try to find and log filter options
|
|
15
|
+
const filterGroups = await page.evaluate(() => {
|
|
16
|
+
const filters = {};
|
|
17
|
+
// Look for filter groups
|
|
18
|
+
const filterContainers = document.querySelectorAll('[data-filters-group]');
|
|
19
|
+
filterContainers.forEach(container => {
|
|
20
|
+
const groupName = container.getAttribute('data-filters-group') || 'unknown';
|
|
21
|
+
const options = [];
|
|
22
|
+
container.querySelectorAll('input[type="checkbox"]').forEach(input => {
|
|
23
|
+
const name = input.name;
|
|
24
|
+
const value = input.value;
|
|
25
|
+
const label = input.parentElement?.textContent?.trim() || '';
|
|
26
|
+
options.push(`${name}=${value} (${label})`);
|
|
27
|
+
});
|
|
28
|
+
if (options.length > 0) {
|
|
29
|
+
filters[groupName] = options;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return filters;
|
|
33
|
+
});
|
|
34
|
+
console.log("Filter groups found:", JSON.stringify(filterGroups, null, 2));
|
|
35
|
+
// Also look for any beach-related elements
|
|
36
|
+
const beachFilters = await page.evaluate(() => {
|
|
37
|
+
const html = document.body.innerHTML;
|
|
38
|
+
const matches = [];
|
|
39
|
+
// Look for beach in filter names
|
|
40
|
+
const beachRegex = /name="[^"]*"[^>]*value="[^"]*"[^>]*(?:beach|ocean|sea)/gi;
|
|
41
|
+
const found = html.match(beachRegex);
|
|
42
|
+
if (found)
|
|
43
|
+
matches.push(...found);
|
|
44
|
+
// Also look for nflt patterns in links
|
|
45
|
+
const nfltRegex = /nflt=[^"&]*(beach|ocean|fitness|gym)[^"&]*/gi;
|
|
46
|
+
const nfltFound = html.match(nfltRegex);
|
|
47
|
+
if (nfltFound)
|
|
48
|
+
matches.push(...nfltFound);
|
|
49
|
+
return matches;
|
|
50
|
+
});
|
|
51
|
+
console.log("\nBeach/fitness related:", beachFilters);
|
|
52
|
+
// Click on a beach filter if we can find one
|
|
53
|
+
const beachCheckbox = await page.$('input[name*="beach"], input[value*="beach"]');
|
|
54
|
+
if (beachCheckbox) {
|
|
55
|
+
await beachCheckbox.click();
|
|
56
|
+
await page.waitForTimeout(2000);
|
|
57
|
+
console.log("\nURL after clicking beach filter:", page.url());
|
|
58
|
+
}
|
|
59
|
+
// Look for fitness/gym filter
|
|
60
|
+
const gymText = await page.evaluate(() => {
|
|
61
|
+
const spans = Array.from(document.querySelectorAll('span, div, label'));
|
|
62
|
+
return spans
|
|
63
|
+
.filter(el => el.textContent?.toLowerCase().includes('fitness') ||
|
|
64
|
+
el.textContent?.toLowerCase().includes('gym'))
|
|
65
|
+
.map(el => el.textContent?.trim())
|
|
66
|
+
.slice(0, 10);
|
|
67
|
+
});
|
|
68
|
+
console.log("\nGym/fitness text found:", gymText);
|
|
69
|
+
await page.waitForTimeout(5000);
|
|
70
|
+
await browser.close();
|
|
71
|
+
}
|
|
72
|
+
debug().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Debug: More precise sponsored detection
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
async function findSponsoredIndicators() {
|
|
4
|
+
const browser = await chromium.launch({ headless: false });
|
|
5
|
+
const context = await browser.newContext({
|
|
6
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
7
|
+
});
|
|
8
|
+
const page = await context.newPage();
|
|
9
|
+
await page.goto("https://www.booking.com/searchresults.html?ss=Puerto+Rico&checkin=2026-03-07&checkout=2026-03-14&group_adults=2&no_rooms=1");
|
|
10
|
+
await page.waitForTimeout(5000);
|
|
11
|
+
try {
|
|
12
|
+
await page.click('#onetrust-accept-btn-handler', { timeout: 3000 });
|
|
13
|
+
}
|
|
14
|
+
catch { }
|
|
15
|
+
try {
|
|
16
|
+
await page.click('button[aria-label="Dismiss sign-in info."]', { timeout: 2000 });
|
|
17
|
+
}
|
|
18
|
+
catch { }
|
|
19
|
+
const sponsoredInfo = await page.evaluate(() => {
|
|
20
|
+
const results = [];
|
|
21
|
+
const cards = document.querySelectorAll('[data-testid="property-card"]');
|
|
22
|
+
cards.forEach((card) => {
|
|
23
|
+
const name = card.querySelector('[data-testid="title"]')?.textContent?.trim() || "Unknown";
|
|
24
|
+
const cardHtml = card.outerHTML;
|
|
25
|
+
const link = card.querySelector('a[data-testid="title-link"]')?.getAttribute('href') || "";
|
|
26
|
+
const indicators = [];
|
|
27
|
+
// Key indicator: nad_ means "native ad" - this is a PAID placement
|
|
28
|
+
const hasNativeAd = cardHtml.includes("nad_") || link.includes("nad_");
|
|
29
|
+
if (hasNativeAd)
|
|
30
|
+
indicators.push("NATIVE AD (nad_)");
|
|
31
|
+
// Look for "Ad" label text in specific location (usually small text near the name)
|
|
32
|
+
const adLabelEl = card.querySelector('[data-testid="property-card-badge"]');
|
|
33
|
+
const hasAdBadge = adLabelEl?.textContent?.toLowerCase().includes("ad") || false;
|
|
34
|
+
if (hasAdBadge)
|
|
35
|
+
indicators.push("AD BADGE");
|
|
36
|
+
// Check for "Sponsored" or "Ad" in a small text element
|
|
37
|
+
const smallTexts = card.querySelectorAll('span, div');
|
|
38
|
+
let hasAdText = false;
|
|
39
|
+
smallTexts.forEach(el => {
|
|
40
|
+
const text = el.textContent?.trim().toLowerCase();
|
|
41
|
+
// Only match standalone "ad" or "sponsored", not words containing "ad"
|
|
42
|
+
if (text === "ad" || text === "sponsored" || text === "promoted") {
|
|
43
|
+
hasAdText = true;
|
|
44
|
+
indicators.push(`AD TEXT: "${el.textContent?.trim()}"`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
// Check for genius program (not really an ad, but a promotion)
|
|
48
|
+
const hasGenius = cardHtml.toLowerCase().includes("genius");
|
|
49
|
+
if (hasGenius)
|
|
50
|
+
indicators.push("Genius program");
|
|
51
|
+
// Check for "Featured" which sometimes indicates paid
|
|
52
|
+
const hasFeatured = card.textContent?.toLowerCase().includes("featured") || false;
|
|
53
|
+
if (hasFeatured)
|
|
54
|
+
indicators.push("Featured");
|
|
55
|
+
const isPaidAd = hasNativeAd; // nad_ is the clearest indicator of paid placement
|
|
56
|
+
const hasAdLabel = hasAdBadge || hasAdText;
|
|
57
|
+
results.push({
|
|
58
|
+
name,
|
|
59
|
+
isPaidAd,
|
|
60
|
+
hasAdLabel,
|
|
61
|
+
indicators,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
return results;
|
|
65
|
+
});
|
|
66
|
+
console.log("=== Sponsored/Paid Ad Analysis ===\n");
|
|
67
|
+
const paidAds = sponsoredInfo.filter(x => x.isPaidAd);
|
|
68
|
+
const organic = sponsoredInfo.filter(x => !x.isPaidAd);
|
|
69
|
+
console.log("--- PAID ADS (nad_ tracking) ---");
|
|
70
|
+
paidAds.forEach((info, i) => {
|
|
71
|
+
console.log(`${i + 1}. ${info.name}`);
|
|
72
|
+
console.log(` Indicators: ${info.indicators.join(", ")}`);
|
|
73
|
+
});
|
|
74
|
+
console.log("\n--- ORGANIC RESULTS ---");
|
|
75
|
+
organic.forEach((info, i) => {
|
|
76
|
+
console.log(`${i + 1}. ${info.name}`);
|
|
77
|
+
if (info.indicators.length > 0) {
|
|
78
|
+
console.log(` Notes: ${info.indicators.join(", ")}`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
console.log(`\n=== Summary ===`);
|
|
82
|
+
console.log(`Total: ${sponsoredInfo.length}`);
|
|
83
|
+
console.log(`Paid ads: ${paidAds.length}`);
|
|
84
|
+
console.log(`Organic: ${organic.length}`);
|
|
85
|
+
await browser.close();
|
|
86
|
+
}
|
|
87
|
+
findSponsoredIndicators().catch(console.error);
|
package/dist/debug.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/debug.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Debug: Extract raw HTML from hotel cards to see what's available
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
async function debug() {
|
|
4
|
+
const browser = await chromium.launch({ headless: false });
|
|
5
|
+
const context = await browser.newContext({
|
|
6
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
7
|
+
viewport: { width: 1280, height: 900 },
|
|
8
|
+
});
|
|
9
|
+
const page = await context.newPage();
|
|
10
|
+
const url = "https://www.booking.com/searchresults.html?ss=San+Juan%2C+Puerto+Rico&checkin=2026-03-01&checkout=2026-03-05&group_adults=2&no_rooms=1&selected_currency=USD&nflt=review_score%3D80";
|
|
11
|
+
await page.goto(url, { waitUntil: "networkidle" });
|
|
12
|
+
await page.waitForTimeout(3000);
|
|
13
|
+
// Dismiss popups
|
|
14
|
+
try {
|
|
15
|
+
const btn = await page.$('[aria-label="Dismiss sign in information."]');
|
|
16
|
+
if (btn)
|
|
17
|
+
await btn.click();
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
// Get first hotel card's full HTML
|
|
21
|
+
const cardHtml = await page.evaluate(() => {
|
|
22
|
+
const card = document.querySelector('[data-testid="property-card"]');
|
|
23
|
+
return card?.outerHTML || "No card found";
|
|
24
|
+
});
|
|
25
|
+
console.log("First hotel card HTML:\n");
|
|
26
|
+
console.log(cardHtml);
|
|
27
|
+
// Also extract text content to see what amenities are visible
|
|
28
|
+
const cardText = await page.evaluate(() => {
|
|
29
|
+
const card = document.querySelector('[data-testid="property-card"]');
|
|
30
|
+
return card?.textContent || "";
|
|
31
|
+
});
|
|
32
|
+
console.log("\n\nCard text content:\n");
|
|
33
|
+
console.log(cardText);
|
|
34
|
+
await page.screenshot({ path: "debug-screenshot.png" });
|
|
35
|
+
await browser.close();
|
|
36
|
+
}
|
|
37
|
+
debug().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Comprehensive debug: Extract ALL filter codes from Booking.com
|
|
2
|
+
import { chromium } from "playwright";
|
|
3
|
+
async function extractAllFilters() {
|
|
4
|
+
const browser = await chromium.launch({ headless: true });
|
|
5
|
+
const context = await browser.newContext({
|
|
6
|
+
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
7
|
+
});
|
|
8
|
+
const page = await context.newPage();
|
|
9
|
+
// Go to a popular destination to get full filter list
|
|
10
|
+
await page.goto("https://www.booking.com/searchresults.html?ss=New+York&checkin=2026-03-07&checkout=2026-03-14&group_adults=2&no_rooms=1");
|
|
11
|
+
await page.waitForTimeout(5000);
|
|
12
|
+
// Dismiss popups
|
|
13
|
+
try {
|
|
14
|
+
await page.click('#onetrust-accept-btn-handler', { timeout: 3000 });
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
try {
|
|
18
|
+
await page.click('button[aria-label="Dismiss sign-in info."]', { timeout: 2000 });
|
|
19
|
+
}
|
|
20
|
+
catch { }
|
|
21
|
+
// Extract ALL filter inputs
|
|
22
|
+
const allFilters = await page.evaluate(() => {
|
|
23
|
+
const filters = {};
|
|
24
|
+
// Get all filter checkboxes
|
|
25
|
+
const checkboxes = document.querySelectorAll('input[type="checkbox"][name]');
|
|
26
|
+
checkboxes.forEach(input => {
|
|
27
|
+
const inp = input;
|
|
28
|
+
const name = inp.name;
|
|
29
|
+
const value = inp.value;
|
|
30
|
+
// Get label text
|
|
31
|
+
let label = '';
|
|
32
|
+
let count = '';
|
|
33
|
+
const container = inp.closest('[data-filters-item]') || inp.parentElement?.parentElement;
|
|
34
|
+
if (container) {
|
|
35
|
+
const labelEl = container.querySelector('span');
|
|
36
|
+
label = labelEl?.textContent?.trim() || '';
|
|
37
|
+
// Extract count if present (usually in parentheses or separate span)
|
|
38
|
+
const countMatch = label.match(/(\d+)$/);
|
|
39
|
+
if (countMatch) {
|
|
40
|
+
count = countMatch[1];
|
|
41
|
+
label = label.replace(/\s*\d+$/, '').trim();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!filters[name]) {
|
|
45
|
+
filters[name] = [];
|
|
46
|
+
}
|
|
47
|
+
// Avoid duplicates
|
|
48
|
+
if (!filters[name].some(f => f.value === value)) {
|
|
49
|
+
filters[name].push({ name, value, label, count });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return filters;
|
|
53
|
+
});
|
|
54
|
+
// Print organized output
|
|
55
|
+
console.log("// ============================================");
|
|
56
|
+
console.log("// BOOKING.COM FILTER CODES - Complete Reference");
|
|
57
|
+
console.log("// ============================================\n");
|
|
58
|
+
const categories = {
|
|
59
|
+
'review_score': 'Rating Filters',
|
|
60
|
+
'class': 'Star Rating',
|
|
61
|
+
'hotelfacility': 'Hotel Facilities',
|
|
62
|
+
'roomfacility': 'Room Facilities',
|
|
63
|
+
'popular_activities': 'Activities & Amenities',
|
|
64
|
+
'ht_beach': 'Beach Access',
|
|
65
|
+
'ht_id': 'Property Types',
|
|
66
|
+
'mealplan': 'Meal Plans',
|
|
67
|
+
'stay_type': 'Stay Type',
|
|
68
|
+
'fc': 'Cancellation & Payment',
|
|
69
|
+
'accessible_facilities': 'Accessibility - Hotel',
|
|
70
|
+
'accessible_room_facilities': 'Accessibility - Room',
|
|
71
|
+
'chaincode': 'Hotel Chains',
|
|
72
|
+
'privacy_type': 'Privacy',
|
|
73
|
+
'SustainablePropertyLevelFilter': 'Sustainability',
|
|
74
|
+
};
|
|
75
|
+
for (const [key, title] of Object.entries(categories)) {
|
|
76
|
+
if (allFilters[key] && allFilters[key].length > 0) {
|
|
77
|
+
console.log(`// --- ${title} (${key}) ---`);
|
|
78
|
+
allFilters[key].forEach(f => {
|
|
79
|
+
console.log(`// ${f.name}=${f.value} => "${f.label}"`);
|
|
80
|
+
});
|
|
81
|
+
console.log();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Print any other categories we didn't anticipate
|
|
85
|
+
console.log("// --- Other Filters ---");
|
|
86
|
+
for (const [key, items] of Object.entries(allFilters)) {
|
|
87
|
+
if (!categories[key] && items.length > 0) {
|
|
88
|
+
console.log(`// Category: ${key}`);
|
|
89
|
+
items.forEach(f => {
|
|
90
|
+
console.log(`// ${f.name}=${f.value} => "${f.label}"`);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await browser.close();
|
|
95
|
+
}
|
|
96
|
+
extractAllFilters().catch(console.error);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Test the original query: Puerto Rico March 7-14, beach + wifi + gym
|
|
2
|
+
import { HotelBrowser } from "./browser.js";
|
|
3
|
+
async function testOriginalQuery() {
|
|
4
|
+
const browser = new HotelBrowser();
|
|
5
|
+
console.log("Initializing browser...");
|
|
6
|
+
await browser.init(false);
|
|
7
|
+
const searchParams = {
|
|
8
|
+
destination: "Puerto Rico",
|
|
9
|
+
checkIn: "2026-03-07",
|
|
10
|
+
checkOut: "2026-03-14",
|
|
11
|
+
guests: 2,
|
|
12
|
+
rooms: 1,
|
|
13
|
+
};
|
|
14
|
+
const filters = {
|
|
15
|
+
beachfront: true,
|
|
16
|
+
freeWifi: true,
|
|
17
|
+
fitness: true,
|
|
18
|
+
minRating: 8.0,
|
|
19
|
+
};
|
|
20
|
+
console.log("\n=== Original Query: Puerto Rico Hotels ===");
|
|
21
|
+
console.log("Dates: March 7-14, 2026");
|
|
22
|
+
console.log("Filters: beachfront, wifi, gym, rating >= 8.0\n");
|
|
23
|
+
const results = await browser.searchHotels(searchParams, filters);
|
|
24
|
+
console.log(`Found ${results.length} hotels:\n`);
|
|
25
|
+
results.slice(0, 10).forEach((hotel, i) => {
|
|
26
|
+
console.log(`${i + 1}. ${hotel.name}`);
|
|
27
|
+
console.log(` Price: ${hotel.priceDisplay}`);
|
|
28
|
+
if (hotel.rating) {
|
|
29
|
+
console.log(` Rating: ${hotel.rating}/10 ${hotel.ratingText} (${hotel.reviewCount || "?"} reviews)`);
|
|
30
|
+
}
|
|
31
|
+
if (hotel.amenities.length > 0) {
|
|
32
|
+
console.log(` Amenities: ${hotel.amenities.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
if (hotel.matchScore !== undefined && hotel.matchScore > 0) {
|
|
35
|
+
console.log(` Match Score: ${hotel.matchScore}`);
|
|
36
|
+
console.log(` Why: ${hotel.matchReasons?.join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
const shortLink = hotel.link.split("?")[0];
|
|
39
|
+
console.log(` Book: ${shortLink}`);
|
|
40
|
+
console.log();
|
|
41
|
+
});
|
|
42
|
+
await browser.takeScreenshot("final-test.png");
|
|
43
|
+
console.log("Screenshot saved to final-test.png");
|
|
44
|
+
await browser.close();
|
|
45
|
+
}
|
|
46
|
+
testOriginalQuery().catch(console.error);
|
package/dist/index.d.ts
ADDED