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
🍺 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.
- 🔁 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
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
First, build the container:
docker-compose build
Then run the sync:
docker-compose run --rm calendar-sync --import
docker-compose run --rm calendar-sync --import --dry-run
docker-compose run --rm calendar-sync --cleanup
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
{
"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": "📦"
}
}
]
}
- 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
usingimport_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Ö
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.
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"
],
Add --dry-run
to see what would happen without making changes:
docker-compose run --rm calendar-sync --import --dry-run
calendar-sync/
├── src/
│ ├── calendar_sync.py # Entry script
│ └── utils.py # Core sync logic
├── config.json # Configuration
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md
Go to Settings → Security → Application Passwords
Select Calendar and Addressbook Client (CalDAV/CardDAV)
Go to the Calendar section → click + Add new calendar
Right-click your new calendar → Properties
→ Copy the URL
This project is licensed under the MIT License.
PRs welcome! File issues or ideas via GitHub.
🍻 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