Skip to content

Latest commit

 

History

History
307 lines (234 loc) · 11.3 KB

File metadata and controls

307 lines (234 loc) · 11.3 KB

📅 calendar-sync

GitHub stars Build Python GitHub forks GitHub issues GitHub last commit License

calendar-sync is a flexible utility to sync one or more ICS feeds (iCalendar) into a CalDAV-compatible calendar — ideal for mailbox.org, Nextcloud, Synology, and more.

It supports features such as:

  • ✅ Deterministic UID generation for clean deduplication
  • 📅 Emoji mapping for more readable calendar events
  • 🔁 Automatic expansion of RRULE:FREQ=YEARLY events
  • 🔁 Full support for recurring events (e.g., yearly holidays) and custom extra events (Mother’s Day, Advent Sundays, etc.)
  • 🧼 Cleanup mode with multi-prefix support (--cleanup PREFIX1,PREFIX2)
  • 📍 Location-based filtering (e.g., for regional holidays in Austria)
  • 🐳 Docker deployment for simple automation
  • 💡 Dry-run mode to preview changes without writing
  • 🕓 Timezone-aware handling for accurate scheduling

This is perfect for importing:

  • 🗑️ Municipal waste collection schedules (e.g., Müll App)
  • 🇦🇹 Austrian public holidays
  • 🏎️ Formula 1 calendar with free practice, qualifying, and GP events

Unlike subscription-based ICS calendars, this tool writes events directly into your calendar, giving you full control over notifications, offline visibility, and data retention.

Use it on your Synology NAS, a server, or as a cron-triggered Docker container — and never miss a bin collection or Grand Prix again.

calendar-sync.mp4


paypal


paypal 🍺 Please support me: Although all my software is free, it is always appreciated if you can support my efforts on Github with a contribution via Paypal - this allows me to write cool projects like this in my personal time and hopefully help you or your business.


✨ Features

  • 🔁 Sync multiple ICS feeds to any CalDAV calendar
  • 🧠 Deterministic UID generation & deduplication
  • 🔁 Automatic expansion of YEARLY recurring events
  • 📍 Location-based filtering for region-specific holidays
  • 🧹 Optional cleanup of old imported events
  • 📅 Supports emoji mapping for event names
  • 🛑 Dry run mode to test before writing
  • 🐳 Docker support for simple deployment

🚀 Usage

Manual

# Default config file (config.json)
python src/calendar_sync.py --import
python src/calendar_sync.py --import --dry-run
python src/calendar_sync.py --cleanup # cleans global prefix
python src/calendar_sync.py --cleanup MUELL-,F1- # cleans multiple prefixes

# Custom config file
python src/calendar_sync.py --import --config /path/to/another_config.json
python src/calendar_sync.py --import --dry-run --config /path/to/another_config.json
python src/calendar_sync.py --cleanup --config /path/to/another_config.json

With Docker Compose

First, build the container:

docker-compose build

Then run the sync:

# Default config file
docker-compose run --rm calendar-sync --import
docker-compose run --rm calendar-sync --import --dry-run
docker-compose run --rm calendar-sync --cleanup

# Custom config file (mount the config file into the container)
docker-compose run --rm -v /path/to/another_config.json:/app/config.json calendar-sync --import
docker-compose run --rm -v /path/to/another_config.json:/app/config.json calendar-sync --import --dry-run
docker-compose run --rm -v /path/to/another_config.json:/app/config.json calendar-sync --cleanup

🧰 Manual Installation

git clone https://github.com/magicdude4eva/calendar-sync.git
cd calendar-sync
python3.13 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

⚙️ Configuration

You can use multiple config files to manage different calendars. By default, the script uses config.json. To use a different config file, pass it via the --config argument:

python src/calendar_sync.py --import --config /path/to/another_config.json

Example config:

{
  "caldav_url": "https://dav-sso.mailbox.org/caldav/...",
  "username": "your@email.com",
  "password": "your-app-password",
  "timezone": "Europe/Vienna",
  "uid_prefix": "ICS-",
  "future_event_limit_days": 365,
  "ics_feeds": [
    {
      "url": "https://example.com/my.ics",
      "uid_prefix": "EXAMPLE-",
      "emoji_mapping": {
        "Papier": "♻️",
        "default": "📦"
      },
      "default_reminder": "1d"
    }
  ]
}

🛠️ How It Works

  • Fetches events from each configured ICS feed
  • Normalizes dates and checks if the UID exists
  • Skips, adds, or replaces events as needed
  • Uses emoji mappings to prefix event names
  • All-day events are handled properly (no time zone shift)
  • Recurring RRULE:FREQ=YEARLY events are expanded into individual years
  • Events can be filtered by LOCATION using import_locations

🗺️ For import_locations, configure it per feed. For example:

{
  "url": "https://www.feiertage-oesterreich.at/kalender-download/ics/feiertage-oesterreich.ics",
  "import_locations": "K,St,V",
  "emoji_mapping": {
    "§": "🇦🇹",
    "default": "🗓️"
  }
}

To discover valid locations, run the sync once and check the logs. Example:

INFO: ⏭️ Skipping 'St. Florian' (2025-05-04) due to unmatched location: OÖ

🗓️ Support for Yearly Recurring Events (RRULE:FREQ=YEARLY)

The script automatically expands ICS events with RRULE:FREQ=YEARLY rules into individual event instances for each year, up to the configured future limit (future_event_limit_days). This ensures recurring events like public holidays or anniversaries are correctly synced across multiple years.

Behavior:

  • Detects yearly recurring events by scanning raw RRULE data.
  • Expands the base event for each year (e.g. from 2025 to 2026).
  • Skips events in the past or beyond the future limit.
  • Deduplicates intelligently using UID hashing per year.

➕ Support for Custom Extra Events

In addition to ICS feeds, you can define your own custom events using the extra_events entry in config.json.

This allows you to add things like:

  • 🌷 Mother's Day (2nd Sunday of May)
  • 👨‍👧‍👦 Father's Day (2nd Sunday of June)
  • 🔥 Summer Solstice (21st June)
  • 🎃 Halloween (31st October)
  • 🕯️ Advent Sundays
  • 🧾 Tax Deadlines
  • ☀️ Daylight Saving Time changes

Supported Formats:

Format Description Example
N.Weekday.Month Nth weekday of a month 2.Sunday.5 → 2nd Sunday in May
-N.Weekday.Month Nth weekday from end of month -1.Sunday.3 → last Sunday in March
DD.MM.fixed Fixed date 31.10.fixed → 31st October

Sample:

    "extra_events": [
      "☀️ Sommerzeit beginnt:-1.Sunday.3",
      "🌷 Muttertag:2.Sunday.5",
      "👨‍👧‍👦 Vatertag:2.Sunday.6",
      "🔥 Sonnwendfeier:21.6.fixed",
      "🧾 Steuererklärung:30.6.fixed",
      "🌒 Sommerzeit endet:-1.Sunday.10",
      "🎃 Halloween:31.10.fixed",
      "🕯️ 1. Advent:-4.Sunday.12",
      "🕯️ 2. Advent:-3.Sunday.12",
      "🕯️ 3. Advent:-2.Sunday.12",
      "🕯️ 4. Advent:-1.Sunday.12",
      "👹 Krampusnacht:5.12.fixed",
      "🎅 Nikolaus:6.12.fixed"    
      ],

🧪 Dry Run

Add --dry-run to see what would happen without making changes:

docker-compose run --rm calendar-sync --import --dry-run

🗂️ Project Structure

calendar-sync/
├── src/
│   ├── calendar_sync.py      # Entry script
│   └── utils.py              # Core sync logic
├── config.json               # Configuration
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md

📷 mailbox.org Setup Guide

1. 🔐 Create Application Password

Go to Settings → Security → Application Passwords
Select Calendar and Addressbook Client (CalDAV/CardDAV)
Create Application Password


2. 📅 Create a New Calendar

Go to the Calendar section → click + Add new calendar
Create a New Calendar


3. 🔗 Get the CalDAV URL

Right-click your new calendar → Properties → Copy the URL
Get the CalDAV URL

Paste it into config.json under "caldav_url"

📄 License

This project is licensed under the MIT License.


❤️ Contributing

PRs welcome! File issues or ideas via GitHub.

Donations are always welcome

🍻 Support my work
All my software is free and built in my personal time. If it helps you or your business, please consider a small donation via PayPal — it keeps the coffee ☕ and ideas flowing!

💸 Crypto Donations
You can also send crypto to one of the addresses below:

(BTC)   bc1qdgdkk7l98pje8ny9u4xavsvrea8dw6yu8jpnyf
(ETH)   0x5986f713A538D6bCaC0865564dCD45E2600A3469  
(POL)   0x5986f713A538D6bCaC0865564dCD45E2600A3469
(CRO)   0xb83c3Fe378F5224fAdD7a0f8a7dD33a6C96C422C (Cronos or Crypto.com Paystring magicdude$paystring.crypto.com)
(BNB)   0x5986f713A538D6bCaC0865564dCD45E2600A3469
(LTC)   ltc1qexst2exxksfyg7erfzlfrm23twkjgf7e5fn64t
(DOGE)  DMQsxc9XGF6526drBJDZeX7AjFDJsEz4mN
(SOL)   t4bYQCUuoCUrp7kJ4Mz314npcTuKoUSXj28UgdMrfTb

🧾 Recommended Platforms

  • 👉 Curve.com: Add your Crypto.com card to Apple Pay
  • 🔐 Crypto.com: Stake and get your free Crypto Visa card
  • 📈 Binance: Trade altcoins easily