Skip to content

choli41/shv-nextcloud-sync

Repository files navigation

shv-nextcloud-sync

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


Deutsch

Was es macht

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.

Features

  • Ein Kalender pro Team, z.B. Handball - U15 oder Handball - 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

Architektur

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.

Voraussetzungen

  1. 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.

  2. Ein CalDAV-Server, den du erreichen kannst. Nextcloud ist die Standardannahme, aber jeder CalDAV-Server geht (siehe CalDAV-Kompatibilität).

  3. Ein dedizierter Service-User auf deinem CalDAV-Server, z.B. handball-sync. Bei Nextcloud anlegen via Web UI oder occ:

    docker exec -e OC_PASS='<starkes-passwort>' -u www-data nextcloud \
      php occ user:add --password-from-env --display-name='Handball Sync' handball-sync
  4. Docker + Docker Compose.

Schnellstart

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-sync

Der Container macht beim Start einen initialen Sync und plant sich danach selbst gemäss SYNC_CRON (Default: jeden Mittwoch 06:00, 0 6 * * 3).

Den HANDBALL_API_KEY bekommen

  1. Mit dem Vereins-Admin-Account in VAT einloggen.

  2. Dataservice öffnen — dort kannst du das Passwort generieren bzw. ansehen.

  3. 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

Konfiguration

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

CalDAV-Kompatibilität

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/

Beispiel: Radicale (minimaler CalDAV-Server)

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:
      - radicale

User in Radicale anlegen (htpasswd ist Default-Auth):

docker exec radicale htpasswd -B -c /data/users handball-sync

Beispiel: Baïkal

Baï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=changeme

Beispiel: Apple iCloud

iCloud verlangt ein App-spezifisches Passwort (nicht dein Apple-ID-Passwort): appleid.apple.comAnmeldung und SicherheitApp-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-passwort

Kalender-Benennung

Das 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.

Kalender teilen

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.

Troubleshooting

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=nextcloud

401 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.

Lizenz

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.


English

What it does

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.

Features

  • One calendar per team, e.g. Handball - U15 or Handball - 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

How it works

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.

Prerequisites

  1. 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.

  2. A CalDAV server you can reach. Nextcloud is the default assumption, but any CalDAV server works (see CalDAV compatibility).

  3. A dedicated service user on your CalDAV server, e.g. handball-sync. For Nextcloud, create it via the web UI or occ:

    docker exec -e OC_PASS='<strong-password>' -u www-data nextcloud \
      php occ user:add --password-from-env --display-name='Handball Sync' handball-sync
  4. Docker + Docker Compose.

Quick start

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-sync

The container runs an initial sync on startup and then schedules itself according to SYNC_CRON (default: every Wednesday 06:00, 0 6 * * 3).

Getting your HANDBALL_API_KEY

  1. Log in to VAT with your club admin account.

  2. Open the Dataservice settings — there you can generate / view the password tied to your club ID.

  3. 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

Configuration

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

CalDAV compatibility

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/

Example: Radicale (minimal CalDAV server)

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:
      - radicale

Create the Radicale user (htpasswd is the default auth backend):

docker exec radicale htpasswd -B -c /data/users handball-sync

Example: Baïkal

Baï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=changeme

Example: Apple iCloud

iCloud requires an app-specific password (not your Apple ID password): appleid.apple.comSign-In and SecurityApp-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-password

Calendar naming

The 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.

Sharing the calendars

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.

Troubleshooting

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=nextcloud

401 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>.

Development

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 --discover

The container is intentionally tiny (Alpine + three Python deps) and the whole script is a single file — read it, fork it, adapt it.

License

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.

About

Sync your Swiss handball club's match schedule from handball.ch into per-team Nextcloud calendars via CalDAV

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors