mirror of
https://github.com/sstent/garminsync-go.git
synced 2026-01-26 17:11:53 +00:00
partital fix
This commit is contained in:
@@ -1,156 +1,222 @@
|
||||
// internal/garmin/client.go
|
||||
package garmin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
retries int // Number of retries for failed requests
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
baseURL: "http://garmin-api:8081",
|
||||
retries: 3, // Default to 3 retries
|
||||
}
|
||||
}
|
||||
|
||||
type GarminActivity struct {
|
||||
ActivityID int `json:"activityId"`
|
||||
ActivityName string `json:"activityName"`
|
||||
StartTimeLocal string `json:"startTimeLocal"`
|
||||
ActivityType map[string]interface{} `json:"activityType"`
|
||||
Distance float64 `json:"distance"`
|
||||
Duration float64 `json:"duration"`
|
||||
MaxHR float64 `json:"maxHR"`
|
||||
AvgHR float64 `json:"avgHR"`
|
||||
AvgPower float64 `json:"avgPower"`
|
||||
Calories float64 `json:"calories"`
|
||||
StartLatitude float64 `json:"startLatitude"`
|
||||
StartLongitude float64 `json:"startLongitude"`
|
||||
Steps float64 `json:"steps"`
|
||||
ElevationGain float64 `json:"elevationGain"`
|
||||
ElevationLoss float64 `json:"elevationLoss"`
|
||||
AvgTemperature float64 `json:"avgTemperature"`
|
||||
MinTemperature float64 `json:"minTemperature"`
|
||||
MaxTemperature float64 `json:"maxTemperature"`
|
||||
ActivityID int `json:"activityId"`
|
||||
ActivityName string `json:"activityName"`
|
||||
StartTimeLocal string `json:"startTimeLocal"`
|
||||
ActivityType map[string]interface{} `json:"activityType"`
|
||||
Distance float64 `json:"distance"`
|
||||
Duration float64 `json:"duration"`
|
||||
MaxHR float64 `json:"maxHR"`
|
||||
AvgHR float64 `json:"avgHR"`
|
||||
AvgPower float64 `json:"avgPower"`
|
||||
Calories float64 `json:"calories"`
|
||||
StartLatitude float64 `json:"startLatitude"`
|
||||
StartLongitude float64 `json:"startLongitude"`
|
||||
Steps float64 `json:"steps"`
|
||||
ElevationGain float64 `json:"elevationGain"`
|
||||
ElevationLoss float64 `json:"elevationLoss"`
|
||||
AvgTemperature float64 `json:"avgTemperature"`
|
||||
MinTemperature float64 `json:"minTemperature"`
|
||||
MaxTemperature float64 `json:"maxTemperature"`
|
||||
}
|
||||
|
||||
// NewClient creates a new Garmin API client
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
baseURL: "http://garmin-api:8081",
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats retrieves user statistics for a specific date via the Python API service
|
||||
func (c *Client) GetStats(date string) (map[string]interface{}, error) {
|
||||
// Construct request URL
|
||||
url := fmt.Sprintf("%s/stats?date=%s", c.baseURL, url.QueryEscape(date))
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var stats map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
url := fmt.Sprintf("%s/stats?date=%s", c.baseURL, url.QueryEscape(date))
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var bodyBytes []byte
|
||||
reqErr := error(nil)
|
||||
|
||||
for i := 0; i <= c.retries; i++ {
|
||||
resp, reqErr = c.httpClient.Do(req)
|
||||
if reqErr != nil || (resp != nil && resp.StatusCode >= 500) {
|
||||
if i < c.retries {
|
||||
backoff := time.Duration(math.Pow(2, float64(i))) * time.Second
|
||||
log.Printf("Request failed (attempt %d/%d), retrying in %v: %v", i+1, c.retries, backoff, reqErr)
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if reqErr != nil {
|
||||
return nil, fmt.Errorf("request failed after %d retries: %w", c.retries, reqErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", readErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, bodyBytes)
|
||||
}
|
||||
|
||||
var stats map[string]interface{}
|
||||
if jsonErr := json.Unmarshal(bodyBytes, &stats); jsonErr != nil {
|
||||
return nil, jsonErr
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetActivities retrieves activities from the Python API wrapper
|
||||
func (c *Client) GetActivities(start, limit int) ([]GarminActivity, error) {
|
||||
url := fmt.Sprintf("%s/activities?start=%d&limit=%d", c.baseURL, start, limit)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var activities []GarminActivity
|
||||
if err := json.NewDecoder(resp.Body).Decode(&activities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return activities, nil
|
||||
url := fmt.Sprintf("%s/activities?start=%d&limit=%d", c.baseURL, start, limit)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var bodyBytes []byte
|
||||
reqErr := error(nil)
|
||||
|
||||
for i := 0; i <= c.retries; i++ {
|
||||
resp, reqErr = c.httpClient.Do(req)
|
||||
if reqErr != nil || (resp != nil && resp.StatusCode >= 500) {
|
||||
if i < c.retries {
|
||||
backoff := time.Duration(math.Pow(2, float64(i))) * time.Second
|
||||
log.Printf("Request failed (attempt %d/%d), retrying in %v: %v", i+1, c.retries, backoff, reqErr)
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if reqErr != nil {
|
||||
return nil, fmt.Errorf("request failed after %d retries: %w", c.retries, reqErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", readErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, bodyBytes)
|
||||
}
|
||||
|
||||
var activities []GarminActivity
|
||||
if jsonErr := json.Unmarshal(bodyBytes, &activities); jsonErr != nil {
|
||||
return nil, jsonErr
|
||||
}
|
||||
|
||||
return activities, nil
|
||||
}
|
||||
|
||||
// Helper function removed - no longer needed
|
||||
|
||||
// DownloadActivity downloads an activity via the Python API wrapper
|
||||
func (c *Client) DownloadActivity(activityID int, format string) ([]byte, error) {
|
||||
url := fmt.Sprintf("%s/activities/%d/download?format=%s", c.baseURL, activityID, format)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
url := fmt.Sprintf("%s/activities/%d/download?format=%s", c.baseURL, activityID, format)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
reqErr := error(nil)
|
||||
|
||||
for i := 0; i <= c.retries; i++ {
|
||||
resp, reqErr = c.httpClient.Do(req)
|
||||
if reqErr != nil || (resp != nil && resp.StatusCode >= 500) {
|
||||
if i < c.retries {
|
||||
backoff := time.Duration(math.Pow(2, float64(i))) * time.Second
|
||||
log.Printf("Download failed (attempt %d/%d), retrying in %v: %v", i+1, c.retries, backoff, reqErr)
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if reqErr != nil {
|
||||
return nil, fmt.Errorf("download failed after %d retries: %w", c.retries, reqErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// GetActivityDetails retrieves details for a specific activity from the Python API wrapper
|
||||
func (c *Client) GetActivityDetails(activityID int) (*GarminActivity, error) {
|
||||
url := fmt.Sprintf("%s/activities/%d", c.baseURL, activityID)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
|
||||
var activity GarminActivity
|
||||
if err := json.NewDecoder(resp.Body).Decode(&activity); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &activity, nil
|
||||
url := fmt.Sprintf("%s/activities/%d", c.baseURL, activityID)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp *http.Response
|
||||
var bodyBytes []byte
|
||||
reqErr := error(nil)
|
||||
|
||||
for i := 0; i <= c.retries; i++ {
|
||||
resp, reqErr = c.httpClient.Do(req)
|
||||
if reqErr != nil || (resp != nil && resp.StatusCode >= 500) {
|
||||
if i < c.retries {
|
||||
backoff := time.Duration(math.Pow(2, float64(i))) * time.Second
|
||||
log.Printf("Request failed (attempt %d/%d), retrying in %v: %v", i+1, c.retries, backoff, reqErr)
|
||||
time.Sleep(backoff)
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if reqErr != nil {
|
||||
return nil, fmt.Errorf("request failed after %d retries: %w", c.retries, reqErr)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, readErr := io.ReadAll(resp.Body)
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", readErr)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, bodyBytes)
|
||||
}
|
||||
|
||||
var activity GarminActivity
|
||||
if jsonErr := json.Unmarshal(bodyBytes, &activity); jsonErr != nil {
|
||||
return nil, jsonErr
|
||||
}
|
||||
|
||||
return &activity, nil
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/sstent/garminsync-go/internal/database"
|
||||
"github.com/sstent/garminsync-go/internal/garmin"
|
||||
@@ -26,18 +27,46 @@ func NewSyncService(garminClient *garmin.Client, db *database.SQLiteDB, dataDir
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SyncService) testAPIConnectivity() error {
|
||||
// Try a simple API call to check connectivity
|
||||
_, err := s.garminClient.GetActivities(0, 1)
|
||||
if err != nil {
|
||||
// Analyze error for troubleshooting hints
|
||||
if strings.Contains(err.Error(), "connection refused") {
|
||||
return fmt.Errorf("API connection failed: service might not be running. Verify garmin-api container is up. Original error: %w", err)
|
||||
} else if strings.Contains(err.Error(), "timeout") {
|
||||
return fmt.Errorf("API connection timeout: service might be slow to start. Original error: %w", err)
|
||||
} else if strings.Contains(err.Error(), "status 5") {
|
||||
return fmt.Errorf("API server error: check garmin-api logs. Original error: %w", err)
|
||||
}
|
||||
return fmt.Errorf("API connectivity test failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SyncService) FullSync(ctx context.Context) error {
|
||||
fmt.Println("=== Starting full sync ===")
|
||||
defer fmt.Println("=== Sync completed ===")
|
||||
fmt.Println("=== Starting full sync ===")
|
||||
defer fmt.Println("=== Sync completed ===")
|
||||
|
||||
// Check API connectivity before proceeding
|
||||
if err := s.testAPIConnectivity(); err != nil {
|
||||
return fmt.Errorf("API connectivity test failed: %w", err)
|
||||
}
|
||||
fmt.Println("✅ API connectivity verified")
|
||||
|
||||
// Check credentials first
|
||||
email := os.Getenv("GARMIN_EMAIL")
|
||||
password := os.Getenv("GARMIN_PASSWORD")
|
||||
|
||||
if email == "" || password == "" {
|
||||
return fmt.Errorf("Missing credentials - GARMIN_EMAIL: '%s', GARMIN_PASSWORD: %s",
|
||||
email,
|
||||
map[bool]string{true: "SET", false: "EMPTY"}[password != ""])
|
||||
errorMsg := fmt.Sprintf("Missing credentials - GARMIN_EMAIL: '%s', GARMIN_PASSWORD: %s",
|
||||
email,
|
||||
map[bool]string{true: "SET", false: "EMPTY"}[password != ""])
|
||||
errorMsg += "\nTroubleshooting:"
|
||||
errorMsg += "\n1. Ensure the .env file exists with GARMIN_EMAIL and GARMIN_PASSWORD"
|
||||
errorMsg += "\n2. Verify docker-compose.yml mounts the .env file"
|
||||
errorMsg += "\n3. Check container env vars: docker-compose exec garminsync env | grep GARMIN"
|
||||
return fmt.Errorf(errorMsg)
|
||||
}
|
||||
|
||||
fmt.Printf("Using credentials - Email: %s, Password: %s\n", email,
|
||||
|
||||
Reference in New Issue
Block a user