mirror of
https://github.com/sstent/go-garth.git
synced 2026-02-06 06:22:10 +00:00
sync
This commit is contained in:
15
cmd/garth/cmd/activities/activities.go
Normal file
15
cmd/garth/cmd/activities/activities.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package activities
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var ActivitiesCmd = &cobra.Command{
|
||||
Use: "activities",
|
||||
Short: "Manage activities from Garmin Connect",
|
||||
Long: `Commands for listing, downloading, and managing activities from Garmin Connect`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
ActivitiesCmd.AddCommand(ListCmd)
|
||||
}
|
||||
22
cmd/garth/cmd/activities/client.go
Normal file
22
cmd/garth/cmd/activities/client.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package activities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
type ActivitiesClient interface {
|
||||
GetActivities(start, end time.Time) ([]types.Activity, error)
|
||||
Login(email, password string) error
|
||||
SaveSession(filename string) error
|
||||
}
|
||||
|
||||
var newClient func(domain string) (ActivitiesClient, error) = func(domain string) (ActivitiesClient, error) {
|
||||
return client.NewClient(domain)
|
||||
}
|
||||
|
||||
func SetClient(f func(domain string) (ActivitiesClient, error)) {
|
||||
newClient = f
|
||||
}
|
||||
131
cmd/garth/cmd/activities/list.go
Normal file
131
cmd/garth/cmd/activities/list.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package activities
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
)
|
||||
|
||||
var (
|
||||
outputFormat string
|
||||
startDate string
|
||||
endDate string
|
||||
)
|
||||
|
||||
var ListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List activities from Garmin Connect",
|
||||
Long: `List activities with filtering by date range and multiple output formats`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newClient("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("client creation failed: %w", err)
|
||||
}
|
||||
|
||||
start, end, err := getDateFilter()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid date filter: %w", err)
|
||||
}
|
||||
activities, err := c.GetActivities(start, end)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get activities: %w", err)
|
||||
}
|
||||
|
||||
switch outputFormat {
|
||||
case "table":
|
||||
renderTable(activities)
|
||||
case "json":
|
||||
if err := renderJSON(activities); err != nil {
|
||||
return err
|
||||
}
|
||||
case "csv":
|
||||
if err := renderCSV(activities); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid output format: %s", outputFormat)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
ListCmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "Output format (table|json|csv)")
|
||||
ListCmd.Flags().StringVar(&startDate, "start", "", "Start date (YYYY-MM-DD)")
|
||||
ListCmd.Flags().StringVar(&endDate, "end", "", "End date (YYYY-MM-DD)")
|
||||
}
|
||||
|
||||
func getDateFilter() (time.Time, time.Time, error) {
|
||||
var start, end time.Time
|
||||
var err error
|
||||
|
||||
if startDate != "" {
|
||||
start, err = time.Parse("2006-01-02", startDate)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid start date format: %w", err)
|
||||
}
|
||||
}
|
||||
if endDate != "" {
|
||||
end, err = time.Parse("2006-01-02", endDate)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid end date format: %w", err)
|
||||
}
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func renderTable(activities []types.Activity) {
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.Header("ID", "Name", "Type", "Date", "Distance (km)", "Duration")
|
||||
|
||||
for _, a := range activities {
|
||||
table.Append(
|
||||
fmt.Sprint(a.ActivityID),
|
||||
a.ActivityName,
|
||||
a.ActivityType.TypeKey,
|
||||
a.StartTimeLocal,
|
||||
fmt.Sprintf("%.2f", a.Distance/1000),
|
||||
time.Duration(a.Duration*float64(time.Second)).String(),
|
||||
)
|
||||
}
|
||||
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func renderJSON(activities []types.Activity) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(activities)
|
||||
}
|
||||
|
||||
func renderCSV(activities []types.Activity) error {
|
||||
w := csv.NewWriter(os.Stdout)
|
||||
defer w.Flush()
|
||||
|
||||
if err := w.Write([]string{"ID", "Name", "Type", "Date", "Distance (km)", "Duration"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, a := range activities {
|
||||
record := []string{
|
||||
fmt.Sprint(a.ActivityID),
|
||||
a.ActivityName,
|
||||
a.ActivityType.TypeKey,
|
||||
a.StartTimeLocal,
|
||||
fmt.Sprintf("%.2f", a.Distance/1000),
|
||||
time.Duration(a.Duration * float64(time.Second)).String(),
|
||||
}
|
||||
if err := w.Write(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
193
cmd/garth/cmd/activities/list_test.go
Normal file
193
cmd/garth/cmd/activities/list_test.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package activities
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sstent/go-garth/garth/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type mockClient struct{}
|
||||
|
||||
func (m *mockClient) GetActivities(start, end time.Time) ([]types.Activity, error) {
|
||||
return []types.Activity{
|
||||
{
|
||||
ActivityID: 123,
|
||||
ActivityName: "Morning Run",
|
||||
ActivityType: types.ActivityType{
|
||||
TypeKey: "running",
|
||||
},
|
||||
StartTimeLocal: "2025-09-18T08:30:00",
|
||||
Distance: 5000,
|
||||
Duration: 1800,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockClient) Login(email, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockClient) SaveSession(filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRenderTable(t *testing.T) {
|
||||
activities := []types.Activity{
|
||||
{
|
||||
ActivityID: 123,
|
||||
ActivityName: "Morning Run",
|
||||
ActivityType: types.ActivityType{
|
||||
TypeKey: "running",
|
||||
},
|
||||
StartTimeLocal: "2025-09-18T08:30:00",
|
||||
Distance: 5000,
|
||||
Duration: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
renderTable(activities)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "123")
|
||||
assert.Contains(t, output, "Morning Run")
|
||||
assert.Contains(t, output, "running")
|
||||
}
|
||||
|
||||
func TestRenderJSON(t *testing.T) {
|
||||
activities := []types.Activity{
|
||||
{
|
||||
ActivityID: 123,
|
||||
ActivityName: "Morning Run",
|
||||
ActivityType: types.ActivityType{
|
||||
TypeKey: "running",
|
||||
},
|
||||
StartTimeLocal: "2025-09-18T08:30:00",
|
||||
Distance: 5000,
|
||||
Duration: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := renderJSON(activities)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, `"activityId": 123`)
|
||||
assert.Contains(t, output, `"activityName": "Morning Run"`)
|
||||
}
|
||||
|
||||
func TestRenderCSV(t *testing.T) {
|
||||
activities := []types.Activity{
|
||||
{
|
||||
ActivityID: 123,
|
||||
ActivityName: "Morning Run",
|
||||
ActivityType: types.ActivityType{
|
||||
TypeKey: "running",
|
||||
},
|
||||
StartTimeLocal: "2025-09-18T08:30:00",
|
||||
Distance: 5000,
|
||||
Duration: 1800,
|
||||
},
|
||||
}
|
||||
|
||||
// Capture stdout
|
||||
old := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err := renderCSV(activities)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.ReadFrom(r)
|
||||
output := buf.String()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, "123,Morning Run,running")
|
||||
}
|
||||
|
||||
func TestGetDateFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
startDate string
|
||||
endDate string
|
||||
wantStart bool
|
||||
wantEnd bool
|
||||
}{
|
||||
{
|
||||
name: "no dates",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
wantStart: false,
|
||||
wantEnd: false,
|
||||
},
|
||||
{
|
||||
name: "start date only",
|
||||
startDate: "2025-09-18",
|
||||
endDate: "",
|
||||
wantStart: true,
|
||||
wantEnd: false,
|
||||
},
|
||||
{
|
||||
name: "both dates",
|
||||
startDate: "2025-09-18",
|
||||
endDate: "2025-09-20",
|
||||
wantStart: true,
|
||||
wantEnd: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Save original values
|
||||
origStart, origEnd := startDate, endDate
|
||||
defer func() {
|
||||
startDate, endDate = origStart, origEnd
|
||||
}()
|
||||
|
||||
startDate = tt.startDate
|
||||
endDate = tt.endDate
|
||||
|
||||
start, end := getDateFilter()
|
||||
|
||||
if tt.wantStart {
|
||||
assert.False(t, start.IsZero(), "expected non-zero start time")
|
||||
} else {
|
||||
assert.True(t, start.IsZero(), "expected zero start time")
|
||||
}
|
||||
|
||||
if tt.wantEnd {
|
||||
assert.False(t, end.IsZero(), "expected non-zero end time")
|
||||
} else {
|
||||
assert.True(t, end.IsZero(), "expected zero end time")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
57
cmd/garth/cmd/auth/login.go
Normal file
57
cmd/garth/cmd/auth/login.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
var (
|
||||
email string
|
||||
password string
|
||||
)
|
||||
|
||||
var LoginCmd = &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Authenticate with Garmin Connect",
|
||||
Long: `Authenticate using Garmin Connect credentials and save session tokens`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if password == "" {
|
||||
fmt.Print("Enter password: ")
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("password input failed: %w", err)
|
||||
}
|
||||
password = string(bytePassword)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
c, err := client.NewClient("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("client creation failed: %w", err)
|
||||
}
|
||||
|
||||
if err := c.Login(email, password); err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
|
||||
if err := c.SaveSession("session.json"); err != nil {
|
||||
return fmt.Errorf("session save failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Logged in successfully. Session saved to session.json")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
LoginCmd.Flags().StringVarP(&email, "email", "e", "", "Garmin login email")
|
||||
LoginCmd.Flags().StringVarP(&password, "password", "p", "", "Garmin login password (optional, will prompt if empty)")
|
||||
|
||||
// Mark required flags
|
||||
LoginCmd.MarkFlagRequired("email")
|
||||
}
|
||||
38
cmd/garth/cmd/auth/logout.go
Normal file
38
cmd/garth/cmd/auth/logout.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var LogoutCmd = &cobra.Command{
|
||||
Use: "logout",
|
||||
Short: "Logout and remove saved session",
|
||||
Long: `Remove the saved session file and clear authentication tokens`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sessionPath, _ := cmd.Flags().GetString("session")
|
||||
if sessionPath == "" {
|
||||
sessionPath = "session.json"
|
||||
}
|
||||
|
||||
// Check if session file exists
|
||||
if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
|
||||
fmt.Printf("No session file found at %s\n", sessionPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove session file
|
||||
if err := os.Remove(sessionPath); err != nil {
|
||||
return fmt.Errorf("failed to remove session file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Logged out successfully. Session file removed: %s\n", sessionPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
LogoutCmd.Flags().StringP("session", "s", "session.json", "Session file path to remove")
|
||||
}
|
||||
59
cmd/garth/cmd/auth/status.go
Normal file
59
cmd/garth/cmd/auth/status.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
var StatusCmd = &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Check authentication status",
|
||||
Long: `Check if you are currently authenticated with Garmin Connect`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
sessionPath, _ := cmd.Flags().GetString("session")
|
||||
if sessionPath == "" {
|
||||
sessionPath = "session.json"
|
||||
}
|
||||
|
||||
// Check if session file exists
|
||||
if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
|
||||
fmt.Printf("❌ Not authenticated\n")
|
||||
fmt.Printf("Session file not found: %s\n", sessionPath)
|
||||
fmt.Printf("Run 'garth auth login' to authenticate\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to create client and load session
|
||||
c, err := client.NewClient("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("client creation failed: %w", err)
|
||||
}
|
||||
|
||||
if err := c.LoadSession(sessionPath); err != nil {
|
||||
fmt.Printf("❌ Session file exists but is invalid\n")
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
fmt.Printf("Run 'garth auth login' to re-authenticate\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to make a simple authenticated request to verify session
|
||||
if _, err := c.GetUserProfile(); err != nil {
|
||||
fmt.Printf("❌ Session exists but authentication failed\n")
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
fmt.Printf("Run 'garth auth login' to re-authenticate\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Authenticated\n")
|
||||
fmt.Printf("Session file: %s\n", sessionPath)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
StatusCmd.Flags().StringP("session", "s", "session.json", "Session file path to check")
|
||||
}
|
||||
111
cmd/garth/cmd/root.go
Normal file
111
cmd/garth/cmd/root.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/sstent/go-garth/cmd/garth/cmd/activities"
|
||||
"github.com/sstent/go-garth/cmd/garth/cmd/auth"
|
||||
garthclient "github.com/sstent/go-garth/garth/client"
|
||||
)
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
garthClient *garthclient.Client
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "garth",
|
||||
Short: "Garmin Connect CLI client",
|
||||
Long: `A comprehensive CLI client for Garmin Connect.
|
||||
Access your activities, health data, and statistics from the command line.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
initClient()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Add subcommands
|
||||
rootCmd.AddCommand(auth.LoginCmd)
|
||||
rootCmd.AddCommand(auth.LogoutCmd)
|
||||
rootCmd.AddCommand(auth.StatusCmd)
|
||||
rootCmd.AddCommand(activities.ActivitiesCmd)
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
|
||||
"config file (default is $HOME/.garth/config.yaml)")
|
||||
rootCmd.PersistentFlags().StringP("format", "f", "table",
|
||||
"output format (table, json, csv)")
|
||||
rootCmd.PersistentFlags().BoolP("verbose", "v", false,
|
||||
"verbose output")
|
||||
rootCmd.PersistentFlags().StringP("session", "s", "",
|
||||
"session file path")
|
||||
|
||||
viper.BindPFlag("format", rootCmd.PersistentFlags().Lookup("format"))
|
||||
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
|
||||
viper.BindPFlag("session", rootCmd.PersistentFlags().Lookup("session"))
|
||||
}
|
||||
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
home, err := os.UserHomeDir()
|
||||
cobra.CheckErr(err)
|
||||
|
||||
configDir := filepath.Join(home, ".garth")
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating config directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
viper.AddConfigPath(configDir)
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv()
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
fmt.Fprintf(os.Stderr, "Error reading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func initClient() {
|
||||
var err error
|
||||
domain := viper.GetString("domain")
|
||||
if domain == "" {
|
||||
domain = "garmin.com"
|
||||
}
|
||||
|
||||
garthClient, err = garthclient.NewClient(domain)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating client: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sessionPath := viper.GetString("session")
|
||||
if sessionPath == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting home directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
sessionPath = filepath.Join(home, ".garth", "session.json")
|
||||
}
|
||||
|
||||
if err := garthClient.LoadSession(sessionPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not load session: %v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"garmin-connect/garth"
|
||||
"garmin-connect/garth/credentials"
|
||||
auth "github.com/sstent/go-garth/internal/auth"
|
||||
garmin "github.com/sstent/go-garth/pkg/garmin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -23,13 +23,13 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Load credentials from .env file
|
||||
email, password, domain, err := credentials.LoadEnvCredentials()
|
||||
email, password, domain, err := auth.LoadEnvCredentials()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load credentials: %v", err)
|
||||
}
|
||||
|
||||
// Create client
|
||||
garminClient, err := garth.NewClient(domain)
|
||||
garminClient, err := garmin.NewClient(domain)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func main() {
|
||||
displayActivities(activities)
|
||||
}
|
||||
|
||||
func outputTokensJSON(c *garth.Client) {
|
||||
func outputTokensJSON(c *garmin.Client) {
|
||||
tokens := struct {
|
||||
OAuth1 *garth.OAuth1Token `json:"oauth1"`
|
||||
OAuth2 *garth.OAuth2Token `json:"oauth2"`
|
||||
|
||||
Reference in New Issue
Block a user