mirror of
https://github.com/sstent/go-garth.git
synced 2026-01-25 08:35:06 +00:00
484 lines
13 KiB
Go
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
|
|
}
|