23
.devcontainer/devcontainer.json
Normal file
23
.devcontainer/devcontainer.json
Normal 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
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/data
|
||||
/config
|
21
go.mod
Normal file
21
go.mod
Normal 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
105
go.sum
Normal 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
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