docs: add technical and high-level documentation
This commit is contained in:
@@ -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.
|
||||
@@ -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é.
|
||||
Reference in New Issue
Block a user