Initial commit

Signed-off-by: Thomas Klaehn <thomas.klaehn@perinet.io>
This commit is contained in:
Thomas Klaehn
2025-10-02 12:02:43 +00:00
commit 2c2969c48e
5 changed files with 586 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
"name": "Golang",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/go:latest"
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/data
/config

21
go.mod Normal file
View File

@@ -0,0 +1,21 @@
module strava
go 1.23.1
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/client9/misspell v0.3.4 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
github.com/kisielk/errcheck v1.6.1 // indirect
github.com/mdempsky/unconvert v0.0.0-20230125054757-2661c2c99a9b // indirect
github.com/tormoder/fit v0.15.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.4.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.5.0 // indirect
honnef.co/go/tools v0.4.2 // indirect
mvdan.cc/gofumpt v0.4.0 // indirect
)

105
go.sum Normal file
View File

@@ -0,0 +1,105 @@
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/bradfitz/latlong v0.0.0-20170410180902-f3db6d0dff40/go.mod h1:ZcXX9BndVQx6Q/JM6B8x7dLE9sl20S+TQsv4KO7tEQk=
github.com/cespare/xxhash v1.0.0/go.mod h1:fX/lfQBkSCDXZSUgv6jVIu/EVA3/JNseAX5asI4c4T4=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 h1:PVRE9d4AQKmbelZ7emNig1+NT27DUmKZn5qXxfio54U=
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
github.com/jonas-p/go-shp v0.1.1/go.mod h1:MRIhyxDQ6VVp0oYeD7yPGr5RSTNScUFKCDsI5DR7PtI=
github.com/kisielk/errcheck v1.6.1 h1:cErYo+J4SmEjdXZrVXGwLJCE2sB06s23LpkcyWNrT+s=
github.com/kisielk/errcheck v1.6.1/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw=
github.com/kortschak/utter v0.0.0-20180609113506-364ec7d7a8f4/go.mod h1:oDr41C7kH9wvAikWyFhr6UFr8R7nelpmCF5XR5rL7I8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mdempsky/unconvert v0.0.0-20230125054757-2661c2c99a9b h1:jdFI9paVi4E33U9TAExBpKPl1l5MnOn7VOLbb4Mvzzg=
github.com/mdempsky/unconvert v0.0.0-20230125054757-2661c2c99a9b/go.mod h1:mOq/NVYz3H5h7Av88ia14HIMF/UdGXj9dp8P/+b566A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/tealeg/xlsx v1.0.3/go.mod h1:uxu5UY2ovkuRPWKQ8Q7JG0JbSivrISjdPzZQKeo74mA=
github.com/tormoder/fit v0.15.0 h1:oW1dhvGqPIwBJdRJfWzW/jqYU705oBmLcJq4TJO7SqU=
github.com/tormoder/fit v0.15.0/go.mod h1:J+m0+sz5qljhPaP34CgJz8uFD8Vzdsf96D3Hj99DMLQ=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH+OtGT2im5wV1YGGDora5vTv/aa5bE=
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
honnef.co/go/tools v0.4.2 h1:6qXr+R5w+ktL5UkwEbPp+fEvfyoMPche6GkOpGHZcLc=
honnef.co/go/tools v0.4.2/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM=
mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=

435
main.go Normal file
View 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)
// }
// }
}