code icon Code

Parse iCal

Parse iCal/ICS calendar files into structured events

Source Code

import fs from "fs";
import path from "path";
import ICAL from "ical.js";

const [inputPath, outputPath, startDate = "", endDate = ""] = process.argv.slice(2);

if (!inputPath || !outputPath) {
  console.error("Usage: inputPath outputPath [startDate] [endDate]");
  process.exit(1);
}

try {
  console.log(`Parsing iCal: ${inputPath}...`);
  const icsContent = fs.readFileSync(inputPath, "utf-8");

  const jcalData = ICAL.parse(icsContent);
  const comp = new ICAL.Component(jcalData);
  const vevents = comp.getAllSubcomponents("vevent");

  console.log(`  Found ${vevents.length} events`);

  const startFilter = startDate ? new Date(startDate) : null;
  const endFilter = endDate ? new Date(endDate) : null;

  const events = [];

  for (const vevent of vevents) {
    const event = new ICAL.Event(vevent);

    const eventStart = event.startDate?.toJSDate();
    const eventEnd = event.endDate?.toJSDate();

    // Apply date filters
    if (startFilter && eventEnd && eventEnd < startFilter) continue;
    if (endFilter && eventStart && eventStart > endFilter) continue;

    // Extract attendees
    const attendees = vevent.getAllProperties("attendee").map((att) => {
      const params = att.jCal[1] || {};
      return {
        email: att.getFirstValue()?.replace("mailto:", "") || null,
        name: params.cn || null,
        role: params.role || null,
        status: params.partstat || null,
      };
    });

    events.push({
      uid: event.uid || null,
      summary: event.summary || null,
      description: event.description || null,
      location: event.location || null,
      start: eventStart?.toISOString() || null,
      end: eventEnd?.toISOString() || null,
      allDay: event.startDate?.isDate || false,
      organizer: event.organizer || null,
      attendees,
      recurrence: event.isRecurring() ? "recurring" : "single",
      status: vevent.getFirstPropertyValue("status") || null,
    });
  }

  // Sort by start date
  events.sort((a, b) => {
    if (!a.start) return 1;
    if (!b.start) return -1;
    return new Date(a.start) - new Date(b.start);
  });

  const result = {
    calendar: {
      name: comp.getFirstPropertyValue("x-wr-calname") || null,
      timezone: comp.getFirstPropertyValue("x-wr-timezone") || null,
    },
    events,
  };

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

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

  console.log(`\nāœ“ Parsed iCal`);
  console.log(`  Events: ${events.length}`);
  if (startFilter || endFilter) {
    console.log(`  Date range: ${startDate || "any"} to ${endDate || "any"}`);
  }
  console.log(`  Written to: ${outputPath}`);

  console.log(
    JSON.stringify({
      success: true,
      inputPath,
      outputPath,
      eventCount: events.length,
      calendarName: result.calendar.name,
    })
  );
} catch (error) {
  console.error("Error:", error.message);
  process.exit(1);
}