Files
connect2gpx/docs/technical.md
T

3.8 KiB

connect2gpx — Description technique

Vue d'ensemble

connect2gpx est un outil en ligne de commande (Python ≥ 3.13) qui convertit un lien Garmin Trails (page web d'un trail curaté) en fichier GPX 1.1. Le projet est organisé en quatre modules sous src/connect2gpx/.

Architecture

src/connect2gpx/
├── cli.py          # point d'entrée argparse
├── fetcher.py      # pilotage de Chromium headless + CDP
├── parser.py       # extraction des points depuis le JSON
└── gpx_writer.py   # sérialisation GPX

Flux d'exécution

  1. cli.main parse les arguments (url, -o/--output), appelle fetch_trail, puis parse_trail, puis write_gpx.

  2. fetcher.fetch_trail(url) :

    • localise le binaire Chromium (shutil.which sur plusieurs noms candidats) ;
    • lance chromium --headless=new --remote-debugging-port=9222 <url> ;
    • se connecte au point de débogage HTTP (http://localhost:9222/json) pour récupérer le webSocketDebuggerUrl de l'onglet page ;
    • ouvre une connexion WebSocket (bibliothèque websockets) et envoie les commandes CDP :
      • Network.enable
      • Page.enable puis Page.navigate vers l'URL cible ;
    • écoute les événements Network.responseReceived / Network.loadingFinished ;
    • filtre les réponses dont l'URL contient course-service/curated ou curated-course/course/curated ;
    • appelle Network.getResponseBody pour récupérer le corps JSON ;
    • sélectionne la réponse dont le corps contient "geoPoints" (trace détaillée) ;
    • parse le corps en dict et le retourne ;
    • termine le processus Chromium dans un bloc finally.
  3. parser.parse_trail(payload) :

    • extrait courseName, description, distanceMeter, elevationGainMeter, elevationLossMeter ;
    • convertit geoPoints (liste de {latitude, longitude, elevation, distance}) en liste de TrailPoint (dataclass) ;
    • retourne un objet Trail.
  4. gpx_writer.write_gpx(trail, path) :

    • construit un arbre XML avec xml.etree.ElementTree ;
    • racine <gpx version="1.1" ...> avec namespace http://www.topografix.com/GPX/1/1 ;
    • <metadata> avec <name> et <desc> ;
    • <trk> / <trkseg> / <trkpt lat=... lon=...> avec <ele> ;
    • indentation via ET.indent et écriture UTF-8 avec déclaration XML.

Pourquoi Chromium headless ?

Les endpoints Garmin (connect.garmin.com/gc-api/... et didt-api/...) sont protégés par Cloudflare : une requête HTTP directe (curl, requests, même avec impersonation TLS via curl_cffi) renvoie 403. Seul un navigateur réel (cookies + empreinte TLS + exécution JS) réussit à charger la page et déclencher les XHR. Le mode headless + CDP permet de capturer les réponses réseau sans interface graphique.

Endpoints exploités

Endpoint Donnée
didt-api/curated-course/course/curated/{pays}/{province}/{slug} Métadonnées + WKT LINESTRING (coordonnées grossières)
gc-api/course-service/curated/{connectCourseId} Trace détaillée : geoPoints avec lat/lon/ele/distance (854 points pour l'exemple)

Le fetcher sélectionne la réponse contenant "geoPoints" pour obtenir la trace la plus fidèle (avec altitude).

Dépendances

  • websockets >= 13.0 (client WebSocket pour CDP)
  • Binaire Chromium/Chrome présent sur le système (shutil.which)
  • Python ≥ 3.13 (type hints list[...], str | Path)

Points de fragilité

  • Les noms d'endpoints et la structure JSON peuvent changer côté Garmin.
  • Le timing (sleep 2 s au démarrage, timeout 60 s) est heuristique.
  • L'extraction repose sur la présence de la sous-chaîne "geoPoints" dans le corps de la réponse.
  • Un seul onglet page est attendu ; si Chromium ouvre plusieurs onglets, le premier est utilisé.