package main import ( "bytes" "encoding/json" "errors" "flag" "io" "log" "net/http" "os" "strconv" "strings" "time" ) type config struct { ClientId string `json:"client_id"` ClientSecret string `json:"client_secret"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` OutputPath string `json:"output_folder"` } type refresh_token_request struct { ClientId string `json:"client_id"` ClientSecret string `json:"client_secret"` RefreshToken string `json:"refresh_token"` GrantType string `json:"grant_type"` } type refresh_token_response struct { TokenType string `json:"token_type"` AccessToken string `json:"access_token"` Expires_at json.Number `json:"expires_at"` Expires_in json.Number `json:"expires_in"` RefreshToken string `json:"refresh_token"` } type stream struct { Type string `json:"type"` Data []interface{} `json:"data"` SeriesType string `json:"series_type"` OriginalSiye json.Number `json:"original_size"` Resolution string `json:"resolution"` } type activity struct { ResourceState json.Number `json:"resource_state"` Athlete struct { Id json.Number `json:"id"` ResourceState json.Number `json:"resource_state"` } `json:"athlete"` Name string `json:"name"` Distance json.Number `json:"distance"` MovingTime json.Number `json:"moving_time"` ElapsedTime json.Number `json:"elapsed_time"` TotalElevationGain json.Number `json:"total_elevation_gain"` Type string `json:"type"` SportType string `json:"sport_type"` Id json.Number `json:"id"` ExternalId string `json:"external_id"` UploadId json.Number `json:"upload_id"` StartDate time.Time `json:"start_date"` StartDateLocal time.Time `json:"start_date_local"` TimeZone string `json:"timezone"` UtcOffset json.Number `json:"utc_offset"` Trainer bool `json:"trainer"` Commute bool `json:"commute"` Manual bool `json:"manual"` Private bool `json:"private"` Flagged bool `json:"flagged"` AverageSpeed json.Number `json:"average_speed"` MaxSpeed json.Number `json:"max_speed"` AverageCadence json.Number `json:"average_cadence"` AverageWatts json.Number `json:"average_watts"` WeightedAverageWatts json.Number `json:"weighted_average_watts"` KiloJoules json.Number `json:"kilojoules"` DeviceWatts bool `json:"device_watts"` HasHeartrate bool `json:"has_heartrate"` AverageHeartrate json.Number `json:"average_heartrate"` MaxHeartrate json.Number `json:"max_heartrate"` MaxWatts json.Number `json:"max_watts"` SufferScore json.Number `json:"suffer_score"` // Streams []stream `json:"streams"` } var ( logger = *log.Default() config_path string config_cache config access_token_path string error_too_many_req = errors.New("Too Many Requests") ) func init() { logger.SetFlags(log.Llongfile | log.Ltime) logger.Print("starting...") } func read_config() { data, err := os.ReadFile(config_path) if err != nil { logger.Printf("Unable to read %s", config_path) return } err = json.Unmarshal(data, &config_cache) if err != nil { logger.Print("Unable to evaluate config data") return } } func get_remote_access() { logger.Print("Requesting new access token.") url := "https://www.strava.com/oauth/token" ref := refresh_token_request{ ClientId: config_cache.ClientId, ClientSecret: config_cache.ClientSecret, GrantType: "refresh_token", RefreshToken: config_cache.RefreshToken, } res, err := json.Marshal(ref) if err != nil { logger.Panic(err) } req, err := http.NewRequest("POST", url, bytes.NewBuffer(res)) if err != nil { logger.Panic(err) } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { logger.Panic(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { logger.Panic(resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { logger.Panic(err) } var access_token refresh_token_response err = json.Unmarshal(body, &access_token) if err != nil { logger.Panic(err) } f, err := os.Create(access_token_path) if err != nil { logger.Panic(err) } data, err := json.Marshal(access_token) f.Write(data) } func get_remote_access_token() (*refresh_token_response, error) { logger.Printf("Reading remote access token from %s.\n", access_token_path) data, err := os.ReadFile(access_token_path) if err != nil { get_remote_access() } data, err = os.ReadFile(access_token_path) if err != nil { return nil, err } var token refresh_token_response err = json.Unmarshal(data, &token) if err != nil { return nil, err } expire, _ := token.Expires_at.Int64() if expire < time.Now().Unix() { get_remote_access() data, err = os.ReadFile(access_token_path) if err != nil { return nil, err } err = json.Unmarshal(data, &token) if err != nil { return nil, err } } return &token, nil } func get_remote_activities_since(token refresh_token_response, date time.Time) (*[]activity, error) { logger.Printf("Download acitivities after %s\n", date) loop_count := 1 client := &http.Client{} value := token.TokenType + " " + token.AccessToken var ret []activity var after int = 0 if date.Unix() > int64(after) { after = int(date.Unix()) } for { url := "https://www.strava.com/api/v3/athlete/activities?per_page=200&page=" + strconv.Itoa(loop_count) + "&after=" + strconv.Itoa(after) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Authorization", value) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { if resp.StatusCode == 429 { return nil, error_too_many_req } logger.Print(resp.Status) logger.Panic(resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var activities []activity err = json.Unmarshal(body, &activities) if err != nil { return nil, err } ret = append(ret, activities[:]...) if len(activities) < 200 { break } loop_count++ } return &ret, nil } // func get_processed_ids() []json.Number { // logger.Print("Getting already processed IDs") // files, err := os.ReadDir(config_cache.OutputPath) // if err != nil { // logger.Panic(err) // } // var ret []json.Number // for _, file := range files { // res, err := os.ReadFile(config_cache.OutputPath + "/" + file.Name()) // if err != nil { // continue // } // var act activity // err = json.Unmarshal(res, &act) // if err != nil { // continue // } // ret = append(ret, act.Id) // } // return ret // } func get_last_processed_date(folder string) *time.Time { logger.Print("Getting date of last processed activity.") files, err := os.ReadDir(folder) if err != nil { logger.Panic(err) } var dates []time.Time for _, file := range files { res, err := os.ReadFile(config_cache.OutputPath + "/" + file.Name()) if err != nil { continue } var act activity err = json.Unmarshal(res, &act) if err != nil { continue } dates = append(dates, act.StartDate) } ret := new(time.Time) for _, date := range dates { if date.After(*ret) { *ret = date } } return ret } // func get_unprocessed_activities(all_acts []activity, processed_ids []json.Number) []activity { // logger.Print("Sort out processed activities out of all activities") // var ret []activity // out: // for _, act := range all_acts { // for _, id := range processed_ids { // if id == act.Id { // continue out // } // } // ret = append(ret, act) // } // return ret // } // func act_add_streams(act *activity, token refresh_token_response) error { // logger.Print("Add stream to activity") // id, err := act.Id.Int64() // if err != nil { // return err // } // url := "https://www.strava.com/api/v3/activities/" + strconv.Itoa(int(id)) + "/streams?keys=time,distance,latlng,altitude,velocity_smooth,heartrate,cadence,watts,temp,moving,grade_smooth" // req, err := http.NewRequest("GET", url, nil) // if err != nil { // return err // } // client := &http.Client{} // value := token.TokenType + " " + token.AccessToken // req.Header.Set("Authorization", value) // resp, err := client.Do(req) // if err != nil { // return err // } // defer resp.Body.Close() // if resp.StatusCode != http.StatusOK { // if resp.StatusCode == 429 { // return error_too_many_req // } // logger.Print(resp.Status) // logger.Panic(resp.StatusCode) // } // body, err := io.ReadAll(resp.Body) // if err != nil { // return err // } // err = json.Unmarshal(body, &act.Streams) // if err != nil { // return err // } // return nil // } func store_activity(act activity, folder string) error { file_name := folder + "/" + act.StartDate.Format("2006-01-02_15:04:05") + ".json" file_name = strings.Replace(file_name, ":", "_", -1) logger.Printf("Store activity %s\n", file_name) f, err := os.Create(file_name) if err != nil { return err } defer f.Close() json, err := json.MarshalIndent(act, "", "\t") if err != nil { return err } _, err = f.Write(json) return err } func main() { flag.StringVar(&config_path, "config-path", "./config/config.json", "Specify path to find the config file. Default is ./config/config.json") flag.StringVar(&access_token_path, "access-token-path", "./config/access_token.json", "Specify path to find the access token. Default is ./data/access_token.json") flag.Parse() read_config() // create output path if not exisisting _, err := os.Stat(config_cache.OutputPath) if err != nil { if os.IsNotExist(err) { logger.Printf("Creating data storage directory: %s\n", config_cache.OutputPath) err = os.MkdirAll(config_cache.OutputPath, os.ModePerm) if err != nil { logger.Panic(err) } } else { logger.Panic(err) } } restart: // processed_ids := get_processed_ids() token, err := get_remote_access_token() if err != nil { logger.Panic(err) } last := get_last_processed_date(config_cache.OutputPath) acts, err := get_remote_activities_since(*token, *last) if err != nil { if err == error_too_many_req { logger.Print(err) logger.Print("waiting for 15 minutes") time.Sleep(time.Minute*15 + time.Second*15) goto restart } else { logger.Panic(err) } } for _, act := range *acts { err = store_activity(act, config_cache.OutputPath) if err != nil { logger.Panic(err) } } // unprocessed_acts := get_unprocessed_activities(*acts, processed_ids) // for _, act := range unprocessed_acts { // redo: // err = act_add_streams(&act, *token) // if err != nil { // if err == error_too_many_req { // logger.Print(err) // logger.Print("waiting for 15 minutes") // time.Sleep(time.Minute*15 + time.Second*15) // goto redo // } else { // logger.Panic(err) // } // } // err = store_activity(act, config_cache.OutputPath) // if err != nil { // logger.Panic(err) // } // } }