hotelzero 1.11.0 → 1.12.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 +30 -0
- package/dist/browser.d.ts +23 -0
- package/dist/browser.js +129 -8
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,36 @@ Logs are JSON-formatted for easy parsing:
|
|
|
81
81
|
{"level":"info","time":"2026-02-15T12:00:02.000Z","service":"hotelzero","module":"browser","destination":"Paris","msg":"Starting hotel search"}
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
## Session Persistence
|
|
85
|
+
|
|
86
|
+
HotelZero automatically saves browser session data (cookies, localStorage) to reduce bot detection and avoid repeated CAPTCHA challenges.
|
|
87
|
+
|
|
88
|
+
### How It Works
|
|
89
|
+
|
|
90
|
+
- Sessions are automatically saved after each successful request
|
|
91
|
+
- On startup, the previous session is loaded if available
|
|
92
|
+
- Default session location: `~/.hotelzero/session.json`
|
|
93
|
+
|
|
94
|
+
### Custom Session Path
|
|
95
|
+
|
|
96
|
+
Use `HOTELZERO_SESSION_PATH` to specify a custom location:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Custom session file location
|
|
100
|
+
HOTELZERO_SESSION_PATH=/path/to/session.json npx hotelzero
|
|
101
|
+
|
|
102
|
+
# Disable session persistence (use empty string)
|
|
103
|
+
HOTELZERO_SESSION_PATH="" npx hotelzero
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Clearing Sessions
|
|
107
|
+
|
|
108
|
+
If you experience issues, you can delete the session file:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
rm ~/.hotelzero/session.json
|
|
112
|
+
```
|
|
113
|
+
|
|
84
114
|
## Quick Start
|
|
85
115
|
|
|
86
116
|
### Run as MCP Server
|
package/dist/browser.d.ts
CHANGED
|
@@ -227,12 +227,35 @@ export interface PriceCalendarResult {
|
|
|
227
227
|
}
|
|
228
228
|
export declare class HotelBrowser {
|
|
229
229
|
private browser;
|
|
230
|
+
private context;
|
|
230
231
|
private page;
|
|
231
232
|
private lastRequestTime;
|
|
232
233
|
private minRequestIntervalMs;
|
|
233
234
|
private proxyConfig;
|
|
234
235
|
private currentUserAgent;
|
|
236
|
+
private sessionPath;
|
|
235
237
|
init(headless?: boolean, proxy?: ProxyConfig): Promise<void>;
|
|
238
|
+
/**
|
|
239
|
+
* Save the current session (cookies, localStorage) to disk
|
|
240
|
+
* Call this after successful requests to persist session state
|
|
241
|
+
*/
|
|
242
|
+
saveSession(): Promise<boolean>;
|
|
243
|
+
/**
|
|
244
|
+
* Check if session persistence is enabled
|
|
245
|
+
*/
|
|
246
|
+
hasSessionPath(): boolean;
|
|
247
|
+
/**
|
|
248
|
+
* Get the current session file path
|
|
249
|
+
*/
|
|
250
|
+
getSessionPath(): string | null;
|
|
251
|
+
/**
|
|
252
|
+
* Check if a saved session file exists
|
|
253
|
+
*/
|
|
254
|
+
static hasExistingSession(): boolean;
|
|
255
|
+
/**
|
|
256
|
+
* Clear the saved session file
|
|
257
|
+
*/
|
|
258
|
+
static clearSession(): boolean;
|
|
236
259
|
/**
|
|
237
260
|
* Check if a proxy is configured
|
|
238
261
|
*/
|
package/dist/browser.js
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { chromium } from "playwright";
|
|
2
2
|
import { browserLogger as logger } from "./logger.js";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
// Session/cookie persistence configuration
|
|
6
|
+
const SESSION_PATH = process.env.HOTELZERO_SESSION_PATH || "";
|
|
7
|
+
/**
|
|
8
|
+
* Get the default session file path
|
|
9
|
+
* Uses ~/.hotelzero/session.json if no custom path is specified
|
|
10
|
+
*/
|
|
11
|
+
function getDefaultSessionPath() {
|
|
12
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
13
|
+
return path.join(homeDir, ".hotelzero", "session.json");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the session file path (custom or default)
|
|
17
|
+
*/
|
|
18
|
+
function getSessionPath() {
|
|
19
|
+
return SESSION_PATH || getDefaultSessionPath();
|
|
20
|
+
}
|
|
3
21
|
// Custom error types for better error handling
|
|
4
22
|
export class HotelSearchError extends Error {
|
|
5
23
|
code;
|
|
@@ -259,17 +277,28 @@ const FILTER_CODES = {
|
|
|
259
277
|
};
|
|
260
278
|
export class HotelBrowser {
|
|
261
279
|
browser = null;
|
|
280
|
+
context = null;
|
|
262
281
|
page = null;
|
|
263
282
|
lastRequestTime = 0;
|
|
264
283
|
minRequestIntervalMs = 2000; // Minimum 2 seconds between requests
|
|
265
284
|
proxyConfig = null;
|
|
266
285
|
currentUserAgent = "";
|
|
286
|
+
sessionPath = "";
|
|
267
287
|
async init(headless = true, proxy) {
|
|
268
288
|
// Store proxy config for reference
|
|
269
289
|
this.proxyConfig = proxy || null;
|
|
270
290
|
// Select a random user agent for this session
|
|
271
291
|
this.currentUserAgent = getRandomUserAgent();
|
|
272
|
-
|
|
292
|
+
// Determine session path
|
|
293
|
+
this.sessionPath = getSessionPath();
|
|
294
|
+
const hasExistingSession = this.sessionPath && fs.existsSync(this.sessionPath);
|
|
295
|
+
logger.debug({
|
|
296
|
+
headless,
|
|
297
|
+
hasProxy: !!proxy,
|
|
298
|
+
userAgent: this.currentUserAgent,
|
|
299
|
+
sessionPath: this.sessionPath || "(disabled)",
|
|
300
|
+
hasExistingSession
|
|
301
|
+
}, "Initializing browser");
|
|
273
302
|
// Build launch options
|
|
274
303
|
const launchOptions = {
|
|
275
304
|
headless,
|
|
@@ -284,12 +313,90 @@ export class HotelBrowser {
|
|
|
284
313
|
};
|
|
285
314
|
}
|
|
286
315
|
this.browser = await chromium.launch(launchOptions);
|
|
287
|
-
|
|
316
|
+
// Build context options
|
|
317
|
+
const contextOptions = {
|
|
288
318
|
userAgent: this.currentUserAgent,
|
|
289
319
|
viewport: { width: 1280, height: 900 },
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
320
|
+
};
|
|
321
|
+
// Load existing session state if available
|
|
322
|
+
if (hasExistingSession) {
|
|
323
|
+
try {
|
|
324
|
+
contextOptions.storageState = this.sessionPath;
|
|
325
|
+
logger.debug({ sessionPath: this.sessionPath }, "Loading existing session");
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
logger.warn({ error, sessionPath: this.sessionPath }, "Failed to load session, starting fresh");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
332
|
+
this.page = await this.context.newPage();
|
|
333
|
+
logger.info({
|
|
334
|
+
hasProxy: !!proxy,
|
|
335
|
+
sessionLoaded: hasExistingSession
|
|
336
|
+
}, "Browser initialized successfully");
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Save the current session (cookies, localStorage) to disk
|
|
340
|
+
* Call this after successful requests to persist session state
|
|
341
|
+
*/
|
|
342
|
+
async saveSession() {
|
|
343
|
+
if (!this.context || !this.sessionPath) {
|
|
344
|
+
logger.debug("Cannot save session: no context or session path disabled");
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
// Ensure directory exists
|
|
349
|
+
const sessionDir = path.dirname(this.sessionPath);
|
|
350
|
+
if (!fs.existsSync(sessionDir)) {
|
|
351
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
352
|
+
logger.debug({ sessionDir }, "Created session directory");
|
|
353
|
+
}
|
|
354
|
+
// Save storage state (cookies + localStorage)
|
|
355
|
+
await this.context.storageState({ path: this.sessionPath });
|
|
356
|
+
logger.debug({ sessionPath: this.sessionPath }, "Session saved successfully");
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
logger.warn({ error, sessionPath: this.sessionPath }, "Failed to save session");
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Check if session persistence is enabled
|
|
366
|
+
*/
|
|
367
|
+
hasSessionPath() {
|
|
368
|
+
return !!this.sessionPath;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Get the current session file path
|
|
372
|
+
*/
|
|
373
|
+
getSessionPath() {
|
|
374
|
+
return this.sessionPath || null;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Check if a saved session file exists
|
|
378
|
+
*/
|
|
379
|
+
static hasExistingSession() {
|
|
380
|
+
const sessionPath = getSessionPath();
|
|
381
|
+
return !!sessionPath && fs.existsSync(sessionPath);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Clear the saved session file
|
|
385
|
+
*/
|
|
386
|
+
static clearSession() {
|
|
387
|
+
const sessionPath = getSessionPath();
|
|
388
|
+
if (sessionPath && fs.existsSync(sessionPath)) {
|
|
389
|
+
try {
|
|
390
|
+
fs.unlinkSync(sessionPath);
|
|
391
|
+
logger.info({ sessionPath }, "Session cleared");
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
logger.warn({ error, sessionPath }, "Failed to clear session");
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return false;
|
|
293
400
|
}
|
|
294
401
|
/**
|
|
295
402
|
* Check if a proxy is configured
|
|
@@ -319,6 +426,7 @@ export class HotelBrowser {
|
|
|
319
426
|
if (this.browser) {
|
|
320
427
|
await this.browser.close();
|
|
321
428
|
this.browser = null;
|
|
429
|
+
this.context = null;
|
|
322
430
|
this.page = null;
|
|
323
431
|
logger.debug("Browser closed");
|
|
324
432
|
}
|
|
@@ -665,9 +773,13 @@ export class HotelBrowser {
|
|
|
665
773
|
if (filters) {
|
|
666
774
|
const scored = this.scoreAndFilterHotels(hotels, filters);
|
|
667
775
|
logger.info({ resultCount: scored.length }, "Search completed with filters");
|
|
776
|
+
// Auto-save session after successful search
|
|
777
|
+
await this.saveSession();
|
|
668
778
|
return scored;
|
|
669
779
|
}
|
|
670
780
|
logger.info({ resultCount: hotels.length }, "Search completed");
|
|
781
|
+
// Auto-save session after successful search
|
|
782
|
+
await this.saveSession();
|
|
671
783
|
return hotels;
|
|
672
784
|
}, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
|
|
673
785
|
logger.warn({ attempt, error: error.message, retryInMs: delayMs }, "Search attempt failed, retrying");
|
|
@@ -1545,7 +1657,7 @@ export class HotelBrowser {
|
|
|
1545
1657
|
else {
|
|
1546
1658
|
message = `${result.roomOptions.length} room types available${lowestPrice ? ` from ${lowestPriceRoom?.priceDisplay || '$' + lowestPrice}` : ''}.`;
|
|
1547
1659
|
}
|
|
1548
|
-
|
|
1660
|
+
const availabilityResult = {
|
|
1549
1661
|
available,
|
|
1550
1662
|
hotelName: result.hotelName,
|
|
1551
1663
|
checkIn,
|
|
@@ -1558,6 +1670,9 @@ export class HotelBrowser {
|
|
|
1558
1670
|
message,
|
|
1559
1671
|
url: urlWithDates,
|
|
1560
1672
|
};
|
|
1673
|
+
// Auto-save session after successful availability check
|
|
1674
|
+
await this.saveSession();
|
|
1675
|
+
return availabilityResult;
|
|
1561
1676
|
}, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
|
|
1562
1677
|
logger.warn({ attempt, error: error.message, retryInMs: delayMs }, "Check availability attempt failed, retrying");
|
|
1563
1678
|
});
|
|
@@ -1814,7 +1929,7 @@ export class HotelBrowser {
|
|
|
1814
1929
|
location: mainPageData.breakdown.location ?? null,
|
|
1815
1930
|
freeWifi: mainPageData.breakdown.freeWifi ?? null,
|
|
1816
1931
|
};
|
|
1817
|
-
|
|
1932
|
+
const reviewsResult = {
|
|
1818
1933
|
hotelName: mainPageData.hotelName,
|
|
1819
1934
|
overallRating: mainPageData.overallRating,
|
|
1820
1935
|
totalReviews: mainPageData.totalReviews,
|
|
@@ -1822,6 +1937,9 @@ export class HotelBrowser {
|
|
|
1822
1937
|
reviews: reviews.slice(0, limit),
|
|
1823
1938
|
url: cleanUrl,
|
|
1824
1939
|
};
|
|
1940
|
+
// Auto-save session after successful reviews fetch
|
|
1941
|
+
await this.saveSession();
|
|
1942
|
+
return reviewsResult;
|
|
1825
1943
|
}, DEFAULT_RETRY_CONFIG, (attempt, error, delayMs) => {
|
|
1826
1944
|
logger.warn({ attempt, error: error.message, retryInMs: delayMs }, "Get reviews attempt failed, retrying");
|
|
1827
1945
|
});
|
|
@@ -1991,7 +2109,7 @@ export class HotelBrowser {
|
|
|
1991
2109
|
// Calculate end date
|
|
1992
2110
|
const endDate = new Date(start);
|
|
1993
2111
|
endDate.setDate(endDate.getDate() + actualNights - 1);
|
|
1994
|
-
|
|
2112
|
+
const priceCalendarResult = {
|
|
1995
2113
|
hotelName,
|
|
1996
2114
|
startDate,
|
|
1997
2115
|
endDate: endDate.toISOString().split("T")[0],
|
|
@@ -2005,5 +2123,8 @@ export class HotelBrowser {
|
|
|
2005
2123
|
averagePrice,
|
|
2006
2124
|
url: cleanUrl,
|
|
2007
2125
|
};
|
|
2126
|
+
// Auto-save session after successful price calendar fetch
|
|
2127
|
+
await this.saveSession();
|
|
2128
|
+
return priceCalendarResult;
|
|
2008
2129
|
}
|
|
2009
2130
|
}
|
package/dist/index.js
CHANGED