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
-
cli.mainparse les arguments (url,-o/--output), appellefetch_trail, puisparse_trail, puiswrite_gpx. -
fetcher.fetch_trail(url):- localise le binaire Chromium (
shutil.whichsur 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 lewebSocketDebuggerUrlde l'ongletpage; - ouvre une connexion WebSocket (bibliothèque
websockets) et envoie les commandes CDP :Network.enablePage.enablepuisPage.navigatevers l'URL cible ;
- écoute les événements
Network.responseReceived/Network.loadingFinished; - filtre les réponses dont l'URL contient
course-service/curatedoucurated-course/course/curated; - appelle
Network.getResponseBodypour récupérer le corps JSON ; - sélectionne la réponse dont le corps contient
"geoPoints"(trace détaillée) ; - parse le corps en
dictet le retourne ; - termine le processus Chromium dans un bloc
finally.
- localise le binaire Chromium (
-
parser.parse_trail(payload):- extrait
courseName,description,distanceMeter,elevationGainMeter,elevationLossMeter; - convertit
geoPoints(liste de{latitude, longitude, elevation, distance}) en liste deTrailPoint(dataclass) ; - retourne un objet
Trail.
- extrait
-
gpx_writer.write_gpx(trail, path):- construit un arbre XML avec
xml.etree.ElementTree; - racine
<gpx version="1.1" ...>avec namespacehttp://www.topografix.com/GPX/1/1; <metadata>avec<name>et<desc>;<trk>/<trkseg>/<trkpt lat=... lon=...>avec<ele>;- indentation via
ET.indentet écriture UTF-8 avec déclaration XML.
- construit un arbre XML avec
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
pageest attendu ; si Chromium ouvre plusieurs onglets, le premier est utilisé.