From 1512d45ce6646f1833633a5a33eb1bac88850aaa Mon Sep 17 00:00:00 2001 From: Thomas Klaehn Date: Thu, 2 Oct 2025 12:02:43 +0000 Subject: [PATCH] Initial commit Signed-off-by: Thomas Klaehn --- .devcontainer/devcontainer.json | 23 ++ .gitignore | 2 + Readme.md | 12 + go.mod | 21 ++ go.sum | 105 ++++++++ main.go | 435 ++++++++++++++++++++++++++++++++ 6 files changed, 598 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .gitignore create mode 100644 Readme.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..eb1f245 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb45f77 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/data +/config diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..7a70c83 --- /dev/null +++ b/Readme.md @@ -0,0 +1,12 @@ +client_id: 69554 +client_secret: 0173554c069b832f55d1851ce9d4fe92ded38dca +access_token: da7afdf787c27b7a63b6245bdaa6adc2bedf240b +refresh_token: 568882a67f77a4635712f294a4b25ecbbf057310 + +{ + "token_type":"Bearer", + "access_token":"da7afdf787c27b7a63b6245bdaa6adc2bedf240b", + "expires_at":1728568428, + "expires_in":19456, + "refresh_token":"568882a67f77a4635712f294a4b25ecbbf057310" +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..13ecb49 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0e46065 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..3d97424 --- /dev/null +++ b/main.go @@ -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) + // } + // } +}