@mattywhite/skyscanner-api 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/LICENSE +22 -0
- package/README.md +407 -0
- package/package.json +33 -0
- package/src/config.js +41 -0
- package/src/devicedata.json +123 -0
- package/src/errors.js +36 -0
- package/src/index.js +20 -0
- package/src/px.js +286 -0
- package/src/skyscanner.js +634 -0
- package/src/types.js +78 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 @irrisolto (original Python version)
|
|
4
|
+
Copyright (c) 2025 Node.js port contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# SkyScanner API Client (Node.js)
|
|
2
|
+
|
|
3
|
+
A Node.js library for querying the reverse-engineered SkyScanner Android app API for flights, airports, locations, and car rentals, with built-in retries and error handling.
|
|
4
|
+
|
|
5
|
+
> **Note**: This is a Node.js port of the original Python library. Original credit goes to @irrisolto.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Quick Start](#quick-start)
|
|
11
|
+
- [Class Reference](#class-reference)
|
|
12
|
+
- [Methods](#methods)
|
|
13
|
+
- [Types and Enums](#types-and-enums)
|
|
14
|
+
- [Error Handling](#error-handling)
|
|
15
|
+
- [Examples](#examples)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install skyscanner-api
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or if you're installing from source:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```javascript
|
|
32
|
+
const { SkyScanner, CabinClass, SpecialTypes } = require('skyscanner-api');
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
// Initialize the client
|
|
36
|
+
const scanner = new SkyScanner({
|
|
37
|
+
locale: "en-US",
|
|
38
|
+
currency: "USD",
|
|
39
|
+
market: "US"
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Search for airports
|
|
43
|
+
const airports = await scanner.searchAirports("London");
|
|
44
|
+
const origin = airports[0];
|
|
45
|
+
|
|
46
|
+
const destinationAirports = await scanner.searchAirports("New York");
|
|
47
|
+
const destination = destinationAirports[0];
|
|
48
|
+
|
|
49
|
+
// Search for flights
|
|
50
|
+
const departDate = new Date('2025-08-15');
|
|
51
|
+
const returnDate = new Date('2025-08-22');
|
|
52
|
+
|
|
53
|
+
const response = await scanner.getFlightPrices({
|
|
54
|
+
origin: origin,
|
|
55
|
+
destination: destination,
|
|
56
|
+
departDate: departDate,
|
|
57
|
+
returnDate: returnDate,
|
|
58
|
+
cabinClass: CabinClass.ECONOMY,
|
|
59
|
+
adults: 2
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
console.log(response.json);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
main().catch(console.error);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Class Reference
|
|
69
|
+
|
|
70
|
+
### SkyScanner
|
|
71
|
+
|
|
72
|
+
The main client class for interacting with Skyscanner APIs.
|
|
73
|
+
|
|
74
|
+
#### Constructor
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
const scanner = new SkyScanner({
|
|
78
|
+
locale: "en-US", // Locale code for results
|
|
79
|
+
currency: "USD", // Currency code for pricing
|
|
80
|
+
market: "US", // Market region code
|
|
81
|
+
retryDelay: 2, // Seconds to wait between polling retries
|
|
82
|
+
maxRetries: 15, // Maximum number of polling retries
|
|
83
|
+
proxy: "", // Proxy URL for HTTP requests
|
|
84
|
+
pxAuthorization: null, // Optional pre-generated PX authorization token
|
|
85
|
+
verify: true // Whether to verify SSL certificates
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Parameters:**
|
|
90
|
+
- `locale` (string): Locale code for results (e.g., "en-US", "fr-FR")
|
|
91
|
+
- `currency` (string): Currency code for pricing (e.g., "USD", "EUR", "GBP")
|
|
92
|
+
- `market` (string): Market region code (e.g., "US", "UK", "DE")
|
|
93
|
+
- `retryDelay` (number): Seconds to wait between polling retries
|
|
94
|
+
- `maxRetries` (number): Maximum number of polling retries before giving up
|
|
95
|
+
- `proxy` (string): Proxy URL configuration for HTTP requests
|
|
96
|
+
- `pxAuthorization` (string | null): Optional pre-generated PX authorization token
|
|
97
|
+
- `verify` (boolean): Whether to verify SSL certificates
|
|
98
|
+
|
|
99
|
+
## Methods
|
|
100
|
+
|
|
101
|
+
### getFlightPrices()
|
|
102
|
+
|
|
103
|
+
Search for flight prices between two locations.
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
await scanner.getFlightPrices({
|
|
107
|
+
origin, // Airport object
|
|
108
|
+
destination, // Airport | SpecialTypes
|
|
109
|
+
departDate, // Date | SpecialTypes | null
|
|
110
|
+
returnDate, // Date | SpecialTypes | null
|
|
111
|
+
cabinClass, // CabinClass (default: ECONOMY)
|
|
112
|
+
adults, // number (default: 1)
|
|
113
|
+
childAges // number[] (default: [])
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Parameters:**
|
|
118
|
+
- `origin` (Airport): Origin airport object
|
|
119
|
+
- `destination` (Airport | SpecialTypes): Destination airport or special search type (e.g., SpecialTypes.EVERYWHERE)
|
|
120
|
+
- `departDate` (Date | SpecialTypes | null): Departure date or SpecialTypes.ANYTIME
|
|
121
|
+
- `returnDate` (Date | SpecialTypes | null): Return date (optional for one-way trips)
|
|
122
|
+
- `cabinClass` (string): Cabin class preference (from CabinClass enum)
|
|
123
|
+
- `adults` (number): Number of adult passengers (1-8)
|
|
124
|
+
- `childAges` (number[]): Ages of child passengers (0-17 years, max 8 children)
|
|
125
|
+
|
|
126
|
+
**Returns:** `SkyscannerResponse` object containing flight options and pricing data
|
|
127
|
+
|
|
128
|
+
**Throws:**
|
|
129
|
+
- `Error`: Invalid dates, passenger counts, or search parameters
|
|
130
|
+
- `BannedWithCaptcha`: When blocked by Skyscanner's anti-bot measures
|
|
131
|
+
- `AttemptsExhaustedIncompleteResponse`: When max retries exceeded
|
|
132
|
+
|
|
133
|
+
### searchAirports()
|
|
134
|
+
|
|
135
|
+
Auto-suggest airports based on a search query.
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
await scanner.searchAirports(query, departDate, returnDate);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Parameters:**
|
|
142
|
+
- `query` (string): Search text (airport name, city, or IATA code)
|
|
143
|
+
- `departDate` (Date | null): Optional departure date for context
|
|
144
|
+
- `returnDate` (Date | null): Optional return date for context
|
|
145
|
+
|
|
146
|
+
**Returns:** Array of `Airport` objects matching the query
|
|
147
|
+
|
|
148
|
+
### searchLocations()
|
|
149
|
+
|
|
150
|
+
Auto-suggest locations for car rentals and other services.
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
await scanner.searchLocations(query);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Parameters:**
|
|
157
|
+
- `query` (string): Location search text
|
|
158
|
+
|
|
159
|
+
**Returns:** Array of `Location` objects
|
|
160
|
+
|
|
161
|
+
### getAirportByCode()
|
|
162
|
+
|
|
163
|
+
Retrieve a specific airport by its IATA code.
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
await scanner.getAirportByCode(airportCode);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Parameters:**
|
|
170
|
+
- `airportCode` (string): Three-letter IATA airport code (e.g., "JFK", "LHR")
|
|
171
|
+
|
|
172
|
+
**Returns:** `Airport` object for the specified code
|
|
173
|
+
|
|
174
|
+
**Throws:**
|
|
175
|
+
- `GenericError`: If airport code not found
|
|
176
|
+
|
|
177
|
+
### getItineraryDetails()
|
|
178
|
+
|
|
179
|
+
Get detailed information for a specific flight itinerary.
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
await scanner.getItineraryDetails(itineraryId, response);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Parameters:**
|
|
186
|
+
- `itineraryId` (string): Unique itinerary identifier from search results
|
|
187
|
+
- `response` (SkyscannerResponse): Original search response containing session data
|
|
188
|
+
|
|
189
|
+
**Returns:** Object with detailed itinerary information including flight legs and preferences
|
|
190
|
+
|
|
191
|
+
### getCarRental()
|
|
192
|
+
|
|
193
|
+
Search for car rental options between locations and times.
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
await scanner.getCarRental({
|
|
197
|
+
origin, // Location | Coordinates | Airport
|
|
198
|
+
departTime, // Date
|
|
199
|
+
returnTime, // Date
|
|
200
|
+
destination, // Location | Coordinates | Airport | null
|
|
201
|
+
isDriverOver25 // boolean (default: true)
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Parameters:**
|
|
206
|
+
- `origin` (Location | Coordinates | Airport): Pickup location
|
|
207
|
+
- `departTime` (Date): Pickup date and time
|
|
208
|
+
- `returnTime` (Date): Drop-off date and time
|
|
209
|
+
- `destination` (Location | Coordinates | Airport | null): Drop-off location (defaults to origin)
|
|
210
|
+
- `isDriverOver25` (boolean): Driver age flag affecting pricing
|
|
211
|
+
|
|
212
|
+
**Returns:** Object containing car rental options and pricing
|
|
213
|
+
|
|
214
|
+
### getCarRentalFromUrl()
|
|
215
|
+
|
|
216
|
+
Parse a Skyscanner car rental URL and fetch rental options.
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
await scanner.getCarRentalFromUrl(url);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Parameters:**
|
|
223
|
+
- `url` (string): Skyscanner car hire URL
|
|
224
|
+
|
|
225
|
+
**Returns:** Car rental search results
|
|
226
|
+
|
|
227
|
+
**Example URL format:**
|
|
228
|
+
```
|
|
229
|
+
https://www.skyscanner.net/g/carhire-quotes/GB/en-GB/GBP/30/27544008/27544008/2025-07-01T10:00/2025-08-01T10:00/
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Types and Enums
|
|
233
|
+
|
|
234
|
+
### Airport
|
|
235
|
+
```javascript
|
|
236
|
+
const airport = new Airport(
|
|
237
|
+
"London Heathrow", // title
|
|
238
|
+
"27544008", // entityId
|
|
239
|
+
"LHR" // skyId (IATA code)
|
|
240
|
+
);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Location
|
|
244
|
+
```javascript
|
|
245
|
+
const location = new Location(
|
|
246
|
+
"London", // entityName
|
|
247
|
+
"27544008", // entityId
|
|
248
|
+
"51.5074,-0.1278" // coordinates (lat,lng)
|
|
249
|
+
);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### CabinClass
|
|
253
|
+
- `CabinClass.ECONOMY`
|
|
254
|
+
- `CabinClass.PREMIUM_ECONOMY`
|
|
255
|
+
- `CabinClass.BUSINESS`
|
|
256
|
+
- `CabinClass.FIRST`
|
|
257
|
+
|
|
258
|
+
### SpecialTypes
|
|
259
|
+
- `SpecialTypes.ANYTIME` - Flexible date search
|
|
260
|
+
- `SpecialTypes.EVERYWHERE` - Open destination search
|
|
261
|
+
|
|
262
|
+
## Error Handling
|
|
263
|
+
|
|
264
|
+
The library defines several custom exceptions:
|
|
265
|
+
|
|
266
|
+
### BannedWithCaptcha
|
|
267
|
+
Raised when Skyscanner blocks requests with CAPTCHA challenges.
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
try {
|
|
271
|
+
const response = await scanner.getFlightPrices({...});
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error instanceof BannedWithCaptcha) {
|
|
274
|
+
console.log(`Blocked by anti-bot measures: ${error.captchaUrl}`);
|
|
275
|
+
// Consider using proxies or reducing request frequency
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### AttemptsExhaustedIncompleteResponse
|
|
281
|
+
Raised when polling retries are exhausted without getting complete results.
|
|
282
|
+
|
|
283
|
+
```javascript
|
|
284
|
+
try {
|
|
285
|
+
const response = await scanner.getFlightPrices({...});
|
|
286
|
+
} catch (error) {
|
|
287
|
+
if (error instanceof AttemptsExhaustedIncompleteResponse) {
|
|
288
|
+
console.log("Search timed out - try again later");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### GenericError
|
|
294
|
+
General API errors with status codes and response details.
|
|
295
|
+
|
|
296
|
+
## Examples
|
|
297
|
+
|
|
298
|
+
### Basic Flight Search
|
|
299
|
+
|
|
300
|
+
```javascript
|
|
301
|
+
const { SkyScanner } = require('skyscanner-api');
|
|
302
|
+
|
|
303
|
+
async function searchFlights() {
|
|
304
|
+
const scanner = new SkyScanner();
|
|
305
|
+
|
|
306
|
+
// Find airports
|
|
307
|
+
const londonAirports = await scanner.searchAirports("London");
|
|
308
|
+
const heathrow = londonAirports[0];
|
|
309
|
+
|
|
310
|
+
const nycAirports = await scanner.searchAirports("New York");
|
|
311
|
+
const jfk = nycAirports[0];
|
|
312
|
+
|
|
313
|
+
// Search flights
|
|
314
|
+
const response = await scanner.getFlightPrices({
|
|
315
|
+
origin: heathrow,
|
|
316
|
+
destination: jfk,
|
|
317
|
+
departDate: new Date('2025-09-01'),
|
|
318
|
+
returnDate: new Date('2025-09-08'),
|
|
319
|
+
adults: 1
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
console.log(response.json);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
searchFlights().catch(console.error);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Flexible Search (Anywhere, Anytime)
|
|
329
|
+
|
|
330
|
+
```javascript
|
|
331
|
+
const { SkyScanner, SpecialTypes } = require('skyscanner-api');
|
|
332
|
+
|
|
333
|
+
async function flexibleSearch() {
|
|
334
|
+
const scanner = new SkyScanner();
|
|
335
|
+
const londonAirports = await scanner.searchAirports("London");
|
|
336
|
+
const heathrow = londonAirports[0];
|
|
337
|
+
|
|
338
|
+
// Search from London to anywhere
|
|
339
|
+
const response = await scanner.getFlightPrices({
|
|
340
|
+
origin: heathrow,
|
|
341
|
+
destination: SpecialTypes.EVERYWHERE,
|
|
342
|
+
departDate: SpecialTypes.ANYTIME
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
console.log(response.json);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
flexibleSearch().catch(console.error);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Car Rental Search
|
|
352
|
+
|
|
353
|
+
```javascript
|
|
354
|
+
const { SkyScanner } = require('skyscanner-api');
|
|
355
|
+
|
|
356
|
+
async function searchCarRentals() {
|
|
357
|
+
const scanner = new SkyScanner();
|
|
358
|
+
|
|
359
|
+
// Search locations
|
|
360
|
+
const locations = await scanner.searchLocations("London");
|
|
361
|
+
const pickupLocation = locations[0];
|
|
362
|
+
|
|
363
|
+
// Set dates
|
|
364
|
+
const pickupDate = new Date('2025-07-01');
|
|
365
|
+
pickupDate.setHours(10, 0);
|
|
366
|
+
|
|
367
|
+
const returnDate = new Date('2025-07-08');
|
|
368
|
+
returnDate.setHours(10, 0);
|
|
369
|
+
|
|
370
|
+
// Search car rentals
|
|
371
|
+
const rentals = await scanner.getCarRental({
|
|
372
|
+
origin: pickupLocation,
|
|
373
|
+
departTime: pickupDate,
|
|
374
|
+
returnTime: returnDate,
|
|
375
|
+
isDriverOver25: true
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
console.log(rentals);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
searchCarRentals().catch(console.error);
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Rate Limiting and Best Practices
|
|
385
|
+
|
|
386
|
+
1. **Use Proxies**: Consider proxy rotation for high-volume usage
|
|
387
|
+
2. **Cache Results**: Store airport/location searches to reduce API calls
|
|
388
|
+
3. **Validate Inputs**: Check dates and passenger counts before API calls
|
|
389
|
+
4. **Reuse PX Authorization**: X-Px-Authorization header isn't single use. Once you've made one you can use it for multiple requests, but once you get captcha you need to switch IP and authorization
|
|
390
|
+
|
|
391
|
+
## Dependencies
|
|
392
|
+
|
|
393
|
+
- `axios`: HTTP client for making API requests
|
|
394
|
+
- `https-proxy-agent`: Proxy support for HTTPS requests
|
|
395
|
+
|
|
396
|
+
## License
|
|
397
|
+
|
|
398
|
+
MIT
|
|
399
|
+
|
|
400
|
+
## Credits
|
|
401
|
+
|
|
402
|
+
Original Python library by @irrisolto on Discord.
|
|
403
|
+
Node.js port created for broader accessibility.
|
|
404
|
+
|
|
405
|
+
## Disclaimer
|
|
406
|
+
|
|
407
|
+
This library uses reverse-engineered Skyscanner Android app endpoints. Use at your own risk. The library is not affiliated with, endorsed by, or sponsored by Skyscanner.
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mattywhite/skyscanner-api",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Node.js library for querying the reverse-engineered SkyScanner Android app API for flights, airports, locations, and car rentals, with built-in retries and error handling",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node examples/flight_prices.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"skyscanner",
|
|
12
|
+
"flight",
|
|
13
|
+
"api",
|
|
14
|
+
"travel",
|
|
15
|
+
"booking",
|
|
16
|
+
"car-rental",
|
|
17
|
+
"airport",
|
|
18
|
+
"flights"
|
|
19
|
+
],
|
|
20
|
+
"author": "@irrisolto (ported to Node.js)",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"axios": "^1.6.0",
|
|
24
|
+
"https-proxy-agent": "^7.0.2"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/irrisolto/skyscanner"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=14.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
// TLS fingerprint
|
|
4
|
+
const JA3 = '771,4865-4866-4867-49195-49196-52393-49199-49200-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-51-45-43-21,29-23-24,0';
|
|
5
|
+
|
|
6
|
+
const AKAMAI = "4:16777216|16711681|0|m,p,a,s";
|
|
7
|
+
|
|
8
|
+
const EXTRA_FP = {
|
|
9
|
+
tls_signature_algorithms: [
|
|
10
|
+
"ecdsa_secp256r1_sha256",
|
|
11
|
+
"rsa_pss_rsae_sha256",
|
|
12
|
+
"rsa_pkcs1_sha256",
|
|
13
|
+
"ecdsa_secp384r1_sha384",
|
|
14
|
+
"rsa_pss_rsae_sha384",
|
|
15
|
+
"rsa_pkcs1_sha384",
|
|
16
|
+
"rsa_pss_rsae_sha512",
|
|
17
|
+
"rsa_pkcs1_sha512",
|
|
18
|
+
"rsa_pkcs1_sha1"
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Endpoints
|
|
23
|
+
const UNIFIED_SEARCH_ENDPOINT = "https://www.skyscanner.net/g/radar/api/v2/unified-search/";
|
|
24
|
+
const SEARCH_ORIGIN_ENDPOINT = "https://www.skyscanner.net/g/fenryr/v1/inputorigin";
|
|
25
|
+
const ITINERARY_DETAILS_ENDPOINT = 'https://www.skyscanner.net/g/sonar/v3/itinerary/details';
|
|
26
|
+
const LOCATION_SEARCH_ENDPOINT = 'https://www.skyscanner.net/g/autosuggest-search/api/v1/search-car/{market}/{locale}/';
|
|
27
|
+
const CAR_RENTAL_ENDPOINT = 'https://www.skyscanner.net/g/carhire-quotes/{market}/{locale}/{currency}/{driver_age}/{first_location}/{second_location}/{first_date}/{second_date}';
|
|
28
|
+
|
|
29
|
+
const PX_DEVICE_DATA_PATH = path.join(__dirname, 'devicedata.json');
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
JA3,
|
|
33
|
+
AKAMAI,
|
|
34
|
+
EXTRA_FP,
|
|
35
|
+
UNIFIED_SEARCH_ENDPOINT,
|
|
36
|
+
SEARCH_ORIGIN_ENDPOINT,
|
|
37
|
+
ITINERARY_DETAILS_ENDPOINT,
|
|
38
|
+
LOCATION_SEARCH_ENDPOINT,
|
|
39
|
+
CAR_RENTAL_ENDPOINT,
|
|
40
|
+
PX_DEVICE_DATA_PATH
|
|
41
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"height": 2400,
|
|
4
|
+
"width": 1080,
|
|
5
|
+
"pixel_density": 428,
|
|
6
|
+
"model": "Pixel 8",
|
|
7
|
+
"brand": "google",
|
|
8
|
+
"build_device": "shiba",
|
|
9
|
+
"os_version": "15-AP1A.240505.005",
|
|
10
|
+
"cpu_cores": 8,
|
|
11
|
+
"sdk_int": 35
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"height": 3120,
|
|
15
|
+
"width": 1440,
|
|
16
|
+
"pixel_density": 501,
|
|
17
|
+
"model": "SM-S928B",
|
|
18
|
+
"brand": "samsung",
|
|
19
|
+
"build_device": "gts8uq",
|
|
20
|
+
"os_version": "15-UP1A.231005.007.S928BXXU1AXCB",
|
|
21
|
+
"cpu_cores": 8,
|
|
22
|
+
"sdk_int": 35
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"height": 3168,
|
|
26
|
+
"width": 1440,
|
|
27
|
+
"pixel_density": 510,
|
|
28
|
+
"model": "CPH2573",
|
|
29
|
+
"brand": "oneplus",
|
|
30
|
+
"build_device": "pandan",
|
|
31
|
+
"os_version": "15-UKQ1.230804.001",
|
|
32
|
+
"cpu_cores": 8,
|
|
33
|
+
"sdk_int": 35
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"height": 2412,
|
|
37
|
+
"width": 1080,
|
|
38
|
+
"pixel_density": 394,
|
|
39
|
+
"model": "A065",
|
|
40
|
+
"brand": "nothing",
|
|
41
|
+
"build_device": "spacewar",
|
|
42
|
+
"os_version": "15-TKQ1.221013.002",
|
|
43
|
+
"cpu_cores": 8,
|
|
44
|
+
"sdk_int": 35
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"height": 2488,
|
|
48
|
+
"width": 1116,
|
|
49
|
+
"pixel_density": 395,
|
|
50
|
+
"model": "AI2401_A",
|
|
51
|
+
"brand": "asus",
|
|
52
|
+
"build_device": "AI2401",
|
|
53
|
+
"os_version": "15-UP1A.231005.007.33.0820.0820.172",
|
|
54
|
+
"cpu_cores": 8,
|
|
55
|
+
"sdk_int": 35
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"height": 2780,
|
|
59
|
+
"width": 1264,
|
|
60
|
+
"pixel_density": 450,
|
|
61
|
+
"model": "RMX3851",
|
|
62
|
+
"brand": "realme",
|
|
63
|
+
"build_device": "mandrill",
|
|
64
|
+
"os_version": "15-UP1A.231005.007",
|
|
65
|
+
"cpu_cores": 8,
|
|
66
|
+
"sdk_int": 35
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"height": 2400,
|
|
70
|
+
"width": 1080,
|
|
71
|
+
"pixel_density": 402,
|
|
72
|
+
"model": "XT2307-1",
|
|
73
|
+
"brand": "motorola",
|
|
74
|
+
"build_device": "manaus",
|
|
75
|
+
"os_version": "15-UP1A.231005.007",
|
|
76
|
+
"cpu_cores": 8,
|
|
77
|
+
"sdk_int": 35
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"height": 2712,
|
|
81
|
+
"width": 1220,
|
|
82
|
+
"pixel_density": 446,
|
|
83
|
+
"model": "2306EPN60G",
|
|
84
|
+
"brand": "xiaomi",
|
|
85
|
+
"build_device": "aristotle",
|
|
86
|
+
"os_version": "15-UP1A.231005.007",
|
|
87
|
+
"cpu_cores": 8,
|
|
88
|
+
"sdk_int": 35
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"height": 2664,
|
|
92
|
+
"width": 1200,
|
|
93
|
+
"pixel_density": 435,
|
|
94
|
+
"model": "REA-NX9",
|
|
95
|
+
"brand": "honor",
|
|
96
|
+
"build_device": "reehan",
|
|
97
|
+
"os_version": "15-UP1A.231005.007",
|
|
98
|
+
"cpu_cores": 8,
|
|
99
|
+
"sdk_int": 35
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"height": 2412,
|
|
103
|
+
"width": 1080,
|
|
104
|
+
"pixel_density": 394,
|
|
105
|
+
"model": "CPH2521",
|
|
106
|
+
"brand": "oppo",
|
|
107
|
+
"build_device": "PHU110",
|
|
108
|
+
"os_version": "15-UP1A.231005.007",
|
|
109
|
+
"cpu_cores": 8,
|
|
110
|
+
"sdk_int": 35
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"height": 3200,
|
|
114
|
+
"width": 1440,
|
|
115
|
+
"pixel_density": 515,
|
|
116
|
+
"model": "Pixel 8 Pro",
|
|
117
|
+
"brand": "google",
|
|
118
|
+
"build_device": "husky",
|
|
119
|
+
"os_version": "15-UP1A.231005.007.100345",
|
|
120
|
+
"cpu_cores": 8,
|
|
121
|
+
"sdk_int": 35
|
|
122
|
+
}
|
|
123
|
+
]
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
class AttemptsExhaustedIncompleteResponse extends Error {
|
|
2
|
+
constructor(message = "All attempts exhausted and response is still incomplete.") {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "AttemptsExhaustedIncompleteResponse";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class BannedWithCaptcha extends Error {
|
|
9
|
+
constructor(captchaUrl, message = null) {
|
|
10
|
+
const msg = message || `Access banned. CAPTCHA challenge encountered: ${captchaUrl}`;
|
|
11
|
+
super(msg);
|
|
12
|
+
this.name = "BannedWithCaptcha";
|
|
13
|
+
this.captchaUrl = captchaUrl;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class PerimeterXError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "PerimeterXError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class GenericError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "GenericError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
AttemptsExhaustedIncompleteResponse,
|
|
33
|
+
BannedWithCaptcha,
|
|
34
|
+
PerimeterXError,
|
|
35
|
+
GenericError
|
|
36
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const SkyScanner = require('./skyscanner');
|
|
2
|
+
const { Location, Airport, SkyscannerResponse, Coordinates, CabinClass, SpecialTypes } = require('./types');
|
|
3
|
+
const { AttemptsExhaustedIncompleteResponse, BannedWithCaptcha, PerimeterXError, GenericError } = require('./errors');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
SkyScanner,
|
|
7
|
+
Location,
|
|
8
|
+
Airport,
|
|
9
|
+
SkyscannerResponse,
|
|
10
|
+
Coordinates,
|
|
11
|
+
CabinClass,
|
|
12
|
+
SpecialTypes,
|
|
13
|
+
AttemptsExhaustedIncompleteResponse,
|
|
14
|
+
BannedWithCaptcha,
|
|
15
|
+
PerimeterXError,
|
|
16
|
+
GenericError
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Default export
|
|
20
|
+
module.exports.default = SkyScanner;
|