435
main.go
Normal file
435
main.go
Normal file
@@ -0,0 +1,435 @@
|
||||
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)
|
||||
// }
|
||||
// }
|
||||
}
|
Reference in New Issue
Block a user