Sync Schweizer Handball-Spielpläne von handball.ch in Nextcloud-Kalender (oder jeden anderen CalDAV-Server) — pro Mannschaft ein Kalender, vollautomatisch.
Sync Swiss handball schedules from handball.ch into Nextcloud calendars (or any other CalDAV server) — one calendar per team, fully automated.
Sprache / Language: Deutsch · English
Pro Mannschaft deines Vereins wird ein eigener Nextcloud-Kalender erstellt und periodisch aktualisiert. Statt jedem Spieler manuell ICS-Dateien zu schicken oder den SpielerPlus-Import zu wiederholen, abonnierst du einmalig den Kalender — Resultate, Hallenwechsel und Verschiebungen erscheinen automatisch.
Gebaut für Vereine, die bereits Nextcloud selbst hosten und einen sauberen, teilbaren Kalender pro Team (Junioren, Herren, Damen) haben wollen. Funktioniert aber gegen jeden CalDAV-Server — nicht nur Nextcloud.
- Ein Kalender pro Team, z.B.
Handball - U15oderHandball - Herren 2. Liga - Stabile iCalendar-UIDs → Resultate, Hallenwechsel und Verschiebungen werden beim nächsten Sync übernommen, keine doppelten Events
- Identifiziert Teams über
teamId, also funktionieren auch Spielgemeinschaften mit gleichlautenden Team-Namen - Heim/Auswärts im Titel (
vs TV Muttenz (Heim)), nach gespieltem Match inkl. Endresultat (vs TV Muttenz 25:28 (Heim)) - Halle, Adresse, Liga, Spielnummer und HZ/FT-Resultat in der Beschreibung
- Idempotent — beliebig oft ausführbar, keine Duplikate
- Läuft als kleiner (~80 MB) Alpine-Container mit eingebautem Cron
- Übersteht die Off-Season als No-op (keine Spiele → nichts zu tun)
- Kompatibel mit Nextcloud, Radicale, Baïkal, SOGo, ownCloud, Apple iCloud und allen anderen CalDAV-konformen Servern
handball.ch (REST) ──► shv-sync (Python) ──► CalDAV Server
│
▼
Handys / SpielerPlus / Outlook
Das Schweizerische Handballverband (SHV) stellt unter
clubapi.handball.ch
eine REST-API zur Verfügung. Jeder Verein bekommt HTTP-Basic-Credentials
über das Vereins Admin Tool (VAT). Das Script lädt alle Teams und Spiele
eines Vereins, gruppiert die Spiele nach Team und schreibt pro Team einen
CalDAV-Kalender.
-
Ein handball.ch-Verein mit aktiviertem Dataservice. Die Credentials werden vom SHV via VAT (
vat.handball.ch) ausgegeben — frag denjenigen in deinem Verein, der das Admin-Tool verwaltet. -
Ein CalDAV-Server, den du erreichen kannst. Nextcloud ist die Standardannahme, aber jeder CalDAV-Server geht (siehe CalDAV-Kompatibilität).
-
Ein dedizierter Service-User auf deinem CalDAV-Server, z.B.
handball-sync. Bei Nextcloud anlegen via Web UI oderocc:docker exec -e OC_PASS='<starkes-passwort>' -u www-data nextcloud \ php occ user:add --password-from-env --display-name='Handball Sync' handball-sync
-
Docker + Docker Compose.
git clone https://github.com/choli41/shv-nextcloud-sync.git
cd shv-nextcloud-sync
cp .env.example .env
$EDITOR .env # CLUB_ID, HANDBALL_API_KEY, CALDAV_* eintragen
# 1. Schauen was die API für deinen Verein zurückgibt
docker compose run --rm shv-sync python sync.py --discover
# 2. Probelauf — alles laden, aber nichts schreiben
docker compose run --rm shv-sync python sync.py --dry-run
# 3. Echter Lauf, dann als Daemon starten
docker compose run --rm shv-sync python sync.py
docker compose up -d shv-syncDer Container macht beim Start einen initialen Sync und plant sich danach
selbst gemäss SYNC_CRON (Default: jeden Mittwoch 06:00, 0 6 * * 3).
-
Mit dem Vereins-Admin-Account in VAT einloggen.
-
Dataservice öffnen — dort kannst du das Passwort generieren bzw. ansehen.
-
API-Key lokal bauen:
echo -n "140285:deinpasswort" | base64 # → MTQwMjg1OmRlaW5wYXNzd29ydA==
Dieser Base64-String ist dein
HANDBALL_API_KEY.
Es gibt auch eine Test-Umgebung mit separaten Daten:
- prod:
https://clubapi.handball.ch - test:
https://clubapi-test.handball.ch - swagger:
https://clubapi-test.handball.ch/swagger/index.html
| Variable | Pflicht | Default | Beschreibung |
|---|---|---|---|
CLUB_ID |
ja | — | Numerische Vereins-ID aus den handball.ch URLs |
HANDBALL_API_KEY |
ja | — | Base64 von clubid:passwort aus VAT Dataservice |
HANDBALL_API_BASE |
nein | https://clubapi.handball.ch |
Test-URL für Entwicklung verwenden |
CALDAV_URL |
ja | — | DAV-Root, z.B. https://nc.example.com/remote.php/dav |
CALDAV_USER |
ja | — | Service-User |
CALDAV_PASSWORD |
ja | — | Service-User-Passwort (oder App-Passwort) |
CALENDAR_PREFIX |
nein | Handball |
Präfix für Kalender-Anzeigenamen |
GAME_DURATION_HOURS |
nein | 2 |
Standarddauer eines Spiels |
EVENT_DOMAIN |
nein | shv-nextcloud-sync.local |
Wird in iCalendar-UIDs verwendet; eigene Domain einsetzen |
SYNC_CRON |
nein | 0 6 * * 3 |
5-Feld-Cron-Ausdruck für den Sync |
TZ |
nein | Europe/Zurich |
Zeitzone im Container |
Das Script nutzt die generische caldav
Library und funktioniert mit allen CalDAV-konformen Servern. Hier die häufigsten
URL-Formate:
| Server | CALDAV_URL Beispiel |
|---|---|
| Nextcloud (gleiches Docker-Netz) | http://nextcloud/remote.php/dav |
| Nextcloud (extern) | https://nc.example.com/remote.php/dav |
| Radicale | http://radicale:5232/handball-sync/ |
| Baïkal | https://baikal.example.com/dav.php/ |
| SOGo | https://sogo.example.com/SOGo/dav/handball-sync/ |
| ownCloud | https://oc.example.com/remote.php/dav |
| Apple iCloud | https://caldav.icloud.com/ |
Radicale ist die leichtgewichtigste Option, falls du Nextcloud nicht installieren willst. Komplettes Setup:
# docker-compose.yml — fügt einen Radicale-Server neben shv-sync hinzu
services:
radicale:
image: tomsquest/docker-radicale
container_name: radicale
restart: unless-stopped
volumes:
- ./radicale-data:/data
ports:
- "5232:5232"
shv-sync:
# ... wie im Standard docker-compose.yml ...
environment:
- CALDAV_URL=http://radicale:5232/handball-sync/
- CALDAV_USER=handball-sync
- CALDAV_PASSWORD=changeme
depends_on:
- radicaleUser in Radicale anlegen (htpasswd ist Default-Auth):
docker exec radicale htpasswd -B -c /data/users handball-syncBaïkal ist ein PHP-CalDAV-Server, ideal für Mini-Setups. Nach dem Web-Wizard einen User anlegen, dann:
CALDAV_URL=https://baikal.example.com/dav.php/
CALDAV_USER=handball-sync
CALDAV_PASSWORD=changemeiCloud verlangt ein App-spezifisches Passwort (nicht dein Apple-ID-Passwort): appleid.apple.com → Anmeldung und Sicherheit → App-spezifische Passwörter. iCloud entdeckt den korrekten Pfad automatisch:
CALDAV_URL=https://caldav.icloud.com/
CALDAV_USER=deine.apple.id@icloud.com
CALDAV_PASSWORD=app-spezifisches-passwortDas Script versucht, Kalendern basierend auf dem leagueShort-Feld der API
saubere Anzeigenamen zu geben:
leagueShort |
Kalendername |
|---|---|
M2, M3, M4, … |
Handball - Herren 2. Liga, … |
F2, F3, F4, … |
Handball - Damen 2. Liga, … |
MU13P S1, MU15P S2, MU17P S1, … |
Handball - U13, Handball - U15, Handball - U17, … |
| alles andere | Handball - <leagueLong> |
Wenn du andere Konventionen willst (z.B. die Spielklasse M2 direkt im Namen),
passe make_calendar_name() in sync.py an.
Sobald die Kalender befüllt sind, melde dich als Service-User in Nextcloud (oder deinem CalDAV-Server) an und nutze die Standard-Sharing-Optionen:
- Read-only mit anderen Nextcloud-Usern teilen — sie sehen die Kalender in ihrer Calendar-App.
- Öffentlicher Link / iCal-Export-URL — für SpielerPlus, Outlook, Apple Calendar etc. Hinweis: SpielerPlus' "Import via URL" macht eine einmalige Kopie, kein Live-Abo.
Access through untrusted domain wenn das Script Nextcloud im selben
Docker-Netzwerk via CALDAV_URL=http://nextcloud/remote.php/dav erreicht:
der Hostname nextcloud ist nicht in den trusted_domains von Nextcloud.
Einmal hinzufügen:
docker exec -u www-data nextcloud php occ config:system:set \
trusted_domains 2 --value=nextcloud401 Unauthorized von handball.ch: dein HANDBALL_API_KEY ist falsch
oder der Dataservice ist für deinen Verein nicht aktiviert. In VAT prüfen.
Gleiche Mannschaft mehrfach in --discover: die API liefert eine Zeile
pro (team, group)-Kombination. Das Script dedupliziert intern über die
teamId, der eigentliche Sync ist also korrekt.
Keine Teams für deinen Verein: prüfe, ob die CLUB_ID mit der Nummer
in der offiziellen URL https://www.handball.ch/de/matchcenter/vereine/<id>
übereinstimmt.
MIT — siehe LICENSE.
Dieses Projekt ist nicht mit dem Schweizerischen Handballverband (SHV) verbunden, von ihm gefördert oder gesponsert. Es nutzt lediglich die öffentlich dokumentierte Open ClubApi.
Creates one Nextcloud calendar per team in your club and keeps it up to date automatically. Instead of sending ICS files to every player or re-importing into SpielerPlus over and over, you subscribe once — scores, venue changes and reschedules show up by themselves.
Built for clubs that already self-host Nextcloud and want a clean, shareable calendar per team (juniors, men, women). But it works against any CalDAV server — not just Nextcloud.
- One calendar per team, e.g.
Handball - U15orHandball - Herren 2. Liga - Stable iCalendar UIDs → score updates and venue changes are picked up on the next sync, no duplicate events
- Identifies teams by
teamId, so clubs with multiple teams sharing the same display name (very common in Spielgemeinschaften) still get separate calendars - Home / away tag in the event title (
vs TV Muttenz (Heim)), final score appended once a game is played (vs TV Muttenz 25:28 (Heim)) - Venue, address, league, game number and HT/FT score in the event description
- Idempotent — safe to run as often as you want
- Runs as a tiny (~80 MB) Alpine container with built-in cron
- Survives the off-season as a no-op (no games → nothing to do)
- Compatible with Nextcloud, Radicale, Baïkal, SOGo, ownCloud, Apple iCloud and any other CalDAV-compliant server
handball.ch (REST) ──► shv-sync (Python) ──► CalDAV server
│
▼
phones / SpielerPlus / Outlook
The Swiss Handball Federation (SHV) exposes a REST API at
clubapi.handball.ch.
Each club gets HTTP Basic credentials via the Vereins Admin Tool (VAT).
This script pulls all teams and games of a single club, groups games by team,
and writes one CalDAV calendar per team.
-
A handball.ch club with the Dataservice enabled. Credentials are issued by the SHV via VAT (
vat.handball.ch) — ask whoever manages your club's admin account. -
A CalDAV server you can reach. Nextcloud is the default assumption, but any CalDAV server works (see CalDAV compatibility).
-
A dedicated service user on your CalDAV server, e.g.
handball-sync. For Nextcloud, create it via the web UI orocc:docker exec -e OC_PASS='<strong-password>' -u www-data nextcloud \ php occ user:add --password-from-env --display-name='Handball Sync' handball-sync
-
Docker + Docker Compose.
git clone https://github.com/choli41/shv-nextcloud-sync.git
cd shv-nextcloud-sync
cp .env.example .env
$EDITOR .env # fill in CLUB_ID, HANDBALL_API_KEY, CALDAV_*
# 1. discover what teams the API returns for your club
docker compose run --rm shv-sync python sync.py --discover
# 2. dry-run — fetch + group, but don't touch CalDAV
docker compose run --rm shv-sync python sync.py --dry-run
# 3. real run, then start the daemon
docker compose run --rm shv-sync python sync.py
docker compose up -d shv-syncThe container runs an initial sync on startup and then schedules itself
according to SYNC_CRON (default: every Wednesday 06:00, 0 6 * * 3).
-
Log in to VAT with your club admin account.
-
Open the Dataservice settings — there you can generate / view the password tied to your club ID.
-
Build the API key locally:
echo -n "140285:yourpassword" | base64 # → MTQwMjg1OnlvdXJwYXNzd29yZA==
That base64 string is your
HANDBALL_API_KEY.
There is also a test environment with separate data:
- prod:
https://clubapi.handball.ch - test:
https://clubapi-test.handball.ch - swagger:
https://clubapi-test.handball.ch/swagger/index.html
| Variable | Required | Default | Description |
|---|---|---|---|
CLUB_ID |
yes | — | Numeric club ID from handball.ch URLs |
HANDBALL_API_KEY |
yes | — | Base64 of clubid:password from VAT Dataservice |
HANDBALL_API_BASE |
no | https://clubapi.handball.ch |
Use the test URL while developing |
CALDAV_URL |
yes | — | Full DAV root, e.g. https://nc.example.com/remote.php/dav |
CALDAV_USER |
yes | — | Dedicated service user |
CALDAV_PASSWORD |
yes | — | Service user password (or app password) |
CALENDAR_PREFIX |
no | Handball |
Prefix for calendar display names |
GAME_DURATION_HOURS |
no | 2 |
Default duration of a single game |
EVENT_DOMAIN |
no | shv-nextcloud-sync.local |
Used inside iCalendar UIDs; pick a domain you control |
SYNC_CRON |
no | 0 6 * * 3 |
5-field cron expression for the recurring sync |
TZ |
no | Europe/Zurich |
Timezone inside the container |
The script uses the generic caldav
library and works with any CalDAV-compliant server. Common URL formats:
| Server | CALDAV_URL example |
|---|---|
| Nextcloud (same docker network) | http://nextcloud/remote.php/dav |
| Nextcloud (external) | https://nc.example.com/remote.php/dav |
| Radicale | http://radicale:5232/handball-sync/ |
| Baïkal | https://baikal.example.com/dav.php/ |
| SOGo | https://sogo.example.com/SOGo/dav/handball-sync/ |
| ownCloud | https://oc.example.com/remote.php/dav |
| Apple iCloud | https://caldav.icloud.com/ |
Radicale is the lightest-weight option if you don't want to install Nextcloud. Full setup:
# docker-compose.yml — adds a Radicale server next to shv-sync
services:
radicale:
image: tomsquest/docker-radicale
container_name: radicale
restart: unless-stopped
volumes:
- ./radicale-data:/data
ports:
- "5232:5232"
shv-sync:
# ... as in the standard docker-compose.yml ...
environment:
- CALDAV_URL=http://radicale:5232/handball-sync/
- CALDAV_USER=handball-sync
- CALDAV_PASSWORD=changeme
depends_on:
- radicaleCreate the Radicale user (htpasswd is the default auth backend):
docker exec radicale htpasswd -B -c /data/users handball-syncBaïkal is a small PHP CalDAV server, perfect for minimal setups. After running the web installer, create a user, then:
CALDAV_URL=https://baikal.example.com/dav.php/
CALDAV_USER=handball-sync
CALDAV_PASSWORD=changemeiCloud requires an app-specific password (not your Apple ID password): appleid.apple.com → Sign-In and Security → App-Specific Passwords. iCloud auto-discovers the right path:
CALDAV_URL=https://caldav.icloud.com/
CALDAV_USER=your.apple.id@icloud.com
CALDAV_PASSWORD=app-specific-passwordThe script tries to give calendars clean display names based on the team's
leagueShort field returned by the API:
leagueShort |
Calendar name |
|---|---|
M2, M3, M4, … |
Handball - Herren 2. Liga, … |
F2, F3, F4, … |
Handball - Damen 2. Liga, … |
MU13P S1, MU15P S2, MU17P S1, … |
Handball - U13, Handball - U15, Handball - U17, … |
| anything else | Handball - <leagueLong> |
Adjust make_calendar_name() in sync.py if you want different conventions.
Once the sync has populated the calendars, log into Nextcloud (or your CalDAV server) as the service user and use the standard sharing options:
- Share read-only with other Nextcloud users — they'll see the calendars in their Calendar app.
- Public link / iCal export URL — useful for SpielerPlus, Outlook, Apple Calendar etc. Note that SpielerPlus' "Import via URL" feature treats the import as a one-time copy, not as a live subscription.
Access through untrusted domain when the script connects to Nextcloud
in a shared Docker network with CALDAV_URL=http://nextcloud/remote.php/dav:
the bare hostname nextcloud isn't in your Nextcloud trusted_domains.
Add it once:
docker exec -u www-data nextcloud php occ config:system:set \
trusted_domains 2 --value=nextcloud401 Unauthorized from handball.ch: your HANDBALL_API_KEY is wrong or
the Dataservice for your club is disabled. Re-check VAT.
Same team appearing multiple times in --discover: the API returns one
row per (team, group) combination. The script already deduplicates by
teamId, so the actual sync is fine.
No teams returned for your club: confirm CLUB_ID matches the number
in the official URL https://www.handball.ch/de/matchcenter/vereine/<id>.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
export $(grep -v '^#' .env | xargs)
python sync.py --discoverThe container is intentionally tiny (Alpine + three Python deps) and the whole script is a single file — read it, fork it, adapt it.
MIT — see LICENSE.
This project is not affiliated with, endorsed by, or sponsored by the Swiss Handball Federation (SHV / Schweizerischer Handballverband). It only consumes the publicly documented Open ClubApi.