Skip to content

Instantly share code, notes, and snippets.

@inouetmhr
Forked from elifkus/Code.gs
Last active January 5, 2020 13:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save inouetmhr/9fb7319d7e50cbb63b0381d64d9faccb to your computer and use it in GitHub Desktop.
Save inouetmhr/9fb7319d7e50cbb63b0381d64d9faccb to your computer and use it in GitHub Desktop.
Google Apps Script to retrieve data from Strava into a Spreadsheet.
// Original: https://gist.github.com/elifkus/09cd63b3cfbf4e070ecc83b4a4358eaa/281be3cc626dafeed767613ee5e3bad078268e38
// TODO; 日単位で集計(加算)して、
/**
* There is a write-up of how to get this code to run. https://elifk.us/en/retrieving-your-strava-data-with-google-app-scripts/
*/
var scriptProp = PropertiesService.getScriptProperties().getProperties();
var CLIENT_ID = scriptProp.STRAVA_CLIENT_ID; //'<ClientId for the Strava App>';
var CLIENT_SECRET = scriptProp.STRAVA_CLIENT_SECRET; //'<Client Secret for the Strava App>';
var SHEET_NAME = scriptProp.SHEET_NAME || "strava_data" ;
var TZ_OFFSET = parseFloat(scriptProp.TZ_OFFSET) || 0 ;
var DEBUG = false;
//If you want to retrieve details such as 'description' you need to make the value of RETRIEVE_DETAILS = true;
var RETRIEVE_DETAILS = false;
var ONLY_WITH_HEARTRATE = false;
//If you want to retrieves only one type of activity, you should write the activity type below. Ex: var FILTER_BY_TYPE = "Ride";
var FILTER_BY_TYPE = "Ride";
//Strava returns a lot of data. You can choose which fields you want to be returned.
//The field names should match the field names returned from getActivities method,
//which you can check http://developers.strava.com/docs/reference/#api-Activities-getLoggedInAthleteActivities on the right
//If you set RETRIEVE_DETAILS to true, you can specify the fields returned
//from http://developers.strava.com/docs/reference/#api-Activities-getActivityById as well. Again check the right part of the page
var DATA_FIELDS = ['start_date_local', 'distance'];//, 'type'];
//This is the sheet heading for the fields above. The number of items should match with DATA_FIELDS
var HEADING_FOR_DATA_FIELDS = ['Date', 'Distance'];//, 'Type'];
//Strava changed its authentication mechanism. The old authentication method will stop working on Oct 15th, 2019
//for old oauth use "view_private", for new one use "activity:read_all" These include reading your private activities.
//If you want to use a different scope or several scopes check https://developers.strava.com/docs/oauth-updates/
//For new scopes, additional scopes can be added with a comma inbetween
//var STRAVA_API_SCOPE = "view_private";
var STRAVA_API_SCOPE = "activity:read_all";
/**
* Configures the service.
*/
function getService() {
return OAuth2.createService('Strava')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://www.strava.com/oauth/authorize')
.setTokenUrl('https://www.strava.com/oauth/token')
// Set the client ID and secret.
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
// Set the name of the callback function that should be invoked to complete
// the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getUserProperties())
//Include private activities when retrieving activities.
.setScope(STRAVA_API_SCOPE)
}
/**
* Handles the OAuth callback.
*/
function authCallback(request) {
var service = getService();
var authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied');
}
}
function logAccessToken() {
var service = getService();
Logger.log("Access token: "+ service.getAccessToken());
}
/**
* Reset the authorization state, so that it can be re-tested.
*/
function reset() {
var service = getService();
service.reset();
}
/**
* Authorizes and makes a request to the API.
*/
function run() {
var service = getService();
if (service.hasAccess()) {
var url = 'https://www.strava.com/api/v3/athlete';
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
function retrieveData() {
//validate input
validateConfig()
//if sheet is empty retrieve all data
var service = getService();
if (service.hasAccess()) {
var sheet = getStravaSheet();
var unixTime = retrieveLastDate(sheet);
var url = 'https://www.strava.com/api/v3/athlete/activities?per_page=100&after=' + unixTime;
//Logger.log('url: %s', url);
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
if (result.length == 0) {
Logger.log("No new data");
return;
}
if (RETRIEVE_DETAILS) {
retrieveAndInsertActivityDetailsForActivityList(result);
}
//var data = convertData(result);
var data = daily_stats(result);
if (data.length == 0) {
Logger.log("No new data with the used filters (ONLY_WITH_HEARTRATE, FILTER_BY_TYPE)");
return;
}
insertData(sheet, data);
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
function validateConfig() {
if (DATA_FIELDS.length != HEADING_FOR_DATA_FIELDS.length) {
Logger.log("The length of DATA_FIELDS does not match with the length of HEADING_FOR_DATA_FIELDS. ");
Logger.log(DATA_FIELDS.length.toString()+" != "+HEADING_FOR_DATA_FIELDS.length.toString() + " Please fix.");
}
}
function retrieveAndInsertActivityDetailsForActivityList(activityListResult) {
var service = getService();
if (service.hasAccess()) {
var url = 'https://www.strava.com/api/v3/activities/';
var query = '?include_all_efforts=false';
for (var i = 0; i < activityListResult.length; i++) {
var activityURL = url + activityListResult[i]['id'] + query;
var response = UrlFetchApp.fetch(activityURL, {
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
extendObj(activityListResult[i], result);
}
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
}
}
function extendObj(obj1, obj2){
for (var key in obj2){
if(!obj1.hasOwnProperty(key)){
obj1[key] = obj2[key];
}
}
}
function retrieveLastDate(sheet) {
var lastRow = sheet.getLastRow();
var unixTime = 0;
if (lastRow > 0) {
var dateCell = sheet.getRange(lastRow, 1);
var date = dateCell.getValue();
//var date = new Date((dateString || "").replace(/-/g,"/").replace(/[TZ]/g," "));
unixTime = date/1000;
}
return unixTime;
}
// aggregate daily distance
function daily_stats(result) {
var data = convertData(result);
/* ES6 not supported on GAS ...
var stats = new Proxy({}, {
get: function(object, property) {
return object.hasOwnProperty(property) ? object[property] : 0;
}
}); */
var stats = {};
data.forEach(function(item){
// this util asumes item to be [date, distance, rest...]
var date = item[0];
var distance = item[1];
var date_unix = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); // round to date from date-time
if (! stats[date_unix]) { stats[date_unix] = 0 };
stats[date_unix] += distance;
});
//Logger.log(stats);
var dates = Object.keys(stats);
var minDate = Math.min.apply(null,dates);
var maxDate = Math.max.apply(null,dates);
var data2 = [];
for (var i = minDate; i <= maxDate ; ) {
//if (! stats[i]) { stats[i] = 0 };
var item = [new Date(i), stats[i]? stats[i]:0 ];
data2.push(item);
var day = new Date(i);
i = day.setDate(day.getDate()+1); //unixtime
}
return data2;
}
function convertData(result) {
var data = [];
for (var i = 0; i < result.length; i++) {
if ((!ONLY_WITH_HEARTRATE || result[i]["has_heartrate"]) && (FILTER_BY_TYPE.length == 0 || result[i]["type"] == FILTER_BY_TYPE)) {
var item = [];
for (var j = 0; j < DATA_FIELDS.length; j++) {
var field_name = DATA_FIELDS[j];
var single_data = result[i][field_name];
//convert date with timezone offset
if (field_name == 'start_date_local') {
var date = new Date(single_data);
date.setHours(date.getHours() + TZ_OFFSET);
single_data = date;
}
//distance is returned in meters, that's why I divide it into 1000 to have kms
if (field_name == 'distance') {
single_data = single_data/1000;
}
// if the data does not exist undefined is returned,
// I replace undefined with '-'
if (single_data == undefined) {
single_data = '-'
}
item.push(single_data);
}
data.push(item);
}
}
return data;
}
function getStravaSheet() {
//var spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = getOrCreateSheet(spreadsheet, SHEET_NAME);
return sheet;
}
function insertData(sheet, data) {
var header = HEADING_FOR_DATA_FIELDS;
ensureHeader(header, sheet);
var lastRow = sheet.getLastRow();
if (lastRow > 0) {
var dateCell = sheet.getRange(lastRow, 1);
var lastdate = dateCell.getValue();
Logger.log(lastdate);
if (lastdate instanceof Date && lastdate.getTime()==data[0][0].getTime()) lastRow -= 1;
}
var range = sheet.getRange(lastRow+1,1,data.length,data[0].length);
range.setValues(data);
}
function ensureHeader(header, sheet) {
// Only add the header if sheet is empty
if (sheet.getLastRow() == 0) {
if (DEBUG) {
Logger.log('Sheet is empty, adding header.');
}
sheet.appendRow(header);
return true;
} else {
if (DEBUG) {
Logger.log('Sheet is not empty, not adding header.')
}
return false;
}
}
function getOrCreateSheet(spreadsheet, sheetName) {
var sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
if (DEBUG) Logger.log('Sheet "%s" does not exist, adding new one.', sheetName);
sheet = spreadsheet.insertSheet(sheetName)
}
return sheet;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment