code icon Code

Fetch Calendar Flight Events

Fetch calendar events pre-filtered for flight-related keywords and patterns

Source Code

import fs from "fs";
import path from "path";

const [yearsBack = "3", outputPath = "session/calendar-flights.json"] =
  process.argv.slice(2);
const yearsNum = parseInt(yearsBack) || 3;

// Calculate date range
const endDate = new Date();
const startDate = new Date();
startDate.setFullYear(startDate.getFullYear() - yearsNum);

console.log(`Fetching calendar events from the last ${yearsNum} years...`);
console.log(
  `Date range: ${startDate.toISOString().split("T")[0]} to ${endDate.toISOString().split("T")[0]}`
);

try {
  // Get user's timezone first
  const calRes = await fetch(
    "https://www.googleapis.com/calendar/v3/calendars/primary",
    { headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
  );
  const calData = await calRes.json();
  if (calData.error)
    throw new Error(`Calendar info failed: ${calData.error.message}`);

  const userTimezone = calData.timeZone;
  console.log(`  User timezone: ${userTimezone}`);

  // Fetch all events with pagination
  const events = [];
  let pageToken = null;

  do {
    const params = new URLSearchParams({
      timeMin: startDate.toISOString(),
      timeMax: endDate.toISOString(),
      singleEvents: "true",
      orderBy: "startTime",
      maxResults: "250",
    });
    if (pageToken) params.set("pageToken", pageToken);

    const res = await fetch(
      `https://www.googleapis.com/calendar/v3/calendars/primary/events?${params}`,
      { headers: { Authorization: "Bearer PLACEHOLDER_TOKEN" } }
    );
    const data = await res.json();

    if (data.error)
      throw new Error(`Events fetch failed: ${data.error.message}`);

    events.push(...(data.items || []));
    pageToken = data.nextPageToken;

    console.log(`  Fetched ${events.length} events so far...`);
  } while (pageToken);

  console.log(`\nTotal events found: ${events.length}`);

  // Filter for potential flight-related events
  const flightKeywords = [
    "flight",
    "fly",
    "flying",
    "airport",
    "airline",
    "→",
    "->",
    "depart",
    "arrive",
    "boarding",
    "terminal",
    "gate",
    "layover",
    "connection",
  ];

  // Also look for airport codes (3 uppercase letters)
  const airportCodePattern = /\b[A-Z]{3}\b/;

  const potentialFlightEvents = events.filter((e) => {
    if (e.status === "cancelled") return false;

    const title = (e.summary || "").toLowerCase();
    const description = (e.description || "").toLowerCase();
    const location = (e.location || "").toLowerCase();
    const combined = `${title} ${description} ${location}`;

    // Check keywords
    const hasKeyword = flightKeywords.some((kw) => combined.includes(kw));

    // Check for airport codes in original (case-sensitive) text
    const originalText = `${e.summary || ""} ${e.description || ""} ${e.location || ""}`;
    const hasAirportCode = airportCodePattern.test(originalText);

    // Check for arrow patterns suggesting routes
    const hasRoute = /\s*[-→>]\s*/.test(e.summary || "");

    return hasKeyword || hasAirportCode || hasRoute;
  });

  console.log(`  Potential flight events: ${potentialFlightEvents.length}`);

  // Process events for output
  const processedEvents = potentialFlightEvents.map((event) => {
    const isAllDay = !event.start?.dateTime;
    const startTime = event.start?.dateTime || event.start?.date;
    const endTime = event.end?.dateTime || event.end?.date;

    return {
      id: event.id,
      title: event.summary || "(No title)",
      start: startTime,
      end: endTime,
      isAllDay,
      timezone: event.start?.timeZone || userTimezone,
      location: event.location || null,
      description: event.description || null,
      isRecurring: !!event.recurringEventId,
    };
  });

  // Sort by start time
  processedEvents.sort((a, b) => new Date(a.start) - new Date(b.start));

  // Compute date range of results
  const eventDates = processedEvents
    .map((e) => new Date(e.start))
    .filter((d) => !isNaN(d.getTime()));
  const dateRange =
    eventDates.length > 0
      ? {
          oldest: eventDates[0].toISOString().split("T")[0],
          newest: eventDates[eventDates.length - 1].toISOString().split("T")[0],
        }
      : null;

  // Ensure output directory exists
  const dir = path.dirname(outputPath);
  if (dir && dir !== ".") fs.mkdirSync(dir, { recursive: true });

  // Build output
  const output = {
    query: {
      start: startDate.toISOString().split("T")[0],
      end: endDate.toISOString().split("T")[0],
      yearsBack: yearsNum,
    },
    timezone: userTimezone,
    fetchedAt: new Date().toISOString(),
    totalEventsScanned: events.length,
    count: processedEvents.length,
    dateRange,
    events: processedEvents,
  };

  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));

  console.log(`\n✓ Found ${processedEvents.length} potential flight events`);
  console.log(`  Written to: ${outputPath}`);
  if (dateRange) {
    console.log(`  Date range: ${dateRange.oldest} to ${dateRange.newest}`);
  }

  // Log sample titles
  if (processedEvents.length > 0) {
    console.log(`\n  Sample events:`);
    processedEvents.slice(0, 5).forEach((e) => {
      const date = e.start.split("T")[0];
      console.log(`    - [${date}] ${e.title.slice(0, 50)}`);
    });
  }

  console.log(
    JSON.stringify({
      success: true,
      outputPath,
      totalScanned: events.length,
      count: processedEvents.length,
      dateRange,
    })
  );
} catch (error) {
  console.error("Error fetching calendar events:", error.message);
  throw error;
}