# ✈️ google-flights-ts

A fast, strongly-typed Google Flights scraper for Node.js.

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.

> **Zero binary dependencies** — the protobuf encoding is hand-rolled, no native modules needed.

## Table of Contents

- [Install](#install)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
  - [`getFlights`](#getflightsoptions)
  - [`getFlightsFromFilter`](#getflightsfromfilterfilter-options)
  - [`FetchFn`](#fetchfn)
  - [`createFilter`](#createfilteroptions)
  - [`FlightData`](#new-flightdataoptions)
  - [`Passengers`](#new-passengersoptions)
  - [`searchAirport`](#searchairportquery)
  - [`Cookies`](#cookies)
- [Fetch Modes](#fetch-modes)
- [Data Sources](#data-sources)
- [Examples](#examples)
- [How It Works](#how-it-works)
- [Scripts](#scripts)
- [License](#license)

## Install

```bash
npm install google-flights-ts
```

### Optional: Local Playwright

If you want to use the `local` fetch mode (headless browser):

```bash
npm install playwright
npx playwright install chromium
```

## Quick Start

```ts
import { FlightData, Passengers, getFlights } from "google-flights-ts";

const result = await getFlights({
  flight_data: [
    new FlightData({
      date: "2026-07-01",
      from_airport: "JFK",
      to_airport: "LAX",
    }),
  ],
  trip: "one-way",
  seat: "economy",
  adults: 1,
});

if (result && "flights" in result) {
  console.log("Price trend:", result.current_price); // "low" | "typical" | "high"
  for (const flight of result.flights) {
    console.log(`${flight.name} ${flight.departure}-${flight.arrival} $${flight.price}`);
  }
}
```

## API Reference

### `getFlights(options)`

High-level function that builds a filter and fetches results in one call.

```ts
const result = await getFlights({
  // Required
  flight_data: FlightData[],
  trip: "one-way" | "round-trip" | "multi-city",

  // Passengers (either pass a Passengers object or use convenience counters)
  passengers?: Passengers,
  adults?: number,        // default: 1
  children?: number,      // default: 0
  infants_in_seat?: number, // default: 0
  infants_on_lap?: number,  // default: 0

  // Optional
  seat?: "economy" | "premium-economy" | "business" | "first", // default: "economy"
  fetch_mode?: FetchMode,       // default: "common"
  max_stops?: number,           // undefined = any
  data_source?: "html" | "js",  // default: "html"
  cookies?: Record<string, string>,
  request_options?: RequestOptions,
  cookie_consent?: boolean,     // default: true
  fetchFn?: FetchFn,            // custom fetch implementation (e.g. for proxies)
});
```

**Returns:** `Promise<Result | DecodedResult | null>`

- `Result` when `data_source` is `"html"` (default)
- `DecodedResult` when `data_source` is `"js"`
- `null` if no data found

---

### `getFlightsFromFilter(filter, options?)`

Lower-level function that takes a pre-built `TFSData` filter.

```ts
import { createFilter, getFlightsFromFilter, FlightData, Passengers } from "google-flights-ts";

const filter = createFilter({
  flight_data: [
    new FlightData({ date: "2026-07-01", from_airport: "LHR", to_airport: "SLC" }),
    new FlightData({ date: "2026-07-15", from_airport: "SLC", to_airport: "LHR" }),
  ],
  trip: "round-trip",
  passengers: new Passengers({ adults: 2, children: 1 }),
  seat: "economy",
  max_stops: 1,
});

const result = await getFlightsFromFilter(filter, {
  currency: "USD",
  mode: "common",
  data_source: "html",
});
```

**Options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `currency` | `string` | `""` | Currency code (e.g. `"USD"`, `"EUR"`) |
| `mode` | `FetchMode` | `"common"` | How to fetch the page (see [Fetch Modes](#fetch-modes)) |
| `data_source` | `"html" \| "js"` | `"html"` | Which parser to use (see [Data Sources](#data-sources)) |
| `cookies` | `Record<string, string>` | - | Custom cookies to send |
| `request_options` | `RequestOptions` | - | Custom headers/cookies for the request |
| `cookie_consent` | `boolean` | `true` | Auto-include default consent cookies |
| `fetchFn` | `FetchFn` | `fetch` | Custom fetch function (e.g. for proxy support) |

---

### `FetchFn`

Type alias for a custom fetch implementation. Useful when you need to route requests through a proxy or add custom transport logic.

```ts
type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
```

Both `getFlights` and `getFlightsFromFilter` accept an optional `fetchFn` parameter. When provided, it replaces the global `fetch` for the HTTP request to Google Flights.

```ts
import { FlightData, getFlights, type FetchFn } from "google-flights-ts";

// Example: route through a SOCKS proxy using node:https + socks-proxy-agent
import { SocksProxyAgent } from "socks-proxy-agent";
import https from "node:https";

const agent = new SocksProxyAgent("socks5://127.0.0.1:1080");

const proxyFetch: FetchFn = (url, init) =>
  new Promise((resolve, reject) => {
    const parsed = new URL(url);
    const req = https.request(
      {
        hostname: parsed.hostname,
        path: parsed.pathname + parsed.search,
        method: init?.method ?? "GET",
        headers: init?.headers as Record<string, string>,
        agent,
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on("data", (c: Buffer) => chunks.push(c));
        res.on("end", () =>
          resolve(
            new Response(Buffer.concat(chunks), {
              status: res.statusCode ?? 200,
              headers: new Headers(res.headers as Record<string, string>),
            }),
          ),
        );
      },
    );
    req.on("error", reject);
    req.end();
  });

const result = await getFlights({
  flight_data: [
    new FlightData({ date: "2026-07-01", from_airport: "JFK", to_airport: "LAX" }),
  ],
  trip: "one-way",
  adults: 1,
  fetchFn: proxyFetch,
});
```

---

### `createFilter(options)`

Builds a `TFSData` filter object without fetching. Useful when you want to inspect the encoded URL or reuse the filter.

```ts
const filter = createFilter({
  flight_data: [new FlightData({ date: "2026-07-01", from_airport: "JFK", to_airport: "LAX" })],
  trip: "one-way",
  passengers: new Passengers({ adults: 1 }),
  seat: "economy",
});

// Get the Google Flights URL
const url = `https://www.google.com/travel/flights?tfs=${filter.toBase64()}`;
console.log(url);
```

---

### `new FlightData(options)`

Represents one leg of a journey.

```ts
new FlightData({
  date: string,           // "YYYY-MM-DD"
  from_airport: string,   // 3-letter IATA code or AIRPORTS key
  to_airport: string,     // 3-letter IATA code or AIRPORTS key
  max_stops?: number,     // 0 = nonstop, 1, 2, etc.
  airlines?: string[],    // 2-letter codes or alliance names
})
```

**Airport codes** can be either raw IATA codes (`"JFK"`, `"LAX"`) or the full name from the `AIRPORTS` object (`"ZURICH_AIRPORT"` resolves to `"ZRH"`).

**Airlines** accepts 2-letter IATA airline codes (`"UA"`, `"AA"`) or alliance names: `"STAR_ALLIANCE"`, `"ONEWORLD"`, `"SKYTEAM"`.

---

### `new Passengers(options?)`

```ts
new Passengers({
  adults?: number,          // default: 0
  children?: number,        // default: 0
  infants_in_seat?: number, // default: 0
  infants_on_lap?: number,  // default: 0
})
```

**Constraints:**
- Total passengers must be <= 9
- `infants_on_lap` must be <= `adults`

---

### `searchAirport(query)`

Case-insensitive search across all 3,311 airports.

```ts
import { searchAirport } from "google-flights-ts";

const results = searchAirport("zurich");
// [{ name: "ZURICH_AIRPORT", code: "ZRH" }]

const results2 = searchAirport("NEW_YORK");
// [{ name: "NEW_YORK_JOHN_F_KENNEDY_INTERNATIONAL_AIRPORT", code: "JFK" }, ...]
```

---

### `Cookies`

Generate Google consent cookies for requests.

```ts
import { Cookies } from "google-flights-ts";

const cookies = Cookies.create({ locale: "en" });
cookies.toDict();    // { CONSENT: "PENDING+987", SOCS: "<base64>" }
cookies.toBase64();  // base64-encoded SOCS protobuf
```

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.

---

## Fetch Modes

| Mode | Description |
|------|-------------|
| `"common"` | Direct HTTP request with Chrome-like headers. Fastest option. **(default)** |
| `"fallback"` | Tries `common` first, falls back to serverless Playwright if it fails. Recommended for reliability. |
| `"force-fallback"` | Always uses serverless Playwright via `try.playwright.tech`. |
| `"local"` | Uses a local Playwright browser. Requires the `playwright` package. Best for privacy. |
| `"bright-data"` | Uses Bright Data proxy API. Requires `BRIGHT_DATA_API_KEY` env var. |

### Bright Data Configuration

Set these environment variables:

| Variable | Required | Default |
|----------|----------|---------|
| `BRIGHT_DATA_API_KEY` | Yes | - |
| `BRIGHT_DATA_API_URL` | No | `https://api.brightdata.com/request` |
| `BRIGHT_DATA_SERP_ZONE` | No | `serp_api1` |

---

## Data Sources

### HTML mode (`data_source: "html"`) -- default

Parses the rendered HTML using CSS selectors. Returns a `Result`:

```ts
interface Result {
  current_price: "low" | "typical" | "high" | string;
  flights: Flight[];
}

interface Flight {
  is_best: boolean;
  name: string;           // Airline name
  departure: string;      // e.g. "2:30 PM"
  arrival: string;        // e.g. "11:45 PM"
  arrival_time_ahead: string; // e.g. "+1 day"
  duration: string;       // e.g. "18h 15m"
  stops: number | string; // 0 for nonstop, or "Unknown"
  delay: string | null;
  price: string;          // e.g. "523" (no currency symbol)
}
```

### JS mode (`data_source: "js"`)

Extracts structured data from the embedded JavaScript. Returns a `DecodedResult` with richer detail:

```ts
interface DecodedResult {
  raw: unknown[];
  best: Itinerary[];
  other: Itinerary[];
}

interface Itinerary {
  airline_code: string;
  airline_names: string[];
  flights: DecodedFlight[];
  layovers: Layover[];
  travel_time: number;          // minutes
  departure_airport: string;
  arrival_airport: string;
  departure_date: [number, number, number]; // [year, month, day]
  arrival_date: [number, number, number];
  departure_time: [number, number];         // [hour, minute]
  arrival_time: [number, number];
  itinerary_summary: ItinerarySummary;
}

interface DecodedFlight {
  airline: string;
  airline_name: string;
  flight_number: string;
  operator: string;
  codeshares: Codeshare[];
  aircraft: string;
  departure_airport: string;
  departure_airport_name: string;
  arrival_airport: string;
  arrival_airport_name: string;
  departure_date: [number, number, number];
  arrival_date: [number, number, number];
  departure_time: [number, number];
  arrival_time: [number, number];
  travel_time: number;
  seat_pitch_short: string;
}

interface Layover {
  minutes: number;
  departure_airport: string;
  departure_airport_name: string;
  departure_airport_city: string;
  arrival_airport: string;
  arrival_airport_name: string;
  arrival_airport_city: string;
}

interface Codeshare {
  airline_code: string;
  flight_number: number;
  airline_name: string;
}

interface ItinerarySummary {
  flights: string;
  price: number;   // dollars (e.g. 150.00)
  currency: string; // e.g. "USD"
}
```

---

## Examples

### One-way flight

```ts
import { FlightData, getFlights } from "google-flights-ts";

const result = await getFlights({
  flight_data: [
    new FlightData({ date: "2026-07-01", from_airport: "SFO", to_airport: "NRT" }),
  ],
  trip: "one-way",
  adults: 1,
  seat: "business",
  max_stops: 1,
});
```

### Round trip

```ts
import { FlightData, Passengers, getFlights } from "google-flights-ts";

const result = await getFlights({
  flight_data: [
    new FlightData({ date: "2026-12-20", from_airport: "LHR", to_airport: "JFK" }),
    new FlightData({ date: "2027-01-03", from_airport: "JFK", to_airport: "LHR" }),
  ],
  trip: "round-trip",
  passengers: new Passengers({ adults: 2, children: 1, infants_on_lap: 1 }),
  seat: "premium-economy",
  fetch_mode: "fallback",
});
```

### JS data source (detailed itinerary info)

```ts
import { FlightData, getFlights, type DecodedResult } from "google-flights-ts";

const result = await getFlights({
  flight_data: [
    new FlightData({ date: "2026-10-04", from_airport: "SJC", to_airport: "LAS" }),
  ],
  trip: "one-way",
  adults: 1,
  data_source: "js",
}) as DecodedResult | null;

if (result) {
  for (const itinerary of result.best) {
    console.log(`${itinerary.airline_names.join(", ")} - ${itinerary.travel_time} min`);
    console.log(`  $${itinerary.itinerary_summary.price} ${itinerary.itinerary_summary.currency}`);
    for (const flight of itinerary.flights) {
      console.log(`  ${flight.flight_number} ${flight.departure_airport}->${flight.arrival_airport} (${flight.aircraft})`);
    }
    for (const layover of itinerary.layovers) {
      console.log(`  Layover: ${layover.minutes} min at ${layover.departure_airport_name}`);
    }
  }
}
```

### Filter-only (generate URL without fetching)

```ts
import { FlightData, Passengers, createFilter } from "google-flights-ts";

const filter = createFilter({
  flight_data: [
    new FlightData({ date: "2026-07-01", from_airport: "EWR", to_airport: "CDG" }),
  ],
  trip: "one-way",
  passengers: new Passengers({ adults: 1 }),
  seat: "economy",
});

console.log(`https://www.google.com/travel/flights?tfs=${filter.toBase64()}`);
```

### Custom cookies and headers

```ts
import { FlightData, Cookies, getFlights } from "google-flights-ts";

const cookies = Cookies.create({ locale: "de" });

const result = await getFlights({
  flight_data: [
    new FlightData({ date: "2026-08-15", from_airport: "FRA", to_airport: "BKK" }),
  ],
  trip: "one-way",
  adults: 1,
  cookies: cookies.toDict(),
  request_options: {
    headers: { "Accept-Language": "de-DE,de;q=0.9" },
  },
  cookie_consent: false, // disable default cookies since we're providing our own
});
```

---

## How It Works

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.

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).

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).

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.

---

## Scripts

```bash
npm run build       # Compile TypeScript to dist/
npm test            # Run tests (vitest)
npm run test:watch  # Run tests in watch mode
```

---

## Disclaimer

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.

## License

MIT
