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
- π Full support for recurring events (e.g., yearly holidays) and custom extra events
- π§Ό Optional cleanup of previously imported events
- π 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
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",
"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 now 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
Paste it into config.json
under "caldav_url"
This project is licensed under the MIT License.
PRs welcome! File issues or ideas via GitHub.
πΊ 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.
(CRO) 0xBAdB43af444055c4031B79a76F74895469BA0CD7 (Cronos)
(USDC) 0xBAdB43af444055c4031B79a76F74895469BA0CD7
(ETH) 0xfc316ba7d8dc325250f1adfafafc320ad75d87c0
(BTC) 1Mhq9SY6DzPhs7PNDx7idXFDWsGtyn7GWM
(BNB) 0xfc316ba7d8dc325250f1adfafafc320ad75d87c0
Crypto.com PayString: magicdude$paystring.crypto.com
Go to Curve.com to add your Crypto.com card to ApplePay and signup to Crypto.com for a staking and free Crypto debit card.
Use Binance Exchange to trade #altcoins. I also accept old-school PayPal.