package client import ( "context" "fmt" "io" "net/http" "time" "github.com/bzimmer/activity" "github.com/bzimmer/activity/zwift" "golang.org/x/time/rate" ) // Client wraps the Zwift API client with rate limiting and retry logic type Client struct { client *zwift.Client rateLimiter *rate.Limiter maxRetries int athleteID int64 // Cached athlete ID } // NewClient creates a new Zwift client func NewClient(username, password string, rateLimit, maxRetries int) (*Client, error) { // Create HTTP client httpClient := &http.Client{ Timeout: 30 * time.Second, } // Create rate limiter (requests per second) limiter := rate.NewLimiter(rate.Limit(rateLimit), rateLimit*2) // Create Zwift client with credentials zwiftClient, err := zwift.NewClient( zwift.WithHTTPClient(httpClient), zwift.WithRateLimiter(limiter), zwift.WithTokenRefresh(username, password), ) if err != nil { return nil, fmt.Errorf("failed to create Zwift client: %w", err) } return &Client{ client: zwiftClient, rateLimiter: limiter, maxRetries: maxRetries, }, nil } // GetProfile gets the authenticated user's profile and caches the athlete ID func (c *Client) GetProfile(ctx context.Context) (int64, error) { // Return cached athlete ID if available if c.athleteID != 0 { return c.athleteID, nil } // Get profile from Zwift (using "me" as the profile ID) profile, err := c.client.Profile.Profile(ctx, zwift.Me) if err != nil { return 0, fmt.Errorf("failed to get profile: %w", err) } // Cache athlete ID c.athleteID = profile.ID return profile.ID, nil } // ListRecentActivities lists recent activities for the athlete func (c *Client) ListRecentActivities(ctx context.Context, athleteID int64, limit int) ([]*zwift.Activity, error) { // Create pagination spec spec := activity.Pagination{ Total: limit, } // List activities from Zwift activities, err := c.client.Activity.Activities(ctx, athleteID, spec) if err != nil { return nil, fmt.Errorf("failed to list activities: %w", err) } return activities, nil } // DownloadFIT downloads the FIT file for an activity func (c *Client) DownloadFIT(ctx context.Context, athleteID, activityID int64) ([]byte, string, error) { // Retry logic var lastErr error for attempt := 0; attempt < c.maxRetries; attempt++ { if attempt > 0 { // Exponential backoff backoff := time.Duration(1<