mirror of
https://github.com/sstent/aicyclingcoach-go.git
synced 2026-02-14 03:12:42 +00:00
sync
This commit is contained in:
Binary file not shown.
@@ -2,8 +2,10 @@ package garmin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/sstent/fitness-tui/internal/garmin/garth"
|
"github.com/sstent/fitness-tui/internal/garmin/garth/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Authenticate performs Garmin Connect authentication
|
// Authenticate performs Garmin Connect authentication
|
||||||
@@ -11,14 +13,34 @@ func (c *Client) Authenticate(logger Logger) error {
|
|||||||
logger.Infof("Authenticating with username: %s", c.username)
|
logger.Infof("Authenticating with username: %s", c.username)
|
||||||
|
|
||||||
// Initialize Garth client
|
// Initialize Garth client
|
||||||
garthClient := garth.New()
|
garthClient, err := client.NewClient("garmin.com")
|
||||||
|
if err != nil {
|
||||||
// Perform authentication
|
logger.Errorf("Failed to create Garmin client: %v", err)
|
||||||
if err := garthClient.Authenticate(c.username, c.password); err != nil {
|
return fmt.Errorf("failed to create client: %w", err)
|
||||||
logger.Errorf("Authentication failed: %v", err)
|
|
||||||
return fmt.Errorf("authentication failed: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to load existing session
|
||||||
|
sessionFile := filepath.Join(os.Getenv("HOME"), ".fitness-tui", "garmin_session.json")
|
||||||
|
if err := garthClient.LoadSession(sessionFile); err != nil {
|
||||||
|
logger.Infof("No existing session found, logging in with credentials")
|
||||||
|
|
||||||
|
// Perform authentication if no session exists
|
||||||
|
if err := garthClient.Login(c.username, c.password); err != nil {
|
||||||
|
logger.Errorf("Authentication failed: %v", err)
|
||||||
|
return fmt.Errorf("authentication failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save session for future use
|
||||||
|
if err := garthClient.SaveSession(sessionFile); err != nil {
|
||||||
|
logger.Warnf("Failed to save session: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Infof("Loaded existing session")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the authenticated client
|
||||||
|
c.garthClient = garthClient
|
||||||
|
|
||||||
logger.Infof("Authentication successful")
|
logger.Infof("Authentication successful")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
garth "garmin-connect/garth"
|
"github.com/sstent/fitness-tui/internal/garmin/garth"
|
||||||
|
"github.com/sstent/fitness-tui/internal/garmin/garth/client"
|
||||||
"github.com/sstent/fitness-tui/internal/tui/models"
|
"github.com/sstent/fitness-tui/internal/tui/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ type Client struct {
|
|||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
storagePath string
|
storagePath string
|
||||||
garthClient *garth.Client
|
garthClient *client.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(username, password, storagePath string) *Client {
|
func NewClient(username, password, storagePath string) *Client {
|
||||||
@@ -92,9 +92,9 @@ func (c *Client) GetActivities(ctx context.Context, limit int, logger Logger) ([
|
|||||||
// Convert to our internal model
|
// Convert to our internal model
|
||||||
activities := make([]*models.Activity, 0, len(garthActivities))
|
activities := make([]*models.Activity, 0, len(garthActivities))
|
||||||
for _, ga := range garthActivities {
|
for _, ga := range garthActivities {
|
||||||
startTime, err := time.Parse(time.RFC3339, ga.StartTimeGMT)
|
// Use the already parsed time from CustomTime struct
|
||||||
if err != nil {
|
if ga.StartTimeGMT.IsZero() {
|
||||||
logger.Warnf("Failed to parse activity time: %v", err)
|
logger.Warnf("Activity %d has invalid start time", ga.ActivityID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ func (c *Client) GetActivities(ctx context.Context, limit int, logger Logger) ([
|
|||||||
ID: fmt.Sprintf("%d", ga.ActivityID),
|
ID: fmt.Sprintf("%d", ga.ActivityID),
|
||||||
Name: ga.ActivityName,
|
Name: ga.ActivityName,
|
||||||
Type: ga.ActivityType.TypeKey,
|
Type: ga.ActivityType.TypeKey,
|
||||||
Date: startTime,
|
Date: ga.StartTimeGMT.Time, // Access the parsed time directly
|
||||||
Distance: ga.Distance,
|
Distance: ga.Distance,
|
||||||
Duration: time.Duration(ga.Duration) * time.Second,
|
Duration: time.Duration(ga.Duration) * time.Second,
|
||||||
Elevation: ga.ElevationGain,
|
Elevation: ga.ElevationGain,
|
||||||
|
|||||||
@@ -224,9 +224,11 @@ func (c *Client) GetActivities(limit int) ([]types.Activity, error) {
|
|||||||
// SaveSession saves the current session to a file
|
// SaveSession saves the current session to a file
|
||||||
func (c *Client) SaveSession(filename string) error {
|
func (c *Client) SaveSession(filename string) error {
|
||||||
session := types.SessionData{
|
session := types.SessionData{
|
||||||
Domain: c.Domain,
|
Domain: c.Domain,
|
||||||
Username: c.Username,
|
Username: c.Username,
|
||||||
AuthToken: c.AuthToken,
|
AuthToken: c.AuthToken,
|
||||||
|
OAuth1Token: c.OAuth1Token,
|
||||||
|
OAuth2Token: c.OAuth2Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.MarshalIndent(session, "", " ")
|
data, err := json.MarshalIndent(session, "", " ")
|
||||||
@@ -276,6 +278,8 @@ func (c *Client) LoadSession(filename string) error {
|
|||||||
c.Domain = session.Domain
|
c.Domain = session.Domain
|
||||||
c.Username = session.Username
|
c.Username = session.Username
|
||||||
c.AuthToken = session.AuthToken
|
c.AuthToken = session.AuthToken
|
||||||
|
c.OAuth1Token = session.OAuth1Token
|
||||||
|
c.OAuth2Token = session.OAuth2Token
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,97 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserProfile struct {
|
type UserProfile struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
ProfileID int `json:"profileId"`
|
ProfileID int `json:"profileId"`
|
||||||
GarminGUID string `json:"garminGuid"`
|
GarminGUID string `json:"garminGuid"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
FullName string `json:"fullName"`
|
FullName string `json:"fullName"`
|
||||||
UserName string `json:"userName"`
|
UserName string `json:"userName"`
|
||||||
ProfileImageType *string `json:"profileImageType"`
|
ProfileImageType *string `json:"profileImageType"`
|
||||||
ProfileImageURLLarge *string `json:"profileImageUrlLarge"`
|
ProfileImageURLLarge *string `json:"profileImageUrlLarge"`
|
||||||
ProfileImageURLMedium *string `json:"profileImageUrlMedium"`
|
ProfileImageURLMedium *string `json:"profileImageUrlMedium"`
|
||||||
ProfileImageURLSmall *string `json:"profileImageUrlSmall"`
|
ProfileImageURLSmall *string `json:"profileImageUrlSmall"`
|
||||||
Location *string `json:"location"`
|
Location *string `json:"location"`
|
||||||
FacebookURL *string `json:"facebookUrl"`
|
FacebookURL *string `json:"facebookUrl"`
|
||||||
TwitterURL *string `json:"twitterUrl"`
|
TwitterURL *string `json:"twitterUrl"`
|
||||||
PersonalWebsite *string `json:"personalWebsite"`
|
PersonalWebsite *string `json:"personalWebsite"`
|
||||||
Motivation *string `json:"motivation"`
|
Motivation *string `json:"motivation"`
|
||||||
Bio *string `json:"bio"`
|
Bio *string `json:"bio"`
|
||||||
PrimaryActivity *string `json:"primaryActivity"`
|
PrimaryActivity *string `json:"primaryActivity"`
|
||||||
FavoriteActivityTypes []string `json:"favoriteActivityTypes"`
|
FavoriteActivityTypes []string `json:"favoriteActivityTypes"`
|
||||||
RunningTrainingSpeed float64 `json:"runningTrainingSpeed"`
|
RunningTrainingSpeed float64 `json:"runningTrainingSpeed"`
|
||||||
CyclingTrainingSpeed float64 `json:"cyclingTrainingSpeed"`
|
CyclingTrainingSpeed float64 `json:"cyclingTrainingSpeed"`
|
||||||
FavoriteCyclingActivityTypes []string `json:"favoriteCyclingActivityTypes"`
|
FavoriteCyclingActivityTypes []string `json:"favoriteCyclingActivityTypes"`
|
||||||
CyclingClassification *string `json:"cyclingClassification"`
|
CyclingClassification *string `json:"cyclingClassification"`
|
||||||
CyclingMaxAvgPower float64 `json:"cyclingMaxAvgPower"`
|
CyclingMaxAvgPower float64 `json:"cyclingMaxAvgPower"`
|
||||||
SwimmingTrainingSpeed float64 `json:"swimmingTrainingSpeed"`
|
SwimmingTrainingSpeed float64 `json:"swimmingTrainingSpeed"`
|
||||||
ProfileVisibility string `json:"profileVisibility"`
|
ProfileVisibility string `json:"profileVisibility"`
|
||||||
ActivityStartVisibility string `json:"activityStartVisibility"`
|
ActivityStartVisibility string `json:"activityStartVisibility"`
|
||||||
ActivityMapVisibility string `json:"activityMapVisibility"`
|
ActivityMapVisibility string `json:"activityMapVisibility"`
|
||||||
CourseVisibility string `json:"courseVisibility"`
|
CourseVisibility string `json:"courseVisibility"`
|
||||||
ActivityHeartRateVisibility string `json:"activityHeartRateVisibility"`
|
ActivityHeartRateVisibility string `json:"activityHeartRateVisibility"`
|
||||||
ActivityPowerVisibility string `json:"activityPowerVisibility"`
|
ActivityPowerVisibility string `json:"activityPowerVisibility"`
|
||||||
BadgeVisibility string `json:"badgeVisibility"`
|
BadgeVisibility string `json:"badgeVisibility"`
|
||||||
ShowAge bool `json:"showAge"`
|
ShowAge bool `json:"showAge"`
|
||||||
ShowWeight bool `json:"showWeight"`
|
ShowWeight bool `json:"showWeight"`
|
||||||
ShowHeight bool `json:"showHeight"`
|
ShowHeight bool `json:"showHeight"`
|
||||||
ShowWeightClass bool `json:"showWeightClass"`
|
ShowWeightClass bool `json:"showWeightClass"`
|
||||||
ShowAgeRange bool `json:"showAgeRange"`
|
ShowAgeRange bool `json:"showAgeRange"`
|
||||||
ShowGender bool `json:"showGender"`
|
ShowGender bool `json:"showGender"`
|
||||||
ShowActivityClass bool `json:"showActivityClass"`
|
ShowActivityClass bool `json:"showActivityClass"`
|
||||||
ShowVO2Max bool `json:"showVo2Max"`
|
ShowVO2Max bool `json:"showVo2Max"`
|
||||||
ShowPersonalRecords bool `json:"showPersonalRecords"`
|
ShowPersonalRecords bool `json:"showPersonalRecords"`
|
||||||
ShowLast12Months bool `json:"showLast12Months"`
|
ShowLast12Months bool `json:"showLast12Months"`
|
||||||
ShowLifetimeTotals bool `json:"showLifetimeTotals"`
|
ShowLifetimeTotals bool `json:"showLifetimeTotals"`
|
||||||
ShowUpcomingEvents bool `json:"showUpcomingEvents"`
|
ShowUpcomingEvents bool `json:"showUpcomingEvents"`
|
||||||
ShowRecentFavorites bool `json:"showRecentFavorites"`
|
ShowRecentFavorites bool `json:"showRecentFavorites"`
|
||||||
ShowRecentDevice bool `json:"showRecentDevice"`
|
ShowRecentDevice bool `json:"showRecentDevice"`
|
||||||
ShowRecentGear bool `json:"showRecentGear"`
|
ShowRecentGear bool `json:"showRecentGear"`
|
||||||
ShowBadges bool `json:"showBadges"`
|
ShowBadges bool `json:"showBadges"`
|
||||||
OtherActivity *string `json:"otherActivity"`
|
OtherActivity *string `json:"otherActivity"`
|
||||||
OtherPrimaryActivity *string `json:"otherPrimaryActivity"`
|
OtherPrimaryActivity *string `json:"otherPrimaryActivity"`
|
||||||
OtherMotivation *string `json:"otherMotivation"`
|
OtherMotivation *string `json:"otherMotivation"`
|
||||||
UserRoles []string `json:"userRoles"`
|
UserRoles []string `json:"userRoles"`
|
||||||
NameApproved bool `json:"nameApproved"`
|
NameApproved bool `json:"nameApproved"`
|
||||||
UserProfileFullName string `json:"userProfileFullName"`
|
UserProfileFullName string `json:"userProfileFullName"`
|
||||||
MakeGolfScorecardsPrivate bool `json:"makeGolfScorecardsPrivate"`
|
MakeGolfScorecardsPrivate bool `json:"makeGolfScorecardsPrivate"`
|
||||||
AllowGolfLiveScoring bool `json:"allowGolfLiveScoring"`
|
AllowGolfLiveScoring bool `json:"allowGolfLiveScoring"`
|
||||||
AllowGolfScoringByConnections bool `json:"allowGolfScoringByConnections"`
|
AllowGolfScoringByConnections bool `json:"allowGolfScoringByConnections"`
|
||||||
UserLevel int `json:"userLevel"`
|
UserLevel int `json:"userLevel"`
|
||||||
UserPoint int `json:"userPoint"`
|
UserPoint int `json:"userPoint"`
|
||||||
LevelUpdateDate time.Time `json:"levelUpdateDate"`
|
LevelUpdateDate CustomTime `json:"levelUpdateDate"`
|
||||||
LevelIsViewed bool `json:"levelIsViewed"`
|
LevelIsViewed bool `json:"levelIsViewed"`
|
||||||
LevelPointThreshold int `json:"levelPointThreshold"`
|
LevelPointThreshold int `json:"levelPointThreshold"`
|
||||||
UserPointOffset int `json:"userPointOffset"`
|
UserPointOffset int `json:"userPointOffset"`
|
||||||
UserPro bool `json:"userPro"`
|
UserPro bool `json:"userPro"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomTime handles Garmin's timestamp format
|
||||||
|
type CustomTime struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
|
||||||
|
s := strings.Trim(string(b), "\"")
|
||||||
|
if s == "null" || s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try parsing with fractional seconds
|
||||||
|
t, err := time.Parse("2006-01-02T15:04:05.999", s)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to parsing without fractional seconds
|
||||||
|
t, err = time.Parse("2006-01-02T15:04:05", s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.Time = t
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,9 @@ func (c *Client) Login(email, password string) (*types.OAuth2Token, *MFAContext,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: Log full response body when title is unexpected
|
||||||
if title != "Success" {
|
if title != "Success" {
|
||||||
|
fmt.Printf("Unexpected login response body: %s\n", string(body))
|
||||||
return nil, nil, fmt.Errorf("login failed, unexpected title: %s", title)
|
return nil, nil, fmt.Errorf("login failed, unexpected title: %s", title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,9 +18,11 @@ type Client struct {
|
|||||||
|
|
||||||
// SessionData represents saved session information
|
// SessionData represents saved session information
|
||||||
type SessionData struct {
|
type SessionData struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
AuthToken string `json:"auth_token"`
|
AuthToken string `json:"auth_token"`
|
||||||
|
OAuth1Token *OAuth1Token `json:"oauth1_token,omitempty"`
|
||||||
|
OAuth2Token *OAuth2Token `json:"oauth2_token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActivityType represents the type of activity
|
// ActivityType represents the type of activity
|
||||||
@@ -40,8 +43,8 @@ type Activity struct {
|
|||||||
ActivityID int64 `json:"activityId"`
|
ActivityID int64 `json:"activityId"`
|
||||||
ActivityName string `json:"activityName"`
|
ActivityName string `json:"activityName"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
StartTimeLocal string `json:"startTimeLocal"`
|
StartTimeLocal CustomTime `json:"startTimeLocal"`
|
||||||
StartTimeGMT string `json:"startTimeGMT"`
|
StartTimeGMT CustomTime `json:"startTimeGMT"`
|
||||||
ActivityType ActivityType `json:"activityType"`
|
ActivityType ActivityType `json:"activityType"`
|
||||||
EventType EventType `json:"eventType"`
|
EventType EventType `json:"eventType"`
|
||||||
Distance float64 `json:"distance"`
|
Distance float64 `json:"distance"`
|
||||||
@@ -65,6 +68,37 @@ type OAuth1Token struct {
|
|||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CustomTime handles Garmin's timestamp formats
|
||||||
|
type CustomTime struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
|
||||||
|
s := strings.Trim(string(b), "\"")
|
||||||
|
if s == "null" || s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try different timestamp formats
|
||||||
|
formats := []string{
|
||||||
|
"2006-01-02T15:04:05.999",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
time.RFC3339,
|
||||||
|
}
|
||||||
|
|
||||||
|
var t time.Time
|
||||||
|
var err error
|
||||||
|
for _, format := range formats {
|
||||||
|
t, err = time.Parse(format, s)
|
||||||
|
if err == nil {
|
||||||
|
ct.Time = t
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// OAuth2Token represents OAuth2 token response
|
// OAuth2Token represents OAuth2 token response
|
||||||
type OAuth2Token struct {
|
type OAuth2Token struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
|
|||||||
BIN
go-garth/main
Executable file
BIN
go-garth/main
Executable file
Binary file not shown.
Reference in New Issue
Block a user