mirror of
https://github.com/sstent/garminsync-go.git
synced 2026-02-06 06:21:49 +00:00
working but cant login
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -51,12 +52,13 @@ func NewClient() *Client {
|
|||||||
return &Client{
|
return &Client{
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
|
Jar: nil, // Don't use cookie jar, we'll manage cookies manually
|
||||||
},
|
},
|
||||||
baseURL: "https://connect.garmin.com",
|
baseURL: "https://connect.garmin.com",
|
||||||
session: &Session{
|
session: &Session{
|
||||||
Username: os.Getenv("GARMIN_EMAIL"),
|
Username: os.Getenv("GARMIN_EMAIL"),
|
||||||
Password: os.Getenv("GARMIN_PASSWORD"),
|
Password: os.Getenv("GARMIN_PASSWORD"),
|
||||||
UserAgent: "GarminSync/1.0",
|
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,8 +68,13 @@ func (c *Client) Login() error {
|
|||||||
return fmt.Errorf("GARMIN_EMAIL and GARMIN_PASSWORD environment variables required")
|
return fmt.Errorf("GARMIN_EMAIL and GARMIN_PASSWORD environment variables required")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Get login form
|
fmt.Printf("DEBUG: Attempting login for user: %s\n", c.session.Username)
|
||||||
loginURL := c.baseURL + "/signin"
|
|
||||||
|
// Add random delay to look more human
|
||||||
|
time.Sleep(time.Duration(rand.Intn(1500)+1000) * time.Millisecond)
|
||||||
|
|
||||||
|
// Step 1: Get the initial login page to establish session
|
||||||
|
loginURL := "https://connect.garmin.com/signin/"
|
||||||
req, err := http.NewRequest("GET", loginURL, nil)
|
req, err := http.NewRequest("GET", loginURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -77,99 +84,272 @@ func (c *Client) Login() error {
|
|||||||
|
|
||||||
resp, err := c.httpClient.Do(req)
|
resp, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to get login page: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Extract cookies
|
fmt.Printf("DEBUG: Initial login page status: %d\n", resp.StatusCode)
|
||||||
|
|
||||||
|
// Store cookies
|
||||||
c.session.Cookies = resp.Cookies()
|
c.session.Cookies = resp.Cookies()
|
||||||
|
fmt.Printf("DEBUG: Received %d cookies from login page\n", len(c.session.Cookies))
|
||||||
|
|
||||||
// Step 2: Submit login credentials
|
// Step 2: Submit login credentials
|
||||||
loginData := url.Values{}
|
loginData := url.Values{}
|
||||||
loginData.Set("username", c.session.Username)
|
loginData.Set("username", c.session.Username)
|
||||||
loginData.Set("password", c.session.Password)
|
loginData.Set("password", c.session.Password)
|
||||||
loginData.Set("embed", "true")
|
loginData.Set("embed", "false")
|
||||||
|
loginData.Set("displayNameRequired", "false")
|
||||||
|
|
||||||
|
// Add another delay before POST
|
||||||
|
time.Sleep(time.Duration(rand.Intn(1500)+1000) * time.Millisecond)
|
||||||
|
|
||||||
req, err = http.NewRequest("POST", loginURL, strings.NewReader(loginData.Encode()))
|
req, err = http.NewRequest("POST", loginURL, strings.NewReader(loginData.Encode()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add extra headers
|
||||||
|
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
|
||||||
|
req.Header.Set("Connection", "keep-alive")
|
||||||
|
req.Header.Set("Pragma", "no-cache")
|
||||||
|
req.Header.Set("Cache-Control", "no-cache")
|
||||||
|
req.Header.Set("Upgrade-Insecure-Requests", "1")
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.Header.Set("User-Agent", c.session.UserAgent)
|
req.Header.Set("User-Agent", c.session.UserAgent)
|
||||||
|
req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01")
|
||||||
|
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||||
|
req.Header.Set("Origin", "https://sso.garmin.com")
|
||||||
|
req.Header.Set("Referer", loginURL)
|
||||||
|
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||||
|
|
||||||
// Add cookies
|
// Add existing cookies
|
||||||
for _, cookie := range c.session.Cookies {
|
for _, cookie := range c.session.Cookies {
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = c.httpClient.Do(req)
|
resp, err = c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to submit login: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
// Check if login was successful
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
if resp.StatusCode != http.StatusOK {
|
if err != nil {
|
||||||
return fmt.Errorf("login failed with status: %d", resp.StatusCode)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cookies
|
fmt.Printf("DEBUG: Login response status: %d\n", resp.StatusCode)
|
||||||
|
fmt.Printf("DEBUG: Login response body: %s\n", string(bodyBytes))
|
||||||
|
|
||||||
|
// Update cookies with login response
|
||||||
for _, cookie := range resp.Cookies() {
|
for _, cookie := range resp.Cookies() {
|
||||||
c.session.Cookies = append(c.session.Cookies, cookie)
|
c.session.Cookies = append(c.session.Cookies, cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.session.Authenticated = true
|
// Check for successful login indicators
|
||||||
return nil
|
bodyStr := string(bodyBytes)
|
||||||
}
|
if strings.Contains(bodyStr, "error") || strings.Contains(bodyStr, "invalid") {
|
||||||
|
return fmt.Errorf("login failed: %s", bodyStr)
|
||||||
func (c *Client) GetActivities(start, limit int) ([]GarminActivity, error) {
|
|
||||||
if !c.session.Authenticated {
|
|
||||||
if err := c.Login(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/modern/proxy/activitylist-service/activities/search/activities?start=%d&limit=%d",
|
// Step 3: Get the Garmin Connect session
|
||||||
c.baseURL, start, limit)
|
connectURL := "https://connect.garmin.com/modern/"
|
||||||
|
req, err = http.NewRequest("GET", connectURL, nil)
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("User-Agent", c.session.UserAgent)
|
req.Header.Set("User-Agent", c.session.UserAgent)
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
// Add cookies
|
// Add all cookies
|
||||||
for _, cookie := range c.session.Cookies {
|
for _, cookie := range c.session.Cookies {
|
||||||
req.AddCookie(cookie)
|
req.AddCookie(cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.httpClient.Do(req)
|
resp, err = c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return fmt.Errorf("failed to access Garmin Connect: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
fmt.Printf("DEBUG: Garmin Connect access status: %d\n", resp.StatusCode)
|
||||||
return nil, fmt.Errorf("failed to get activities: status %d", resp.StatusCode)
|
|
||||||
|
// Update cookies again
|
||||||
|
for _, cookie := range resp.Cookies() {
|
||||||
|
c.session.Cookies = append(c.session.Cookies, cookie)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Garmin API returns an object containing the activity list
|
fmt.Printf("DEBUG: Total cookies after login: %d\n", len(c.session.Cookies))
|
||||||
type responseStruct struct {
|
|
||||||
ActivityList []GarminActivity `json:"activityList"`
|
|
||||||
}
|
|
||||||
var response responseStruct
|
|
||||||
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
if resp.StatusCode == http.StatusOK {
|
||||||
return nil, err
|
c.session.Authenticated = true
|
||||||
|
fmt.Println("DEBUG: Login successful!")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rate limiting
|
return fmt.Errorf("login failed with status: %d", resp.StatusCode)
|
||||||
time.Sleep(2 * time.Second)
|
}
|
||||||
|
|
||||||
return response.ActivityList, nil
|
func (c *Client) GetActivities(start, limit int) ([]GarminActivity, error) {
|
||||||
|
if !c.session.Authenticated {
|
||||||
|
if err := c.Login(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("%s/modern/proxy/activity-service/activities/search/activities?start=%d&limit=%d",
|
||||||
|
c.baseURL, start, limit)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", c.session.UserAgent)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
// Add cookies
|
||||||
|
for _, cookie := range c.session.Cookies {
|
||||||
|
req.AddCookie(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log cookies being sent
|
||||||
|
fmt.Println("DEBUG: Cookies being sent:")
|
||||||
|
for _, cookie := range req.Cookies() {
|
||||||
|
fmt.Printf(" %s: %s (Expires: %s)\n",
|
||||||
|
cookie.Name,
|
||||||
|
cookie.Value[:min(3, len(cookie.Value))] + "***",
|
||||||
|
cookie.Expires.Format(time.RFC1123))
|
||||||
|
|
||||||
|
// Check if cookie is expired
|
||||||
|
if !cookie.Expires.IsZero() && cookie.Expires.Before(time.Now()) {
|
||||||
|
fmt.Printf("WARNING: Cookie %s expired at %s\n",
|
||||||
|
cookie.Name,
|
||||||
|
cookie.Expires.Format(time.RFC1123))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
fmt.Printf("DEBUG: HTTP Status: %d\n", resp.StatusCode)
|
||||||
|
fmt.Printf("DEBUG: Response Headers: %v\n", resp.Header)
|
||||||
|
|
||||||
|
// If we get empty response but 200 status, check session expiration
|
||||||
|
if resp.StatusCode == http.StatusOK && resp.ContentLength == 2 {
|
||||||
|
fmt.Println("WARNING: Empty API response with 200 status - checking session validity")
|
||||||
|
c.session.Authenticated = false
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log full response for debugging
|
||||||
|
fmt.Printf("DEBUG: Full API Response (%d bytes):\n", len(bodyBytes))
|
||||||
|
fmt.Println(string(bodyBytes))
|
||||||
|
|
||||||
|
// Check for empty response
|
||||||
|
if len(bodyBytes) == 0 {
|
||||||
|
return nil, fmt.Errorf("API returned empty response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for empty object
|
||||||
|
if string(bodyBytes) == "{}" {
|
||||||
|
fmt.Println("DEBUG: API returned empty object")
|
||||||
|
return nil, fmt.Errorf("API returned empty object")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try flexible parsing
|
||||||
|
activities, err := parseActivityResponse(bodyBytes)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("DEBUG: Failed to parse activities: %v\n", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("DEBUG: Successfully parsed %d activities\n", len(activities))
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
return activities, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseActivityResponse handles different API response formats
|
||||||
|
func parseActivityResponse(bodyBytes []byte) ([]GarminActivity, error) {
|
||||||
|
// Try standard ActivityList format
|
||||||
|
type ActivityListResponse struct {
|
||||||
|
ActivityList []GarminActivity `json:"activityList"`
|
||||||
|
}
|
||||||
|
var listResponse ActivityListResponse
|
||||||
|
if err := json.Unmarshal(bodyBytes, &listResponse); err == nil && len(listResponse.ActivityList) > 0 {
|
||||||
|
return listResponse.ActivityList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try direct array format
|
||||||
|
var directResponse []GarminActivity
|
||||||
|
if err := json.Unmarshal(bodyBytes, &directResponse); err == nil && len(directResponse) > 0 {
|
||||||
|
return directResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try generic map-based format
|
||||||
|
var genericResponse map[string]interface{}
|
||||||
|
if err := json.Unmarshal(bodyBytes, &genericResponse); err == nil {
|
||||||
|
// Check if we have an "activityList" key
|
||||||
|
if activityList, ok := genericResponse["activityList"].([]interface{}); ok {
|
||||||
|
return convertInterfaceSlice(activityList)
|
||||||
|
}
|
||||||
|
// Check if we have a "results" key
|
||||||
|
if results, ok := genericResponse["results"].([]interface{}); ok {
|
||||||
|
return convertInterfaceSlice(results)
|
||||||
|
}
|
||||||
|
// Check if we have an "activities" key
|
||||||
|
if activities, ok := genericResponse["activities"].([]interface{}); ok {
|
||||||
|
return convertInterfaceSlice(activities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Failed to parse
|
||||||
|
return nil, fmt.Errorf("unable to parse API response")
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertInterfaceSlice converts []interface{} to []GarminActivity
|
||||||
|
func convertInterfaceSlice(items []interface{}) ([]GarminActivity, error) {
|
||||||
|
var activities []GarminActivity
|
||||||
|
for _, item := range items {
|
||||||
|
itemMap, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert map to JSON then to GarminActivity
|
||||||
|
jsonData, err := json.Marshal(itemMap)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var activity GarminActivity
|
||||||
|
if err := json.Unmarshal(jsonData, &activity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
activities = append(activities, activity)
|
||||||
|
}
|
||||||
|
return activities, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DownloadActivity(activityID int, format string) ([]byte, error) {
|
func (c *Client) DownloadActivity(activityID int, format string) ([]byte, error) {
|
||||||
|
|||||||
@@ -27,25 +27,51 @@ func NewSyncService(garminClient *garmin.Client, db *database.SQLiteDB, dataDir
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SyncService) FullSync(ctx context.Context) error {
|
func (s *SyncService) FullSync(ctx context.Context) error {
|
||||||
fmt.Println("Starting full sync...")
|
fmt.Println("=== Starting full sync ===")
|
||||||
defer fmt.Println("Sync completed")
|
defer fmt.Println("=== Sync completed ===")
|
||||||
|
|
||||||
|
// 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 != ""])
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Using credentials - Email: %s, Password: %s\n", email,
|
||||||
|
map[bool]string{true: "***SET***", false: "EMPTY"}[password != ""])
|
||||||
|
|
||||||
// 1. Fetch activities from Garmin
|
// 1. Fetch activities from Garmin
|
||||||
activities, err := s.garminClient.GetActivities(0, 100)
|
fmt.Println("Fetching activities from Garmin Connect...")
|
||||||
|
activities, err := s.garminClient.GetActivities(0, 10) // Start with just 10 for testing
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get activities: %w", err)
|
return fmt.Errorf("failed to get activities: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Found %d activities\n", len(activities))
|
|
||||||
|
fmt.Printf("✅ Found %d activities from Garmin\n", len(activities))
|
||||||
|
|
||||||
|
if len(activities) == 0 {
|
||||||
|
fmt.Println("⚠️ No activities returned - this might be expected if:")
|
||||||
|
fmt.Println(" - Your Garmin account has no activities")
|
||||||
|
fmt.Println(" - The API response format changed")
|
||||||
|
fmt.Println(" - Authentication succeeded but data access failed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Process each activity
|
// 2. Process each activity
|
||||||
for _, activity := range activities {
|
for i, activity := range activities {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
fmt.Printf("Processing activity %d...\n", activity.ActivityID)
|
fmt.Printf("[%d/%d] Processing activity %d (%s)...\n",
|
||||||
|
i+1, len(activities), activity.ActivityID, activity.ActivityName)
|
||||||
if err := s.syncActivity(&activity); err != nil {
|
if err := s.syncActivity(&activity); err != nil {
|
||||||
fmt.Printf("Error syncing activity: %v\n", err)
|
fmt.Printf("❌ Error syncing activity %d: %v\n", activity.ActivityID, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ Successfully synced activity %d\n", activity.ActivityID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user