STRAVA - RUNNING LOG
1. OVERVIEW
I've been using Strava for few years now and simultaneously I've been updating my running log on Google Sheets. I treat Google Sheets as my backup (you never know when your data disappear from Strava) and it's much more convenient to browser my activities using GS than Strava. Moreover when you have your own running log, you there is much more freedom in terms of what data you need and want to see. So to avoid double work I've written a script that uses Strava and Google Sheets APIs to transfer all data I want to my running log.
2. Prerequisites
The first step is to create a running log Google Sheet template. You can do it by accessing
this link.
Before we will be able to use the script, we have to set up two APIs - Google Sheets and Strava. How to do it?
- In regards of Google Sheets, I highly recommend you to follow this tutorial → "Working with Google Sheets" by Al Sweigart which is a part of free ebook "Automate The Boring Stuff" → Automate The Boring Stuff, chapter 14.
-
To set up Strava API, you have to visit Strava Settings
and create your API application. As a Authorization Callback Domain please set localhost.
Once your API application is created, please install all required modules provided in the requirements file, which can be found on my GitHub. When the modules are installed and APIs are set up, you can proceed with the code:Show/Hide code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
import gspread, requests, urllib3, time, os from oauth2client.service_account import ServiceAccountCredentials from pace_calulator import calculator urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) number = input("How many last activities would you like to update? ") scopes = [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive' ] path = os.getcwd() credentials_path = os.path.join(path, "credentials.json") creds = ServiceAccountCredentials.from_json_keyfile_name(credentials_path, scopes = scopes) file = gspread.authorize(creds) workbook = file.open_by_key('xxxxxWorkBookKeyxxxxx') sheet = workbook.worksheet('2023') auth_url = "https://www.strava.com/oauth/token" activites_url = "https://www.strava.com/api/v3/athlete/activities" payload = { 'client_id': "97763", 'client_secret': 'XXXXXXXXXXXXXXXXXXX', 'refresh_token': 'XXXXXXXXXXXXXXXXXXX', 'grant_type': "refresh_token", 'f': 'json' } print("Connecting to Strava...") res = requests.post(auth_url, data=payload, verify=False) access_token = res.json()['access_token'] print("Access Token = {}.\n" "Access granted.\n".format(access_token)) header_all = {'Authorization': 'Bearer ' + access_token} param_all = {'per_page': number, 'page': 1} print(f"Validating last {number} activities on Strava... ")
There are few lines which you have to adjust with your data:workbook = file.open_by_key('xxxxxWorkBookKeyxxxxx')
andsheet = workbook.worksheet ('2023')
(lines 15 and 16) have to be filled in with your workbook key and worksheet name;'client_id': "97763"
,'client_secret': 'XXXXXXXXXXXXXXXXXXX'
and'refresh_token': 'XXXXXXXXXXXXXXXXXXX'
have to be filled with respective data form your Strava API application.
The snippet presented above is responsible for API connection and getting a number (represented by a variablenumber
) of running activities to store it in a listall_activities
.
We are also importingpace_calculator
function fromcalculator
module. It computes the pace from m/s to min/km. Calculator's code can be found below:Show/Hide code
1 2 3 4 5 6 7
def calculator(distance, total_time): total_seconds = (int(total_time)) seconds_per_km = float(total_seconds) / (float(distance)/1000) minutes_per_km = int(seconds_per_km / 60) seconds_remainder = int(seconds_per_km - (minutes_per_km * 60)) return '{}:{:0=2d} min/km'.format(minutes_per_km, seconds_remainder)
3. CODE
Let's have a look at the rest of the code.Show/Hide code
1 2 3 4 5 | activities = [] for activity in all_activities: get_activity_url = "https://www.strava.com/api/v3/activities/{}".format(activity['id']) my_activity = requests.get(get_activity_url, headers=header).json() activities.append(my_activity) |
activities
. Then we loop through all running activities stored in all_activities
(which was created in a step Prerequisites), we retrieve a link to each activity and then we store each activity (which
is basically a dictionary) in the activities
list (so we got a list of dictionaries).
Once we have all needed data stored in activities
, we can proceed with sending it to Google Sheets.
We start with creating a list all_dates
and we fill it with all dates which are in the first column
of our Google Sheets template. Then we go through every activity stored in the activities
list, as every activity is
a dictionary, we access all of the needed pieces of data and assign it to appropriate Google Sheets cell. Depending on the
name of the activity, we assign a type of a training to it. When an activity is updated in Google Sheets successfully,
we send a message to the user.Show/Hide code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
all_dates = sheet.col_values(1)
for activity in activities:
if activity["start_date_local"][:10] in all_dates:
activity_date = activity["start_date_local"][:10]
date = sheet.find(activity_date)
activity_name = activity['name']
print(activity_name, 'at', activity_date, 'found! \nUpdating...')
sheet.update_cell(date.row, date.col+1, activity['name'])
time.sleep(1)
sheet.update_cell(date.row, date.col+2, activity['distance'])
time.sleep(1)
sheet.update_cell(date.row, date.col+3, time.strftime("%H:%M:%S", time.gmtime(activity['moving_time'])))
time.sleep(1)
sheet.update_cell(date.row, date.col+4, calculator(activity['distance'],activity['moving_time']))
time.sleep(1)
sheet.update_cell(date.row, date.col + 6, activity['total_elevation_gain'])
time.sleep(1)
sheet.update_cell(date.row, date.col + 7, activity['description'])
time.sleep(3)
if activity_name.startswith('Rozbieganie na bieżni'):
sheet.update_cell(date.row, date.col + 8, "rozbieganie (%)")
elif activity_name.startswith('Rozbieganie'):
sheet.update_cell(date.row, date.col + 8, "rozbieganie")
elif activity_name.startswith('Siła biegowa - podbiegi'):
sheet.update_cell(date.row, date.col + 8, "siła (podbiegi)")
elif activity_name.startswith('Siła biegowa - skip'):
sheet.update_cell(date.row, date.col + 8, "siła (skip)")
elif activity_name.startswith('Siła biegowa - VK'):
sheet.update_cell(date.row, date.col + 8, "siła (VK)")
elif activity_name.startswith('Długie wybieganie'):
sheet.update_cell(date.row, date.col + 8, "długie wybieganie")
elif activity_name.startswith('Wycieczka biegowa'):
sheet.update_cell(date.row, date.col + 8, "wycieczka biegowa")
elif activity_name.startswith('Rozbieganie na bieżni'):
sheet.update_cell(date.row, date.col + 8, "rozbieganie (%)")
elif activity_name.startswith('Zabawa biegowa'):
sheet.update_cell(date.row, date.col + 8, activity_name[16:])
elif activity_name.startswith('Drugi zakres na bieżni'):
sheet.update_cell(date.row, date.col + 8, "drugi zakres (%)")
elif activity_name.startswith('Drugi zakres -'):
sheet.update_cell(date.row, date.col + 8, "drugi zakres")
elif "x400 m" in activity_name:
sheet.update_cell(date.row, date.col + 8, "400 m")
elif "x800 m" in activity_name:
sheet.update_cell(date.row, date.col + 8, "800 m")
elif "x1 km" in activity_name:
sheet.update_cell(date.row, date.col + 8, "1 km")
elif "x200 m" in activity_name:
sheet.update_cell(date.row, date.col + 8, "1 km")
if activity['has_heartrate'] == True or False:
sheet.update_cell(date.row, date.col + 5,
f"{int(activity['average_heartrate'])} ({int(activity['max_heartrate'])})")
print(activity['name'], 'updated!\n')
When all activities have been updated, scripts looks for empty Activity name cells, because it means it was a rest day.
When an empty cell is found, the row is being merged and formatted as a rest day. When the script is done, it prints out
a summary, how many training/rest days have been updated.
Show/Hide code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
all_activities_done = sheet.col_values(2)
rest_days = 0
print("\nUpdating rest days...\n")
for index, val in enumerate(all_activities_done, start=1):
if val == "":
rest_days += 1
sheet.merge_cells(f"B{index}:I{index}", merge_type='MERGE_ALL')
time.sleep(1)
sheet.update_cell(index, 2, 'WOLNE')
time.sleep(1)
sheet.format(f"B{index}:I{index}",{
"backgroundColor": {
"red": 0.65,
"green": 0.65,
"blue": 0.65
},
"horizontalAlignment": "CENTER",
})
print("---- UPDATE COMPLETED ----")
print(f'{len(activities)} training day(s) updated!')
print(rest_days, 'rest day(s) updated!')
4. Complete code
Complete code can be found below and on my GitHub and below.
Show/hide complete code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#!/bin/bash
"""
contact - [email protected]
Strava - https://www.strava.com/athletes/23952867
version history
1.0 - 26/12/2022
1.1 - 26/02/2023 - added
________________________
Script uses Strava API to gather your running activities and sends them to Google Sheet running journal (template can
be found here -> https://docs.google.com/spreadsheets/d/1j7l_FCBkn2b91tRHDA_iI0bXczwVnMg0p0RmMQwrH4I/copy.
Script consists of two files. Script anc pace_calculator.py. Calculator is necessary to calculate the running pace.
Provide script with an integer - how many last activities (non-running activities included) would you like to analyse
and paste to the Google Sheet. Then script separates non-running activities from running activities and pastes your
running data to Google sheet. The last step is to update the rest days. Script updates all rows with DATE cell empty.
TODO list:
1. Instead of "How many last activities would you like to update", user should be able to choose the starting date.
"""
#!/bin/bash
import gspread, requests, urllib3, time, os
from oauth2client.service_account import ServiceAccountCredentials
from pace_calulator import calculator
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
number = input("How many last activities would you like to update? ")
scopes = [
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive'
]
path = os.getcwd()
credentials_path = os.path.join(path, "credentials.json")
creds = ServiceAccountCredentials.from_json_keyfile_name(credentials_path, scopes = scopes)
file = gspread.authorize(creds)
workbook = file.open_by_key('xxxxxWorkBookKeyxxxxx')
sheet = workbook.worksheet('2023')
auth_url = "https://www.strava.com/oauth/token"
activites_url = "https://www.strava.com/api/v3/athlete/activities"
payload = {
'client_id': "97763",
'client_secret': 'XXXXXXXXXXXXXXXXXXX',
'refresh_token': 'XXXXXXXXXXXXXXXXXXX',
'grant_type': "refresh_token",
'f': 'json'
}
print("Connecting to Strava...")
res = requests.post(auth_url, data=payload, verify=False)
access_token = res.json()['access_token']
print("Access Token = {}.\n"
"Access granted.\n".format(access_token))
header_all = {'Authorization': 'Bearer ' + access_token}
param_all = {'per_page': number, 'page': 1}
print(f"Validating last {number} activities on Strava... ")
all_activities = []
my_dataset = requests.get(activites_url, headers=header_all, params=param_all).json()
for item in reversed(my_dataset):
if item["sport_type"] == "Run":
all_activities.append(item)
print(f"Found {len(all_activities)} running activities!")
res = requests.post(auth_url, data=payload, verify=False) # auth_url & payload referenced in code already above
access_token = res.json()['access_token']
header = {'Authorization': 'Bearer ' + access_token}
activities = []
for activity in all_activities:
get_activity_url = "https://www.strava.com/api/v3/activities/{}".format(activity['id'])
my_activity = requests.get(get_activity_url, headers=header).json()
activities.append(my_activity)
all_dates = sheet.col_values(1)
for activity in activities:
if activity["start_date_local"][:10] in all_dates:
activity_date = activity["start_date_local"][:10]
date = sheet.find(activity_date)
activity_name = activity['name']
print(activity_name, 'on', activity_date, 'found! \nUpdating...')
sheet.update_cell(date.row, date.col+1, activity['name'])
time.sleep(1)
sheet.update_cell(date.row, date.col+2, activity['distance'])
sheet.update_cell(date.row, date.col+3, time.strftime("%H:%M:%S", time.gmtime(activity['moving_time'])))
time.sleep(1)
sheet.update_cell(date.row, date.col+4, calculator(activity['distance'],activity['moving_time']))
sheet.update_cell(date.row, date.col + 6, activity['total_elevation_gain'])
sheet.update_cell(date.row, date.col + 7, activity['description'])
time.sleep(1)
if activity_name.startswith('Rozbieganie na bieżni'):
sheet.update_cell(date.row, date.col + 8, "rozbieganie (%)")
elif activity_name.startswith('Rozbieganie'):
sheet.update_cell(date.row, date.col + 8, "rozbieganie")
elif activity_name.startswith('Siła biegowa - podbiegi'):
sheet.update_cell(date.row, date.col + 8, "siła (podbiegi)")
elif activity_name.startswith('Siła biegowa - skip'):
sheet.update_cell(date.row, date.col + 8, "siła (skip)")
elif activity_name.startswith('Siła biegowa - VK'):
sheet.update_cell(date.row, date.col + 8, "siła (VK)")
elif activity_name.startswith('Długie wybieganie'):
sheet.update_cell(date.row, date.col + 8, "długie wybieganie")
elif activity_name.startswith('Wycieczka biegowa'):
sheet.update_cell(date.row, date.col + 8, "wycieczka biegowa")
elif activity_name.startswith('Rozbieganie na bieżni'):
sheet.update_cell(date.row, date.col + 8, "rozbieganie (%)")
elif activity_name.startswith('Zabawa biegowa'):
sheet.update_cell(date.row, date.col + 8, activity_name[16:])
elif activity_name.startswith('Drugi zakres na bieżni'):
sheet.update_cell(date.row, date.col + 8, "drugi zakres (%)")
elif activity_name.startswith('Drugi zakres -'):
sheet.update_cell(date.row, date.col + 8, "drugi zakres")
elif "x400 m" in activity_name:
sheet.update_cell(date.row, date.col + 8, "400 m")
elif "x800 m" in activity_name:
sheet.update_cell(date.row, date.col + 8, "800 m")
elif "x1 km" in activity_name:
sheet.update_cell(date.row, date.col + 8, "1 km")
elif "x200 m" in activity_name:
sheet.update_cell(date.row, date.col + 8, "1 km")
if activity['has_heartrate'] == True or False:
sheet.update_cell(date.row, date.col + 5,
f"{int(activity['average_heartrate'])} ({int(activity['max_heartrate'])})")
print(activity['name'], 'updated!\n')
all_activities_done = sheet.col_values(2)
rest_days = 0
print("\nUpdating rest days...\n")
for index, val in enumerate(all_activities_done, start=1):
if val == "":
rest_days += 1
sheet.merge_cells(f"B{index}:I{index}", merge_type='MERGE_ALL')
time.sleep(1)
sheet.update_cell(index, 2, 'WOLNE')
time.sleep(1)
sheet.format(f"B{index}:I{index}",{
"backgroundColor": {
"red": 0.65,
"green": 0.65,
"blue": 0.65
},
"horizontalAlignment": "CENTER",
})
print("---- UPDATE COMPLETED ----")
print(f'{len(activities)} training day(s) updated!')
print(rest_days, 'rest day(s) updated!')
5. Demo
6. Version history
version 1.0 - 26/12/2022
version 1.1 - 26/02/2023 - rest day detection added
version 1.2 - 27/02/2023 - rest day counter added