mirror of
https://github.com/sstent/go-garth-cli.git
synced 2025-12-06 08:02:01 +00:00
sync
This commit is contained in:
19
shared/interfaces/api_client.go
Normal file
19
shared/interfaces/api_client.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
types "go-garth/internal/models/types"
|
||||
"go-garth/shared/models"
|
||||
)
|
||||
|
||||
// APIClient defines the interface for making API calls that data packages need.
|
||||
type APIClient interface {
|
||||
ConnectAPI(path string, method string, params url.Values, body io.Reader) ([]byte, error)
|
||||
GetUsername() string
|
||||
GetUserSettings() (*models.UserSettings, error)
|
||||
GetUserProfile() (*types.UserProfile, error)
|
||||
GetWellnessData(startDate, endDate time.Time) ([]types.WellnessData, error)
|
||||
}
|
||||
129
shared/interfaces/data.go
Normal file
129
shared/interfaces/data.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-garth/internal/utils"
|
||||
)
|
||||
|
||||
// Data defines the interface for Garmin Connect data models.
|
||||
// Concrete data types (BodyBattery, HRV, Sleep, etc.) must implement this interface.
|
||||
//
|
||||
// The Get method retrieves data for a single day.
|
||||
// The List method concurrently retrieves data for a range of days.
|
||||
type Data interface {
|
||||
Get(day time.Time, c APIClient) (interface{}, error)
|
||||
List(end time.Time, days int, c APIClient, maxWorkers int) ([]interface{}, []error)
|
||||
}
|
||||
|
||||
// BaseData provides a reusable implementation for data types to embed.
|
||||
// It handles the concurrent List() implementation while allowing concrete types
|
||||
// to focus on implementing the Get() method for their specific data structure.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// type BodyBatteryData {
|
||||
// interfaces.BaseData
|
||||
// // ... additional fields
|
||||
// }
|
||||
//
|
||||
// func NewBodyBatteryData() *BodyBatteryData {
|
||||
// bb := &BodyBatteryData{}
|
||||
// bb.GetFunc = bb.get // Assign the concrete Get implementation
|
||||
// return bb
|
||||
// }
|
||||
//
|
||||
// func (bb *BodyBatteryData) get(day time.Time, c APIClient) (interface{}, error) {
|
||||
// // Implementation specific to body battery data
|
||||
// }
|
||||
type BaseData struct {
|
||||
// GetFunc must be set by concrete types to implement the Get method.
|
||||
// This function pointer allows BaseData to call the concrete implementation.
|
||||
GetFunc func(day time.Time, c APIClient) (interface{}, error)
|
||||
}
|
||||
|
||||
// Get implements the Data interface by calling the configured GetFunc.
|
||||
// Returns an error if GetFunc is not set.
|
||||
func (b *BaseData) Get(day time.Time, c APIClient) (interface{}, error) {
|
||||
if b.GetFunc == nil {
|
||||
return nil, errors.New("GetFunc not implemented for this data type")
|
||||
}
|
||||
return b.GetFunc(day, c)
|
||||
}
|
||||
|
||||
// List implements concurrent data fetching using a worker pool pattern.
|
||||
// This method efficiently retrieves data for multiple days by distributing
|
||||
// work across a configurable number of workers (goroutines).
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// end: The end date of the range (inclusive)
|
||||
// days: Number of days to fetch (going backwards from end date)
|
||||
// c: Client instance for API access
|
||||
// maxWorkers: Maximum concurrent workers (minimum 1)
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// []interface{}: Slice of results (order matches date range)
|
||||
// []error: Slice of errors encountered during processing
|
||||
func (b *BaseData) List(end time.Time, days int, c APIClient, maxWorkers int) ([]interface{}, []error) {
|
||||
if maxWorkers < 1 {
|
||||
maxWorkers = 10 // Match Python's MAX_WORKERS
|
||||
}
|
||||
|
||||
dates := utils.DateRange(end, days)
|
||||
|
||||
// Define result type for channel
|
||||
type result struct {
|
||||
data interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
workCh := make(chan time.Time, days)
|
||||
resultsCh := make(chan result, days)
|
||||
|
||||
// Worker function
|
||||
worker := func() {
|
||||
defer wg.Done()
|
||||
for date := range workCh {
|
||||
data, err := b.Get(date, c)
|
||||
resultsCh <- result{data: data, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
// Start workers
|
||||
wg.Add(maxWorkers)
|
||||
for i := 0; i < maxWorkers; i++ {
|
||||
go worker()
|
||||
}
|
||||
|
||||
// Send work
|
||||
go func() {
|
||||
for _, date := range dates {
|
||||
workCh <- date
|
||||
}
|
||||
close(workCh)
|
||||
}()
|
||||
|
||||
// Close results channel when workers are done
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsCh)
|
||||
}()
|
||||
|
||||
var results []interface{}
|
||||
var errs []error
|
||||
|
||||
for r := range resultsCh {
|
||||
if r.err != nil {
|
||||
errs = append(errs, r.err)
|
||||
} else if r.data != nil {
|
||||
results = append(results, r.data)
|
||||
}
|
||||
}
|
||||
|
||||
return results, errs
|
||||
}
|
||||
88
shared/models/user_settings.go
Normal file
88
shared/models/user_settings.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type PowerFormat struct {
|
||||
FormatID int `json:"formatId"`
|
||||
FormatKey string `json:"formatKey"`
|
||||
MinFraction int `json:"minFraction"`
|
||||
MaxFraction int `json:"maxFraction"`
|
||||
GroupingUsed bool `json:"groupingUsed"`
|
||||
DisplayFormat *string `json:"displayFormat"`
|
||||
}
|
||||
|
||||
type FirstDayOfWeek struct {
|
||||
DayID int `json:"dayId"`
|
||||
DayName string `json:"dayName"`
|
||||
SortOrder int `json:"sortOrder"`
|
||||
IsPossibleFirstDay bool `json:"isPossibleFirstDay"`
|
||||
}
|
||||
|
||||
type WeatherLocation struct {
|
||||
UseFixedLocation *bool `json:"useFixedLocation"`
|
||||
Latitude *float64 `json:"latitude"`
|
||||
Longitude *float64 `json:"longitude"`
|
||||
LocationName *string `json:"locationName"`
|
||||
ISOCountryCode *string `json:"isoCountryCode"`
|
||||
PostalCode *string `json:"postalCode"`
|
||||
}
|
||||
|
||||
type UserData struct {
|
||||
Gender string `json:"gender"`
|
||||
Weight float64 `json:"weight"`
|
||||
Height float64 `json:"height"`
|
||||
TimeFormat string `json:"timeFormat"`
|
||||
BirthDate time.Time `json:"birthDate"`
|
||||
MeasurementSystem string `json:"measurementSystem"`
|
||||
ActivityLevel *string `json:"activityLevel"`
|
||||
Handedness string `json:"handedness"`
|
||||
PowerFormat PowerFormat `json:"powerFormat"`
|
||||
HeartRateFormat PowerFormat `json:"heartRateFormat"`
|
||||
FirstDayOfWeek FirstDayOfWeek `json:"firstDayOfWeek"`
|
||||
VO2MaxRunning *float64 `json:"vo2MaxRunning"`
|
||||
VO2MaxCycling *float64 `json:"vo2MaxCycling"`
|
||||
LactateThresholdSpeed *float64 `json:"lactateThresholdSpeed"`
|
||||
LactateThresholdHeartRate *float64 `json:"lactateThresholdHeartRate"`
|
||||
DiveNumber *int `json:"diveNumber"`
|
||||
IntensityMinutesCalcMethod string `json:"intensityMinutesCalcMethod"`
|
||||
ModerateIntensityMinutesHRZone int `json:"moderateIntensityMinutesHrZone"`
|
||||
VigorousIntensityMinutesHRZone int `json:"vigorousIntensityMinutesHrZone"`
|
||||
HydrationMeasurementUnit string `json:"hydrationMeasurementUnit"`
|
||||
HydrationContainers []map[string]interface{} `json:"hydrationContainers"`
|
||||
HydrationAutoGoalEnabled bool `json:"hydrationAutoGoalEnabled"`
|
||||
FirstbeatMaxStressScore *float64 `json:"firstbeatMaxStressScore"`
|
||||
FirstbeatCyclingLTTimestamp *int64 `json:"firstbeatCyclingLtTimestamp"`
|
||||
FirstbeatRunningLTTimestamp *int64 `json:"firstbeatRunningLtTimestamp"`
|
||||
ThresholdHeartRateAutoDetected bool `json:"thresholdHeartRateAutoDetected"`
|
||||
FTPAutoDetected *bool `json:"ftpAutoDetected"`
|
||||
TrainingStatusPausedDate *string `json:"trainingStatusPausedDate"`
|
||||
WeatherLocation *WeatherLocation `json:"weatherLocation"`
|
||||
GolfDistanceUnit *string `json:"golfDistanceUnit"`
|
||||
GolfElevationUnit *string `json:"golfElevationUnit"`
|
||||
GolfSpeedUnit *string `json:"golfSpeedUnit"`
|
||||
ExternalBottomTime *float64 `json:"externalBottomTime"`
|
||||
}
|
||||
|
||||
type UserSleep struct {
|
||||
SleepTime int `json:"sleepTime"`
|
||||
DefaultSleepTime bool `json:"defaultSleepTime"`
|
||||
WakeTime int `json:"wakeTime"`
|
||||
DefaultWakeTime bool `json:"defaultWakeTime"`
|
||||
}
|
||||
|
||||
type UserSleepWindow struct {
|
||||
SleepWindowFrequency string `json:"sleepWindowFrequency"`
|
||||
StartSleepTimeSecondsFromMidnight int `json:"startSleepTimeSecondsFromMidnight"`
|
||||
EndSleepTimeSecondsFromMidnight int `json:"endSleepTimeSecondsFromMidnight"`
|
||||
}
|
||||
|
||||
type UserSettings struct {
|
||||
ID int `json:"id"`
|
||||
UserData UserData `json:"userData"`
|
||||
UserSleep UserSleep `json:"userSleep"`
|
||||
ConnectDate *string `json:"connectDate"`
|
||||
SourceType *string `json:"sourceType"`
|
||||
UserSleepWindows []UserSleepWindow `json:"userSleepWindows,omitempty"`
|
||||
}
|
||||
Reference in New Issue
Block a user