diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-03 21:53:04 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-03 21:53:04 +0200 |
| commit | 69f317d8cb51b911732712a9fcc8ac49563326c6 (patch) | |
| tree | f8afcada62a43326c7d3fda34e4cd4627b7b0d1c /strava/sync.go | |
| parent | 9707f9ad09db935af7d06b5de08bc52de13ba6a7 (diff) | |
feat: Sync Strava rides as GPX files
Diffstat (limited to 'strava/sync.go')
| -rw-r--r-- | strava/sync.go | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/strava/sync.go b/strava/sync.go new file mode 100644 index 0000000..f104585 --- /dev/null +++ b/strava/sync.go @@ -0,0 +1,110 @@ +package strava + +import ( + "fmt" + "net/url" + "os" + "strconv" + "time" + + "github.com/martinlehoux/kagamigo/kcore" + "github.com/tkrajina/gpxgo/gpx" +) + +type Activity struct { + ID int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + SportType string `json:"sport_type"` + StartDate time.Time `json:"start_date"` +} + +func (c *Client) ListActivities(after time.Time, types ...string) ([]Activity, error) { + allowed := map[string]bool{} + for _, t := range types { + allowed[t] = true + } + var all []Activity + for page := 1; ; page++ { + q := url.Values{} + q.Set("per_page", "200") + q.Set("page", strconv.Itoa(page)) + q.Set("after", strconv.FormatInt(after.Unix(), 10)) + var batch []Activity + err := c.GetJSON("/athlete/activities?"+q.Encode(), &batch) + if err != nil { + return nil, kcore.Wrap(err, "failed to list Strava activities") + } + if len(batch) == 0 { + break + } + for _, activity := range batch { + if len(allowed) == 0 || allowed[activity.Type] || allowed[activity.SportType] { + all = append(all, activity) + } + } + if len(batch) < 200 { + break + } + } + return all, nil +} + +func (c *Client) ActivityStreams(id int64) (latlng [][2]float64, altitude []float64, seconds []float64, err error) { + path := fmt.Sprintf("/activities/%d/streams?keys=latlng,altitude,time&key_by_type=true", id) + var streams struct { + LatLng *struct { + Data [][2]float64 `json:"data"` + } `json:"latlng"` + Altitude *struct { + Data []float64 `json:"data"` + } `json:"altitude"` + Time *struct { + Data []float64 `json:"data"` + } `json:"time"` + } + err = c.GetJSON(path, &streams) + if err != nil { + return nil, nil, nil, kcore.Wrap(err, "failed to fetch Strava streams") + } + if streams.LatLng != nil { + latlng = streams.LatLng.Data + } + if streams.Altitude != nil { + altitude = streams.Altitude.Data + } + if streams.Time != nil { + seconds = streams.Time.Data + } + return latlng, altitude, seconds, nil +} + +func WriteActivityGPX(file string, activity Activity, latlng [][2]float64, altitude, seconds []float64) error { + points := make([]gpx.GPXPoint, len(latlng)) + for i := range latlng { + point := gpx.GPXPoint{} + point.Latitude = latlng[i][0] + point.Longitude = latlng[i][1] + if i < len(altitude) { + point.Elevation = *gpx.NewNullableFloat64(altitude[i]) + } + if i < len(seconds) { + point.Timestamp = activity.StartDate.Add(time.Duration(seconds[i]) * time.Second) + } + points[i] = point + } + doc := gpx.GPX{ + Version: "1.1", + Creator: "biking_home", + Name: activity.Name, + Tracks: []gpx.GPXTrack{{ + Name: activity.Name, + Segments: []gpx.GPXTrackSegment{{Points: points}}, + }}, + } + xmlData, err := doc.ToXml(gpx.ToXmlParams{}) + if err != nil { + return kcore.Wrap(err, "failed to build GPX XML") + } + return os.WriteFile(file, xmlData, 0o644) +} |