google-flights-ts 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wooldox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,543 @@
1
+ # ✈️ google-flights-ts
2
+
3
+ A fast, strongly-typed Google Flights scraper for Node.js.
4
+
5
+ TypeScript port of [AWeirdDev/flights](https://github.com/AWeirdDev/flights). Encodes search parameters into Google's protobuf-based `tfs` query parameter, fetches the results page, and parses flight data from the HTML or embedded JS response.
6
+
7
+ > **Zero binary dependencies** — the protobuf encoding is hand-rolled, no native modules needed.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Install](#install)
12
+ - [Quick Start](#quick-start)
13
+ - [API Reference](#api-reference)
14
+ - [`getFlights`](#getflightsoptions)
15
+ - [`getFlightsFromFilter`](#getflightsfromfilterfilter-options)
16
+ - [`FetchFn`](#fetchfn)
17
+ - [`createFilter`](#createfilteroptions)
18
+ - [`FlightData`](#new-flightdataoptions)
19
+ - [`Passengers`](#new-passengersoptions)
20
+ - [`searchAirport`](#searchairportquery)
21
+ - [`Cookies`](#cookies)
22
+ - [Fetch Modes](#fetch-modes)
23
+ - [Data Sources](#data-sources)
24
+ - [Examples](#examples)
25
+ - [How It Works](#how-it-works)
26
+ - [Scripts](#scripts)
27
+ - [License](#license)
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ npm install google-flights-ts
33
+ ```
34
+
35
+ ### Optional: Local Playwright
36
+
37
+ If you want to use the `local` fetch mode (headless browser):
38
+
39
+ ```bash
40
+ npm install playwright
41
+ npx playwright install chromium
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ```ts
47
+ import { FlightData, Passengers, getFlights } from "google-flights-ts";
48
+
49
+ const result = await getFlights({
50
+ flight_data: [
51
+ new FlightData({
52
+ date: "2026-07-01",
53
+ from_airport: "JFK",
54
+ to_airport: "LAX",
55
+ }),
56
+ ],
57
+ trip: "one-way",
58
+ seat: "economy",
59
+ adults: 1,
60
+ });
61
+
62
+ if (result && "flights" in result) {
63
+ console.log("Price trend:", result.current_price); // "low" | "typical" | "high"
64
+ for (const flight of result.flights) {
65
+ console.log(`${flight.name} ${flight.departure}-${flight.arrival} $${flight.price}`);
66
+ }
67
+ }
68
+ ```
69
+
70
+ ## API Reference
71
+
72
+ ### `getFlights(options)`
73
+
74
+ High-level function that builds a filter and fetches results in one call.
75
+
76
+ ```ts
77
+ const result = await getFlights({
78
+ // Required
79
+ flight_data: FlightData[],
80
+ trip: "one-way" | "round-trip" | "multi-city",
81
+
82
+ // Passengers (either pass a Passengers object or use convenience counters)
83
+ passengers?: Passengers,
84
+ adults?: number, // default: 1
85
+ children?: number, // default: 0
86
+ infants_in_seat?: number, // default: 0
87
+ infants_on_lap?: number, // default: 0
88
+
89
+ // Optional
90
+ seat?: "economy" | "premium-economy" | "business" | "first", // default: "economy"
91
+ fetch_mode?: FetchMode, // default: "common"
92
+ max_stops?: number, // undefined = any
93
+ data_source?: "html" | "js", // default: "html"
94
+ cookies?: Record<string, string>,
95
+ request_options?: RequestOptions,
96
+ cookie_consent?: boolean, // default: true
97
+ fetchFn?: FetchFn, // custom fetch implementation (e.g. for proxies)
98
+ });
99
+ ```
100
+
101
+ **Returns:** `Promise<Result | DecodedResult | null>`
102
+
103
+ - `Result` when `data_source` is `"html"` (default)
104
+ - `DecodedResult` when `data_source` is `"js"`
105
+ - `null` if no data found
106
+
107
+ ---
108
+
109
+ ### `getFlightsFromFilter(filter, options?)`
110
+
111
+ Lower-level function that takes a pre-built `TFSData` filter.
112
+
113
+ ```ts
114
+ import { createFilter, getFlightsFromFilter, FlightData, Passengers } from "google-flights-ts";
115
+
116
+ const filter = createFilter({
117
+ flight_data: [
118
+ new FlightData({ date: "2026-07-01", from_airport: "LHR", to_airport: "SLC" }),
119
+ new FlightData({ date: "2026-07-15", from_airport: "SLC", to_airport: "LHR" }),
120
+ ],
121
+ trip: "round-trip",
122
+ passengers: new Passengers({ adults: 2, children: 1 }),
123
+ seat: "economy",
124
+ max_stops: 1,
125
+ });
126
+
127
+ const result = await getFlightsFromFilter(filter, {
128
+ currency: "USD",
129
+ mode: "common",
130
+ data_source: "html",
131
+ });
132
+ ```
133
+
134
+ **Options:**
135
+
136
+ | Option | Type | Default | Description |
137
+ |--------|------|---------|-------------|
138
+ | `currency` | `string` | `""` | Currency code (e.g. `"USD"`, `"EUR"`) |
139
+ | `mode` | `FetchMode` | `"common"` | How to fetch the page (see [Fetch Modes](#fetch-modes)) |
140
+ | `data_source` | `"html" \| "js"` | `"html"` | Which parser to use (see [Data Sources](#data-sources)) |
141
+ | `cookies` | `Record<string, string>` | - | Custom cookies to send |
142
+ | `request_options` | `RequestOptions` | - | Custom headers/cookies for the request |
143
+ | `cookie_consent` | `boolean` | `true` | Auto-include default consent cookies |
144
+ | `fetchFn` | `FetchFn` | `fetch` | Custom fetch function (e.g. for proxy support) |
145
+
146
+ ---
147
+
148
+ ### `FetchFn`
149
+
150
+ Type alias for a custom fetch implementation. Useful when you need to route requests through a proxy or add custom transport logic.
151
+
152
+ ```ts
153
+ type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
154
+ ```
155
+
156
+ Both `getFlights` and `getFlightsFromFilter` accept an optional `fetchFn` parameter. When provided, it replaces the global `fetch` for the HTTP request to Google Flights.
157
+
158
+ ```ts
159
+ import { FlightData, getFlights, type FetchFn } from "google-flights-ts";
160
+
161
+ // Example: route through a SOCKS proxy using node:https + socks-proxy-agent
162
+ import { SocksProxyAgent } from "socks-proxy-agent";
163
+ import https from "node:https";
164
+
165
+ const agent = new SocksProxyAgent("socks5://127.0.0.1:1080");
166
+
167
+ const proxyFetch: FetchFn = (url, init) =>
168
+ new Promise((resolve, reject) => {
169
+ const parsed = new URL(url);
170
+ const req = https.request(
171
+ {
172
+ hostname: parsed.hostname,
173
+ path: parsed.pathname + parsed.search,
174
+ method: init?.method ?? "GET",
175
+ headers: init?.headers as Record<string, string>,
176
+ agent,
177
+ },
178
+ (res) => {
179
+ const chunks: Buffer[] = [];
180
+ res.on("data", (c: Buffer) => chunks.push(c));
181
+ res.on("end", () =>
182
+ resolve(
183
+ new Response(Buffer.concat(chunks), {
184
+ status: res.statusCode ?? 200,
185
+ headers: new Headers(res.headers as Record<string, string>),
186
+ }),
187
+ ),
188
+ );
189
+ },
190
+ );
191
+ req.on("error", reject);
192
+ req.end();
193
+ });
194
+
195
+ const result = await getFlights({
196
+ flight_data: [
197
+ new FlightData({ date: "2026-07-01", from_airport: "JFK", to_airport: "LAX" }),
198
+ ],
199
+ trip: "one-way",
200
+ adults: 1,
201
+ fetchFn: proxyFetch,
202
+ });
203
+ ```
204
+
205
+ ---
206
+
207
+ ### `createFilter(options)`
208
+
209
+ Builds a `TFSData` filter object without fetching. Useful when you want to inspect the encoded URL or reuse the filter.
210
+
211
+ ```ts
212
+ const filter = createFilter({
213
+ flight_data: [new FlightData({ date: "2026-07-01", from_airport: "JFK", to_airport: "LAX" })],
214
+ trip: "one-way",
215
+ passengers: new Passengers({ adults: 1 }),
216
+ seat: "economy",
217
+ });
218
+
219
+ // Get the Google Flights URL
220
+ const url = `https://www.google.com/travel/flights?tfs=${filter.toBase64()}`;
221
+ console.log(url);
222
+ ```
223
+
224
+ ---
225
+
226
+ ### `new FlightData(options)`
227
+
228
+ Represents one leg of a journey.
229
+
230
+ ```ts
231
+ new FlightData({
232
+ date: string, // "YYYY-MM-DD"
233
+ from_airport: string, // 3-letter IATA code or AIRPORTS key
234
+ to_airport: string, // 3-letter IATA code or AIRPORTS key
235
+ max_stops?: number, // 0 = nonstop, 1, 2, etc.
236
+ airlines?: string[], // 2-letter codes or alliance names
237
+ })
238
+ ```
239
+
240
+ **Airport codes** can be either raw IATA codes (`"JFK"`, `"LAX"`) or the full name from the `AIRPORTS` object (`"ZURICH_AIRPORT"` resolves to `"ZRH"`).
241
+
242
+ **Airlines** accepts 2-letter IATA airline codes (`"UA"`, `"AA"`) or alliance names: `"STAR_ALLIANCE"`, `"ONEWORLD"`, `"SKYTEAM"`.
243
+
244
+ ---
245
+
246
+ ### `new Passengers(options?)`
247
+
248
+ ```ts
249
+ new Passengers({
250
+ adults?: number, // default: 0
251
+ children?: number, // default: 0
252
+ infants_in_seat?: number, // default: 0
253
+ infants_on_lap?: number, // default: 0
254
+ })
255
+ ```
256
+
257
+ **Constraints:**
258
+ - Total passengers must be <= 9
259
+ - `infants_on_lap` must be <= `adults`
260
+
261
+ ---
262
+
263
+ ### `searchAirport(query)`
264
+
265
+ Case-insensitive search across all 3,311 airports.
266
+
267
+ ```ts
268
+ import { searchAirport } from "google-flights-ts";
269
+
270
+ const results = searchAirport("zurich");
271
+ // [{ name: "ZURICH_AIRPORT", code: "ZRH" }]
272
+
273
+ const results2 = searchAirport("NEW_YORK");
274
+ // [{ name: "NEW_YORK_JOHN_F_KENNEDY_INTERNATIONAL_AIRPORT", code: "JFK" }, ...]
275
+ ```
276
+
277
+ ---
278
+
279
+ ### `Cookies`
280
+
281
+ Generate Google consent cookies for requests.
282
+
283
+ ```ts
284
+ import { Cookies } from "google-flights-ts";
285
+
286
+ const cookies = Cookies.create({ locale: "en" });
287
+ cookies.toDict(); // { CONSENT: "PENDING+987", SOCS: "<base64>" }
288
+ cookies.toBase64(); // base64-encoded SOCS protobuf
289
+ ```
290
+
291
+ The library embeds default consent cookies automatically (controlled by the `cookie_consent` option). You only need `Cookies` if you want to generate fresh ones or customize the locale.
292
+
293
+ ---
294
+
295
+ ## Fetch Modes
296
+
297
+ | Mode | Description |
298
+ |------|-------------|
299
+ | `"common"` | Direct HTTP request with Chrome-like headers. Fastest option. **(default)** |
300
+ | `"fallback"` | Tries `common` first, falls back to serverless Playwright if it fails. Recommended for reliability. |
301
+ | `"force-fallback"` | Always uses serverless Playwright via `try.playwright.tech`. |
302
+ | `"local"` | Uses a local Playwright browser. Requires the `playwright` package. Best for privacy. |
303
+ | `"bright-data"` | Uses Bright Data proxy API. Requires `BRIGHT_DATA_API_KEY` env var. |
304
+
305
+ ### Bright Data Configuration
306
+
307
+ Set these environment variables:
308
+
309
+ | Variable | Required | Default |
310
+ |----------|----------|---------|
311
+ | `BRIGHT_DATA_API_KEY` | Yes | - |
312
+ | `BRIGHT_DATA_API_URL` | No | `https://api.brightdata.com/request` |
313
+ | `BRIGHT_DATA_SERP_ZONE` | No | `serp_api1` |
314
+
315
+ ---
316
+
317
+ ## Data Sources
318
+
319
+ ### HTML mode (`data_source: "html"`) -- default
320
+
321
+ Parses the rendered HTML using CSS selectors. Returns a `Result`:
322
+
323
+ ```ts
324
+ interface Result {
325
+ current_price: "low" | "typical" | "high" | string;
326
+ flights: Flight[];
327
+ }
328
+
329
+ interface Flight {
330
+ is_best: boolean;
331
+ name: string; // Airline name
332
+ departure: string; // e.g. "2:30 PM"
333
+ arrival: string; // e.g. "11:45 PM"
334
+ arrival_time_ahead: string; // e.g. "+1 day"
335
+ duration: string; // e.g. "18h 15m"
336
+ stops: number | string; // 0 for nonstop, or "Unknown"
337
+ delay: string | null;
338
+ price: string; // e.g. "523" (no currency symbol)
339
+ }
340
+ ```
341
+
342
+ ### JS mode (`data_source: "js"`)
343
+
344
+ Extracts structured data from the embedded JavaScript. Returns a `DecodedResult` with richer detail:
345
+
346
+ ```ts
347
+ interface DecodedResult {
348
+ raw: unknown[];
349
+ best: Itinerary[];
350
+ other: Itinerary[];
351
+ }
352
+
353
+ interface Itinerary {
354
+ airline_code: string;
355
+ airline_names: string[];
356
+ flights: DecodedFlight[];
357
+ layovers: Layover[];
358
+ travel_time: number; // minutes
359
+ departure_airport: string;
360
+ arrival_airport: string;
361
+ departure_date: [number, number, number]; // [year, month, day]
362
+ arrival_date: [number, number, number];
363
+ departure_time: [number, number]; // [hour, minute]
364
+ arrival_time: [number, number];
365
+ itinerary_summary: ItinerarySummary;
366
+ }
367
+
368
+ interface DecodedFlight {
369
+ airline: string;
370
+ airline_name: string;
371
+ flight_number: string;
372
+ operator: string;
373
+ codeshares: Codeshare[];
374
+ aircraft: string;
375
+ departure_airport: string;
376
+ departure_airport_name: string;
377
+ arrival_airport: string;
378
+ arrival_airport_name: string;
379
+ departure_date: [number, number, number];
380
+ arrival_date: [number, number, number];
381
+ departure_time: [number, number];
382
+ arrival_time: [number, number];
383
+ travel_time: number;
384
+ seat_pitch_short: string;
385
+ }
386
+
387
+ interface Layover {
388
+ minutes: number;
389
+ departure_airport: string;
390
+ departure_airport_name: string;
391
+ departure_airport_city: string;
392
+ arrival_airport: string;
393
+ arrival_airport_name: string;
394
+ arrival_airport_city: string;
395
+ }
396
+
397
+ interface Codeshare {
398
+ airline_code: string;
399
+ flight_number: number;
400
+ airline_name: string;
401
+ }
402
+
403
+ interface ItinerarySummary {
404
+ flights: string;
405
+ price: number; // dollars (e.g. 150.00)
406
+ currency: string; // e.g. "USD"
407
+ }
408
+ ```
409
+
410
+ ---
411
+
412
+ ## Examples
413
+
414
+ ### One-way flight
415
+
416
+ ```ts
417
+ import { FlightData, getFlights } from "google-flights-ts";
418
+
419
+ const result = await getFlights({
420
+ flight_data: [
421
+ new FlightData({ date: "2026-07-01", from_airport: "SFO", to_airport: "NRT" }),
422
+ ],
423
+ trip: "one-way",
424
+ adults: 1,
425
+ seat: "business",
426
+ max_stops: 1,
427
+ });
428
+ ```
429
+
430
+ ### Round trip
431
+
432
+ ```ts
433
+ import { FlightData, Passengers, getFlights } from "google-flights-ts";
434
+
435
+ const result = await getFlights({
436
+ flight_data: [
437
+ new FlightData({ date: "2026-12-20", from_airport: "LHR", to_airport: "JFK" }),
438
+ new FlightData({ date: "2027-01-03", from_airport: "JFK", to_airport: "LHR" }),
439
+ ],
440
+ trip: "round-trip",
441
+ passengers: new Passengers({ adults: 2, children: 1, infants_on_lap: 1 }),
442
+ seat: "premium-economy",
443
+ fetch_mode: "fallback",
444
+ });
445
+ ```
446
+
447
+ ### JS data source (detailed itinerary info)
448
+
449
+ ```ts
450
+ import { FlightData, getFlights, type DecodedResult } from "google-flights-ts";
451
+
452
+ const result = await getFlights({
453
+ flight_data: [
454
+ new FlightData({ date: "2026-10-04", from_airport: "SJC", to_airport: "LAS" }),
455
+ ],
456
+ trip: "one-way",
457
+ adults: 1,
458
+ data_source: "js",
459
+ }) as DecodedResult | null;
460
+
461
+ if (result) {
462
+ for (const itinerary of result.best) {
463
+ console.log(`${itinerary.airline_names.join(", ")} - ${itinerary.travel_time} min`);
464
+ console.log(` $${itinerary.itinerary_summary.price} ${itinerary.itinerary_summary.currency}`);
465
+ for (const flight of itinerary.flights) {
466
+ console.log(` ${flight.flight_number} ${flight.departure_airport}->${flight.arrival_airport} (${flight.aircraft})`);
467
+ }
468
+ for (const layover of itinerary.layovers) {
469
+ console.log(` Layover: ${layover.minutes} min at ${layover.departure_airport_name}`);
470
+ }
471
+ }
472
+ }
473
+ ```
474
+
475
+ ### Filter-only (generate URL without fetching)
476
+
477
+ ```ts
478
+ import { FlightData, Passengers, createFilter } from "google-flights-ts";
479
+
480
+ const filter = createFilter({
481
+ flight_data: [
482
+ new FlightData({ date: "2026-07-01", from_airport: "EWR", to_airport: "CDG" }),
483
+ ],
484
+ trip: "one-way",
485
+ passengers: new Passengers({ adults: 1 }),
486
+ seat: "economy",
487
+ });
488
+
489
+ console.log(`https://www.google.com/travel/flights?tfs=${filter.toBase64()}`);
490
+ ```
491
+
492
+ ### Custom cookies and headers
493
+
494
+ ```ts
495
+ import { FlightData, Cookies, getFlights } from "google-flights-ts";
496
+
497
+ const cookies = Cookies.create({ locale: "de" });
498
+
499
+ const result = await getFlights({
500
+ flight_data: [
501
+ new FlightData({ date: "2026-08-15", from_airport: "FRA", to_airport: "BKK" }),
502
+ ],
503
+ trip: "one-way",
504
+ adults: 1,
505
+ cookies: cookies.toDict(),
506
+ request_options: {
507
+ headers: { "Accept-Language": "de-DE,de;q=0.9" },
508
+ },
509
+ cookie_consent: false, // disable default cookies since we're providing our own
510
+ });
511
+ ```
512
+
513
+ ---
514
+
515
+ ## How It Works
516
+
517
+ 1. **Protobuf encoding** -- Flight search parameters (airports, dates, passengers, seat class, trip type) are serialized into a Protocol Buffer binary message, then base64-encoded. This becomes the `tfs` query parameter that Google Flights uses internally.
518
+
519
+ 2. **HTTP request** -- The encoded `tfs` parameter is sent to `https://www.google.com/travel/flights` using one of five fetch strategies (direct HTTP, serverless Playwright, local Playwright, or Bright Data proxy).
520
+
521
+ 3. **Response parsing** -- The HTML response is parsed with [cheerio](https://cheerio.js.org/) to extract flight cards (HTML mode), or the embedded JavaScript data blob is decoded from nested JSON arrays (JS mode).
522
+
523
+ The protobuf encoding is hand-rolled (no runtime protobuf library needed) since the schema is small and fixed. The `.proto` definitions are in `src/proto/` for reference.
524
+
525
+ ---
526
+
527
+ ## Scripts
528
+
529
+ ```bash
530
+ npm run build # Compile TypeScript to dist/
531
+ npm test # Run tests (vitest)
532
+ npm run test:watch # Run tests in watch mode
533
+ ```
534
+
535
+ ---
536
+
537
+ ## Disclaimer
538
+
539
+ This project is not affiliated with, endorsed by, or associated with Google. "Google Flights" is a trademark of Google LLC. This library is provided as-is for personal and educational use. Use responsibly and in accordance with Google's Terms of Service.
540
+
541
+ ## License
542
+
543
+ MIT