Files
go-garth/activities.go
2025-09-02 06:52:24 -07:00

307 lines
8.3 KiB
Go

package garth
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"time"
)
// Activity represents a summary of a Garmin activity
type Activity struct {
ActivityID int64 `json:"activityId"`
Name string `json:"activityName"`
Type string `json:"activityType"`
StartTime time.Time `json:"startTime"`
Distance float64 `json:"distance"`
Duration float64 `json:"duration"`
Calories int `json:"calories"`
}
// ActivityDetails contains detailed information about an activity
type ActivityDetails struct {
ActivityID int64 `json:"activityId"`
Name string `json:"activityName"`
Description string `json:"description"`
Type string `json:"activityType"`
StartTime time.Time `json:"startTime"`
Distance float64 `json:"distance"`
Duration float64 `json:"duration"`
Calories int `json:"calories"`
ElevationGain float64 `json:"elevationGain"`
ElevationLoss float64 `json:"elevationLoss"`
MaxHeartRate int `json:"maxHeartRate"`
AvgHeartRate int `json:"avgHeartRate"`
MaxSpeed float64 `json:"maxSpeed"`
AvgSpeed float64 `json:"avgSpeed"`
Steps int `json:"steps"`
Stress int `json:"stress"`
TotalSteps int `json:"totalSteps"`
Device json.RawMessage `json:"device"`
Location json.RawMessage `json:"location"`
Weather json.RawMessage `json:"weather"`
HeartRateZones json.RawMessage `json:"heartRateZones"`
TrainingEffect json.RawMessage `json:"trainingEffect"`
ActivityMetrics json.RawMessage `json:"activityMetrics"`
}
// ActivityListOptions provides filtering options for listing activities
type ActivityListOptions struct {
Limit int
StartDate time.Time
EndDate time.Time
ActivityType string
NameContains string
}
// ActivityUpdate represents fields that can be updated on an activity
type ActivityUpdate struct {
Name string `json:"activityName,omitempty"`
Description string `json:"description,omitempty"`
Type string `json:"activityType,omitempty"`
StartTime time.Time `json:"startTime,omitempty"`
Distance float64 `json:"distance,omitempty"`
Duration float64 `json:"duration,omitempty"`
}
// ActivityService provides access to activity operations
type ActivityService struct {
client *APIClient
}
// NewActivityService creates a new ActivityService instance
func NewActivityService(client *APIClient) *ActivityService {
return &ActivityService{client: client}
}
// List retrieves a list of activities for the current user with optional filters
func (s *ActivityService) List(ctx context.Context, opts ActivityListOptions) ([]Activity, error) {
params := url.Values{}
if opts.Limit > 0 {
params.Set("limit", strconv.Itoa(opts.Limit))
}
if !opts.StartDate.IsZero() {
params.Set("startDate", opts.StartDate.Format(time.RFC3339))
}
if !opts.EndDate.IsZero() {
params.Set("endDate", opts.EndDate.Format(time.RFC3339))
}
if opts.ActivityType != "" {
params.Set("activityType", opts.ActivityType)
}
if opts.NameContains != "" {
params.Set("nameContains", opts.NameContains)
}
path := "/activitylist-service/activities/search/activities"
if len(params) > 0 {
path += "?" + params.Encode()
}
resp, err := s.client.Get(ctx, path)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &APIError{
StatusCode: resp.StatusCode,
Message: "Failed to get activities list",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read activities response",
Cause: err,
}
}
var activities []Activity
if err := json.Unmarshal(body, &activities); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse activities data",
Cause: err,
}
}
return activities, nil
}
// Create creates a new activity
func (s *ActivityService) Create(ctx context.Context, activity Activity) (*Activity, error) {
jsonBody, err := json.Marshal(activity)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to marshal activity",
Cause: err,
}
}
resp, err := s.client.Post(ctx, "/activity-service/activity", bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return nil, &APIError{
StatusCode: resp.StatusCode,
Message: "Failed to create activity",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read activity response",
Cause: err,
}
}
var createdActivity Activity
if err := json.Unmarshal(body, &createdActivity); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse activity data",
Cause: err,
}
}
return &createdActivity, nil
}
// Update updates an existing activity
func (s *ActivityService) Update(ctx context.Context, activityID int64, update ActivityUpdate) (*Activity, error) {
jsonBody, err := json.Marshal(update)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to marshal activity update",
Cause: err,
}
}
path := "/activity-service/activity/" + strconv.FormatInt(activityID, 10)
resp, err := s.client.Put(ctx, path, bytes.NewReader(jsonBody))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &APIError{
StatusCode: resp.StatusCode,
Message: "Failed to update activity",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read activity response",
Cause: err,
}
}
var updatedActivity Activity
if err := json.Unmarshal(body, &updatedActivity); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse activity data",
Cause: err,
}
}
return &updatedActivity, nil
}
// Delete deletes an existing activity
func (s *ActivityService) Delete(ctx context.Context, activityID int64) error {
path := "/activity-service/activity/" + strconv.FormatInt(activityID, 10)
resp, err := s.client.Delete(ctx, path, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
return &APIError{
StatusCode: resp.StatusCode,
Message: "Failed to delete activity",
}
}
return nil
}
// Get retrieves detailed information about a specific activity
func (s *ActivityService) Get(ctx context.Context, activityID int64) (*ActivityDetails, error) {
path := "/activity-service/activity/" + strconv.FormatInt(activityID, 10)
resp, err := s.client.Get(ctx, path)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &APIError{
StatusCode: resp.StatusCode,
Message: "Failed to get activity details",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read activity response",
Cause: err,
}
}
var details ActivityDetails
if err := json.Unmarshal(body, &details); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse activity data",
Cause: err,
}
}
return &details, nil
}
// Export exports an activity in the specified format (gpx, tcx, original)
func (s *ActivityService) Export(ctx context.Context, activityID int64, format string) (io.ReadCloser, error) {
path := "/download-service/export/" + format + "/activity/" + strconv.FormatInt(activityID, 10)
resp, err := s.client.Get(ctx, path)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
defer resp.Body.Close()
return nil, &APIError{
StatusCode: resp.StatusCode,
Message: "Failed to export activity",
}
}
return resp.Body, nil
}