Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd
*.egg-info/
dist/
build/
.eggs/
.venv/
venv/
env/

# Generated calendar output
*.ics

# Test / coverage artefacts
.pytest_cache/
.coverage
htmlcov/

# Editor / OS
.DS_Store
*.swp
.idea/
.vscode/
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,57 @@
# Calendar_Management
Add Calendar events to Outlook

Scrape the [CinemaCon Schedule of Events](https://www.cinemacon.com/en/schedule-of-events) and generate an `.ics` calendar file that can be imported directly into Microsoft Outlook (or any iCalendar-compatible application such as Google Calendar or Apple Calendar).

---

## How it works

1. **`scraper.py`** – Fetches the CinemaCon schedule page and parses all events (title, date, start/end time, location) using three progressive strategies: table-based, card/heading-based, and a plain-text heuristic fallback.
2. **`calendar_integration.py`** – Converts the parsed events into RFC 5545 iCalendar (`VEVENT`) entries and writes them to an `.ics` file.
3. **`main.py`** – Command-line entry point that ties the two modules together.

---

## Requirements

- Python 3.10+

Install dependencies:

```bash
pip install -r requirements.txt
```

---

## Usage

```bash
# Scrape the schedule and write to cinemacon_events.ics (default)
python main.py

# Specify a custom output file
python main.py --output my_calendar.ics

# Enable verbose debug logging
python main.py --verbose

# Use a custom schedule URL
python main.py --url https://www.cinemacon.com/en/schedule-of-events
```

### Importing into Outlook

1. Run `python main.py` to generate `cinemacon_events.ics`.
2. In Outlook, go to **File → Open & Export → Import/Export**.
3. Select **Import an iCalendar (.ics) or vCalendar file (.vcs)** and choose the generated file.
4. The CinemaCon events will be added to your calendar.

---

## Running the tests

```bash
pip install pytest
python -m pytest test_cinemacon.py -v
```
155 changes: 155 additions & 0 deletions calendar_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""
Calendar Integration
Converts CinemaCon schedule events into .ics files importable by Outlook (and
any other iCalendar-compatible application such as Google Calendar or Apple
Calendar).
"""

import logging
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional
from zoneinfo import ZoneInfo

from icalendar import Calendar, Event, vText

from scraper import CinemaConEvent

logger = logging.getLogger(__name__)

# Default event duration when an end-time cannot be parsed from the schedule
DEFAULT_DURATION_HOURS = 1

# Calendar metadata
CALENDAR_PRODID = "-//CinemaCon Schedule//cinemacon.com//EN"
CALENDAR_NAME = "CinemaCon Events"


_LAS_VEGAS_TZ = ZoneInfo("America/Los_Angeles")


def _make_utc(dt: datetime) -> datetime:
"""Ensure a datetime is UTC-aware. Naive datetimes are assumed to be in
US/Pacific (America/Los_Angeles, i.e. Las Vegas) time and are converted
correctly for both PDT (UTC-7) and PST (UTC-8) periods."""
if dt.tzinfo is not None:
return dt.astimezone(timezone.utc)
# Attach the Las Vegas timezone so DST rules are applied automatically
return dt.replace(tzinfo=_LAS_VEGAS_TZ).astimezone(timezone.utc)


def event_to_vevent(event: CinemaConEvent) -> Event:
"""
Convert a CinemaConEvent to an icalendar VEVENT component.

Args:
event: The CinemaCon event to convert.

Returns:
An icalendar Event object.
"""
vevent = Event()

vevent.add("uid", str(uuid.uuid4()))
vevent.add("summary", event.title)

if event.start_time:
start_utc = _make_utc(event.start_time)
vevent.add("dtstart", start_utc)

if event.end_time:
end_utc = _make_utc(event.end_time)
else:
end_utc = start_utc + timedelta(hours=DEFAULT_DURATION_HOURS)
vevent.add("dtend", end_utc)
else:
# All-day placeholder when no time is available
today = datetime.now(tz=timezone.utc).date()
vevent.add("dtstart", today)
vevent.add("dtend", today + timedelta(days=1))

vevent.add("location", vText(event.location))

description_parts = []
if event.description:
description_parts.append(event.description)
if event.tags:
description_parts.append("Tags: " + ", ".join(event.tags))
if description_parts:
vevent.add("description", "\n".join(description_parts))

vevent.add("dtstamp", datetime.now(tz=timezone.utc))

return vevent


def build_calendar(events: list[CinemaConEvent], calendar_name: str = CALENDAR_NAME) -> Calendar:
"""
Build an icalendar Calendar object from a list of CinemaConEvent objects.

Args:
events: List of events to include.
calendar_name: Display name for the calendar.

Returns:
An icalendar Calendar object.
"""
cal = Calendar()
cal.add("prodid", CALENDAR_PRODID)
cal.add("version", "2.0")
cal.add("x-wr-calname", calendar_name)
cal.add("x-wr-timezone", "America/Los_Angeles")
cal.add("calscale", "GREGORIAN")
cal.add("method", "PUBLISH")

for event in events:
vevent = event_to_vevent(event)
cal.add_component(vevent)

logger.info("Built calendar with %d events", len(events))
return cal


def save_calendar(cal: Calendar, output_path: Optional[Path] = None) -> Path:
"""
Serialise the calendar to an .ics file.

Args:
cal: The icalendar Calendar object to save.
output_path: Destination file path. Defaults to
``cinemacon_events.ics`` in the current directory.

Returns:
The path to the saved .ics file.
"""
if output_path is None:
output_path = Path("cinemacon_events.ics")

output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)

ics_bytes = cal.to_ical()
output_path.write_bytes(ics_bytes)
logger.info("Calendar saved to %s", output_path)
return output_path


def create_calendar_file(
events: list[CinemaConEvent],
output_path: Optional[Path] = None,
calendar_name: str = CALENDAR_NAME,
) -> Path:
"""
Convenience function: build and save a calendar from a list of events.

Args:
events: List of CinemaCon events.
output_path: Destination .ics file path.
calendar_name: Display name for the calendar.

Returns:
The path to the saved .ics file.
"""
cal = build_calendar(events, calendar_name=calendar_name)
return save_calendar(cal, output_path)
118 changes: 118 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""
Main entry point for the CinemaCon Calendar Management tool.

Usage:
python main.py [--output OUTPUT] [--url URL] [--verbose]

This script:
1. Scrapes the CinemaCon schedule from https://www.cinemacon.com/en/schedule-of-events
2. Parses all events from the page
3. Generates a .ics calendar file that can be imported into Outlook
(or any iCalendar-compatible application)
"""

import argparse
import logging
import sys
from pathlib import Path

from calendar_integration import create_calendar_file
from scraper import CINEMACON_SCHEDULE_URL, scrape_events


def configure_logging(verbose: bool = False) -> None:
"""Set up console logging."""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%H:%M:%S",
)


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(
description="Scrape CinemaCon schedule and create an Outlook-compatible calendar file.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Examples:\n"
" python main.py\n"
" python main.py --output my_calendar.ics\n"
" python main.py --url https://www.cinemacon.com/en/schedule-of-events --verbose\n"
),
)
parser.add_argument(
"--output",
"-o",
default="cinemacon_events.ics",
metavar="FILE",
help="Output .ics file path (default: cinemacon_events.ics)",
)
parser.add_argument(
"--url",
"-u",
default=CINEMACON_SCHEDULE_URL,
metavar="URL",
help=f"CinemaCon schedule URL (default: {CINEMACON_SCHEDULE_URL})",
)
parser.add_argument(
"--verbose",
"-v",
action="store_true",
help="Enable verbose debug output",
)
return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
"""
Run the CinemaCon scraper and generate a calendar file.

Returns:
0 on success, non-zero on failure.
"""
args = parse_args(argv)
configure_logging(args.verbose)
logger = logging.getLogger(__name__)

logger.info("=== CinemaCon Calendar Manager ===")
logger.info("Source URL : %s", args.url)
logger.info("Output file: %s", args.output)

try:
logger.info("Step 1/2 Scraping schedule …")
events = scrape_events(args.url)
except Exception as exc: # noqa: BLE001
logger.error("Failed to scrape schedule: %s", exc)
return 1

if not events:
logger.warning(
"No events were found on the schedule page. "
"The page layout may have changed. "
"Please check %s manually.",
args.url,
)
return 1

logger.info("Found %d event(s).", len(events))
for i, evt in enumerate(events, 1):
start = evt.start_time.strftime("%a %b %d %H:%M") if evt.start_time else "TBD"
end = evt.end_time.strftime("%H:%M") if evt.end_time else "TBD"
logger.info(" %2d. [%s – %s] %s", i, start, end, evt.title)

try:
logger.info("Step 2/2 Writing calendar file …")
output_path = create_calendar_file(events, output_path=Path(args.output))
except Exception as exc: # noqa: BLE001
logger.error("Failed to write calendar file: %s", exc)
return 1

logger.info("Done! Import '%s' into Outlook (or any iCalendar app) to add", output_path)
logger.info(" these events to your calendar.")
return 0


if __name__ == "__main__":
sys.exit(main())
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
requests>=2.31.0
beautifulsoup4>=4.12.0
icalendar>=5.0.0
python-dateutil>=2.8.2
lxml>=4.9.0
Loading