This commit is contained in:
2025-08-26 19:33:02 -07:00
commit 79b95a9f1f
53 changed files with 47463 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
package api
import (
"context"
"fmt"
"time"
)
// Activity represents a Garmin Connect activity
type Activity struct {
ActivityID int64 `json:"activityId"`
Name string `json:"activityName"`
Type string `json:"activityType"`
StartTime time.Time `json:"startTimeLocal"`
Duration float64 `json:"duration"`
Distance float64 `json:"distance"`
}
// ActivitiesResponse represents the response from the activities endpoint
type ActivitiesResponse struct {
Activities []Activity `json:"activities"`
Pagination Pagination `json:"pagination"`
}
// Pagination represents pagination information in API responses
type Pagination struct {
PageSize int `json:"pageSize"`
TotalCount int `json:"totalCount"`
Page int `json:"page"`
}
// GetActivities retrieves a list of activities with pagination
func (c *Client) GetActivities(ctx context.Context, page int, pageSize int) ([]Activity, *Pagination, error) {
path := "/activitylist-service/activities/search"
query := fmt.Sprintf("?page=%d&pageSize=%d", page, pageSize)
var response ActivitiesResponse
err := c.Get(ctx, path+query, &response)
if err != nil {
return nil, nil, fmt.Errorf("failed to get activities: %w", err)
}
// Validate we received some activities
if len(response.Activities) == 0 {
return nil, nil, fmt.Errorf("no activities found")
}
return response.Activities, &response.Pagination, nil
}

121
internal/api/client.go Normal file
View File

@@ -0,0 +1,121 @@
package api
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
"golang.org/x/time/rate"
)
// Client handles communication with the Garmin Connect API
type Client struct {
baseURL *url.URL
httpClient *http.Client
limiter *rate.Limiter
logger Logger
}
// NewClient creates a new API client
func NewClient(baseURL string, httpClient *http.Client) (*Client, error) {
u, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("invalid base URL: %w", err)
}
if httpClient == nil {
httpClient = http.DefaultClient
}
return &Client{
baseURL: u,
httpClient: httpClient,
limiter: rate.NewLimiter(rate.Every(time.Second/10), 10), // 10 requests per second
logger: &stdLogger{},
}, nil
}
// SetLogger sets the client's logger
func (c *Client) SetLogger(logger Logger) {
c.logger = logger
}
// SetRateLimit configures the rate limiter
func (c *Client) SetRateLimit(interval time.Duration, burst int) {
c.limiter = rate.NewLimiter(rate.Every(interval), burst)
}
// Get performs a GET request
func (c *Client) Get(ctx context.Context, path string, v interface{}) error {
return c.doRequest(ctx, http.MethodGet, path, nil, v)
}
// Post performs a POST request
func (c *Client) Post(ctx context.Context, path string, body io.Reader, v interface{}) error {
return c.doRequest(ctx, http.MethodPost, path, body, v)
}
func (c *Client) doRequest(ctx context.Context, method, path string, body io.Reader, v interface{}) error {
// Wait for rate limiter
if err := c.limiter.Wait(ctx); err != nil {
return fmt.Errorf("rate limit wait failed: %w", err)
}
// Create request
u := c.baseURL.ResolveReference(&url.URL{Path: path})
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
if err != nil {
return fmt.Errorf("create request failed: %w", err)
}
// Set headers
req.Header.Set("Accept", "application/json")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
c.logger.Debugf("Request: %s %s", method, u.String())
// Execute request
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
c.logger.Debugf("Response status: %s", resp.Status)
// Handle non-200 responses
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
// Parse response
if v == nil {
return nil
}
if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
return fmt.Errorf("decode response failed: %w", err)
}
return nil
}
// Logger defines the logging interface for the client
type Logger interface {
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Errorf(format string, args ...interface{})
}
// stdLogger is the default logger that uses the standard log package
type stdLogger struct{}
func (l *stdLogger) Debugf(format string, args ...interface{}) {}
func (l *stdLogger) Infof(format string, args ...interface{}) {}
func (l *stdLogger) Errorf(format string, args ...interface{}) {}

38
internal/api/user.go Normal file
View File

@@ -0,0 +1,38 @@
package api
import (
"context"
"fmt"
)
// UserProfile represents a Garmin Connect user profile
type UserProfile struct {
DisplayName string `json:"displayName"`
FullName string `json:"fullName"`
EmailAddress string `json:"emailAddress"`
Username string `json:"username"`
ProfileID string `json:"profileId"`
ProfileImage string `json:"profileImageUrlLarge"`
Location string `json:"location"`
FitnessLevel string `json:"fitnessLevel"`
Height float64 `json:"height"`
Weight float64 `json:"weight"`
Birthdate string `json:"birthDate"`
}
// GetUserProfile retrieves the user's profile information
func (c *Client) GetUserProfile(ctx context.Context) (*UserProfile, error) {
var profile UserProfile
path := "/userprofile-service/socialProfile"
if err := c.Get(ctx, path, &profile); err != nil {
return nil, fmt.Errorf("failed to get user profile: %w", err)
}
// Handle empty profile response
if profile.ProfileID == "" {
return nil, fmt.Errorf("user profile not found")
}
return &profile, nil
}