Files
go-garth/workouts.go
2025-09-03 08:29:15 -07:00

484 lines
13 KiB
Go

package garth
import (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"time"
)
// WorkoutService provides methods for interacting with Garmin workout data.
type WorkoutService struct {
client *APIClient
}
// NewWorkoutService creates a new WorkoutService instance.
// client: The authenticated APIClient used to make requests.
func NewWorkoutService(client *APIClient) *WorkoutService {
return &WorkoutService{client: client}
}
// Workout represents a Garmin workout with basic information
type Workout struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
SportType string `json:"sportType"`
SubSportType string `json:"subSportType"`
CreatedDate time.Time `json:"createdDate"`
UpdatedDate time.Time `json:"updatedDate"`
OwnerID int64 `json:"ownerId"`
WorkoutSegments []WorkoutSegment `json:"workoutSegments,omitempty"`
}
// WorkoutDetails contains detailed information about a workout
type WorkoutDetails struct {
Workout
EstimatedDuration int64 `json:"estimatedDuration"`
EstimatedDistance float64 `json:"estimatedDistance"`
TrainingStressScore float64 `json:"trainingStressScore"`
IntensityFactor float64 `json:"intensityFactor"`
WorkoutProvider string `json:"workoutProvider"`
WorkoutSource string `json:"workoutSource"`
WorkoutMetrics json.RawMessage `json:"workoutMetrics"`
WorkoutGoals json.RawMessage `json:"workoutGoals"`
WorkoutTags []string `json:"workoutTags"`
WorkoutSegments []WorkoutSegment `json:"workoutSegments"`
}
// WorkoutSegment represents a segment within a workout
type WorkoutSegment struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Order int `json:"order"`
Duration int64 `json:"duration"`
Distance float64 `json:"distance"`
Exercises []WorkoutExercise `json:"exercises"`
}
// WorkoutExercise represents an exercise within a workout segment
type WorkoutExercise struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Order int `json:"order"`
Duration int64 `json:"duration"`
Distance float64 `json:"distance"`
Repetitions int `json:"repetitions"`
Weight float64 `json:"weight"`
Intensity string `json:"intensity"`
ExerciseMetrics json.RawMessage `json:"exerciseMetrics"`
}
// WorkoutListOptions provides filtering options for listing workouts
type WorkoutListOptions struct {
Limit int
StartDate time.Time
EndDate time.Time
SportType string
NameContains string
OwnerID int64
Offset int
SortBy string
SortOrder string
Type string
Status string
}
// WorkoutUpdate represents fields that can be updated on a workout
type WorkoutUpdate struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Type string `json:"type,omitempty"`
SportType string `json:"sportType,omitempty"`
SubSportType string `json:"subSportType,omitempty"`
}
// List retrieves a list of workouts for the current user with optional filters
func (s *WorkoutService) List(ctx context.Context, opts WorkoutListOptions) ([]Workout, error) {
params := url.Values{}
if opts.Limit > 0 {
params.Set("limit", strconv.Itoa(opts.Limit))
}
if opts.Offset > 0 {
params.Set("offset", strconv.Itoa(opts.Offset))
}
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.SportType != "" {
params.Set("sportType", opts.SportType)
}
if opts.Type != "" {
params.Set("type", opts.Type)
}
if opts.Status != "" {
params.Set("status", opts.Status)
}
if opts.NameContains != "" {
params.Set("nameContains", opts.NameContains)
}
if opts.OwnerID > 0 {
params.Set("ownerId", strconv.FormatInt(opts.OwnerID, 10))
}
if opts.SortBy != "" {
params.Set("sortBy", opts.SortBy)
}
if opts.SortOrder != "" {
params.Set("sortOrder", opts.SortOrder)
}
path := "/workout-service/workouts"
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 workouts list",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read workouts response",
Cause: err,
}
}
var workouts []Workout
if err := json.Unmarshal(body, &workouts); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse workouts data",
Cause: err,
}
}
return workouts, nil
}
// Get retrieves detailed information about a specific workout
func (s *WorkoutService) Get(ctx context.Context, id string) (*WorkoutDetails, error) {
path := "/workout-service/workout/" + id
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 workout details",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read workout response",
Cause: err,
}
}
var details WorkoutDetails
if err := json.Unmarshal(body, &details); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse workout data",
Cause: err,
}
}
return &details, nil
}
// Create creates a new workout
func (s *WorkoutService) Create(ctx context.Context, workout Workout) (*Workout, error) {
jsonBody, err := json.Marshal(workout)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to marshal workout",
Cause: err,
}
}
resp, err := s.client.Post(ctx, "/workout-service/workout", 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 workout",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read workout response",
Cause: err,
}
}
var createdWorkout Workout
if err := json.Unmarshal(body, &createdWorkout); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse workout data",
Cause: err,
}
}
return &createdWorkout, nil
}
// Update updates an existing workout
func (s *WorkoutService) Update(ctx context.Context, id string, update WorkoutUpdate) (*Workout, error) {
jsonBody, err := json.Marshal(update)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to marshal workout update",
Cause: err,
}
}
path := "/workout-service/workout/" + id
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 workout",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read workout response",
Cause: err,
}
}
var updatedWorkout Workout
if err := json.Unmarshal(body, &updatedWorkout); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse workout data",
Cause: err,
}
}
return &updatedWorkout, nil
}
// Delete deletes an existing workout
func (s *WorkoutService) Delete(ctx context.Context, id string) error {
path := "/workout-service/workout/" + id
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 workout",
}
}
return nil
}
// GetWorkoutTemplates retrieves all workout templates for the current user
func (s *WorkoutService) GetWorkoutTemplates(ctx context.Context) ([]Workout, error) {
path := "/workout-service/templates"
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 workout templates",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read workout templates response",
Cause: err,
}
}
var templates []Workout
if err := json.Unmarshal(body, &templates); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse workout templates data",
Cause: err,
}
}
return templates, nil
}
// SearchWorkouts searches workouts by name or description
func (s *WorkoutService) SearchWorkouts(ctx context.Context, query string, limit int) ([]Workout, error) {
params := url.Values{}
params.Set("q", query)
if limit > 0 {
params.Set("limit", strconv.Itoa(limit))
}
path := "/workout-service/search?" + 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 search workouts",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read search response",
Cause: err,
}
}
var workouts []Workout
if err := json.Unmarshal(body, &workouts); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse search results",
Cause: err,
}
}
return workouts, nil
}
// CopyWorkout creates a copy of an existing workout
func (s *WorkoutService) CopyWorkout(ctx context.Context, id string, newName string) (*Workout, error) {
path := "/workout-service/workout/" + id + "/copy"
requestBody := map[string]string{"name": newName}
jsonBody, err := json.Marshal(requestBody)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to marshal request body",
Cause: err,
}
}
resp, err := s.client.Post(ctx, path, 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 copy workout",
}
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to read copy response",
Cause: err,
}
}
var copiedWorkout Workout
if err := json.Unmarshal(body, &copiedWorkout); err != nil {
return nil, &APIError{
StatusCode: http.StatusInternalServerError,
Message: "Failed to parse copied workout data",
Cause: err,
}
}
return &copiedWorkout, nil
}
// Export exports a workout in the specified format (fit, tcx, json)
func (s *WorkoutService) Export(ctx context.Context, id string, format string) (io.ReadCloser, error) {
if format != "fit" && format != "tcx" && format != "json" {
return nil, &APIError{
StatusCode: http.StatusBadRequest,
Message: "Invalid format. Supported formats: fit, tcx, json",
}
}
path := "/workout-service/workout/" + id + "/export/" + format
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 workout",
}
}
return resp.Body, nil
}