docs: add technical and high-level documentation

This commit is contained in:
2026-11-09 15:05:02 +01:00
parent fb49e1b553
commit 4c3bee89ed
2 changed files with 140 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# connect2gpx — Présentation
## En une phrase
`connect2gpx` transforme un lien de **trail Garmin Connect** en fichier **GPX**
que l'on peut ouvrir dans n'importe quelle application de randonnée ou de
navigation (Garmin, Komoot, Strava, OsmAnd, etc.).
## Le problème
Garmin Connect propose une section « Trails » avec des parcours curatés affichés
sur une carte. Ces parcours sont consultables en ligne, mais il n'est pas
toujours simple d'en récupérer la trace GPS au format GPX pour la charger sur
un appareil ou la partager.
## Ce que fait l'outil
1. **Vous lui donnez l'URL** d'une page Garmin Trails (par exemple une boucle
dans le Hainaut).
2. **Il ouvre automatiquement un navigateur invisible** (Chromium) pour
contourner la protection anti-robots de Garmin.
3. **Il récupère la trace complète** du parcours, point par point, avec
l'altitude.
4. **Il écrit un fichier `.gpx`** standard, prêt à être utilisé.
## Résultat
Un fichier GPX contenant :
- le nom et la description du trail ;
- la trace complète (latitude, longitude, altitude) ;
- typiquement plusieurs centaines à milliers de points.
Pour l'exemple analysé (« Boucle de Poteaupré et Étang de Virelles ») :
854 points, ~20,7 km, dénivelé positif ~251 m.
## Utilisation
```bash
connect2gpx "https://connect.garmin.com/app/trails/..." -o sortie.gpx
```
## Prérequis
- Python 3.13 ou supérieur
- Chromium ou Google Chrome installé sur la machine
## Limites
- Fonctionne uniquement avec les **trails publics** de Garmin Connect (pas
avec vos activités personnelles, qui nécessitent une authentification).
- Dépend de la structure interne du site Garmin : si Garmin modifie son site,
l'outil peut nécessiter une mise à jour.
- Un parcours prend environ 10 à 20 secondes à récupérer.
+87
View File
@@ -0,0 +1,87 @@
# 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é.