SHARE
TWEET

gc-cycling

a guest Jun 18th, 2019 97 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3.  
  4. """
  5. Original credits:
  6.  
  7. File: gcexport.py
  8. Original author: Kyle Krafka (https://github.com/kjkjava/)
  9. Date: April 28, 2015
  10. Fork author: Michael P (https://github.com/moderation/)
  11. Date: February 15, 2018
  12.  
  13. Description:    Use this script to export your fitness data from Garmin Connect.
  14.                See README.md for more information.
  15.  
  16. Activity & event types:
  17.    https://connect.garmin.com/modern/main/js/properties/event_types/event_types.properties
  18.    https://connect.garmin.com/modern/main/js/properties/activity_types/activity_types.properties
  19. """
  20.  
  21. def show_exception_and_exit(exc_type, exc_value, tb):
  22.     import traceback
  23.     traceback.print_exception(exc_type, exc_value, tb)
  24.     input("Press ENTER to exit.")
  25.     sys.exit(-1)
  26.  
  27. import sys
  28. sys.excepthook = show_exception_and_exit
  29.  
  30. # ##############################################
  31.  
  32. from datetime import datetime, timedelta
  33. from getpass import getpass
  34. from os import mkdir, remove, stat
  35. from os.path import isdir, isfile
  36. from subprocess import call
  37. from sys import argv
  38. from xml.dom.minidom import parseString
  39.  
  40. import argparse
  41. import http.cookiejar
  42. import json
  43. import re
  44. import urllib.error
  45. import urllib.parse
  46. import urllib.request
  47. import zipfile
  48.  
  49. SCRIPT_VERSION = '0.0.1'
  50. #CURRENT_DATE = datetime.now().strftime('%Y-%m-%d')
  51. #ACTIVITIES_DIRECTORY = './' + CURRENT_DATE + '_garmin_connect_export'
  52.  
  53. PARSER = argparse.ArgumentParser()
  54.  
  55. # TODO: Implement verbose and/or quiet options.
  56. # PARSER.add_argument('-v', '--verbose', help="increase output verbosity", action="store_true")
  57. PARSER.add_argument('--version', help="print version and exit", action="store_true")
  58. PARSER.add_argument('--username', help="your Garmin Connect username (otherwise, you will be \
  59.    prompted)", nargs='?')
  60. PARSER.add_argument('--password', help="your Garmin Connect password (otherwise, you will be \
  61.    prompted)", nargs='?')
  62.  
  63. PARSER.add_argument('--startdate', help="the date of the first activity to set to private (e.g. 2018-09-30) (otherwise, you will be \
  64.    prompted)", nargs='?')
  65. PARSER.add_argument('--enddate', help="the date of the last activity to set to private (e.g. 2018-10-30) (otherwise, you will be \
  66.    prompted)", nargs='?')
  67.  
  68. PARSER.add_argument('--privacy', help="public, private, subscribers, groups", nargs='?')
  69.  
  70.    
  71. ARGS = PARSER.parse_args()
  72.  
  73. if ARGS.version:
  74.     print(argv[0] + ", version " + SCRIPT_VERSION)
  75.     exit(0)
  76.  
  77. COOKIE_JAR = http.cookiejar.CookieJar()
  78. OPENER = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(COOKIE_JAR))
  79. # print(COOKIE_JAR)
  80.  
  81.  
  82. # url is a string, post is the raw post data, headers is a dictionary of headers.
  83. def http_req(url, post=None, headers=None):
  84.     """Helper function that makes the HTTP requests."""
  85.     request = urllib.request.Request(url)
  86.     # Tell Garmin we're some supported browser.
  87.     request.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, \
  88.        like Gecko) Chrome/54.0.2816.0 Safari/537.36')
  89.     if headers:
  90.         for header_key, header_value in headers.items():
  91.             request.add_header(header_key, header_value)
  92.     if post:
  93.         #post = urllib.parse.urlencode(post)
  94.         post = post.encode('utf-8')  # Convert dictionary to POST parameter string.
  95.     # print("request.headers: " + str(request.headers) + " COOKIE_JAR: " + str(COOKIE_JAR))
  96.     # print("post: " + str(post) + "request: " + str(request))
  97.     response = OPENER.open((request), data=post)
  98.  
  99.     if response.getcode() == 204:
  100.         # For activities without GPS coordinates, there is no GPX download (204 = no content).
  101.         # Write an empty file to prevent redownloading it.
  102.         #print('Writing empty file since there was no GPX activity data...')
  103.         return ''
  104.     elif response.getcode() != 200:
  105.         raise Exception('Bad return code (' + str(response.getcode()) + ') for: ' + url)
  106.     # print(response.getcode())
  107.  
  108.     return response.read()
  109.  
  110. print('Welcome to the Garmin Connect Activity Type Tool (cycling version)!')
  111. print('  Currently this tool supports changing your activity type to cycling')
  112. print('')
  113. USERNAME=''
  114. PASSWORD=''
  115. while not USERNAME:
  116.     USERNAME = ARGS.username if ARGS.username else input('Username: ')
  117.     if not USERNAME:
  118.         print("Please enter a username.")
  119.         print("")
  120. while not PASSWORD:
  121.     PASSWORD = ARGS.password if ARGS.password else getpass()
  122.     if not PASSWORD:
  123.         print("Please enter a password.")
  124.         print("")
  125.  
  126. # Maximum # of activities you can search for at once (URL_GC_LIST)
  127. LIMIT_ACTIVITY_LIST = 9999
  128.  
  129. print('Select Activities')
  130. print('  Up to ' + str(LIMIT_ACTIVITY_LIST) + ' activities can be processed at one time.')
  131. print('  Leave the start date blank to start at the beginning.')
  132. print('  Leave the end date blank to end at the latest activity.')
  133. print("")
  134.  
  135. def promptDate(prompt, defaultValue, errorStr):
  136.     while True:
  137.         DATE = defaultValue if defaultValue else input(prompt)
  138.         DATE = DATE.strip()
  139.         if not DATE:
  140.             break;
  141.         try:
  142.             datetime.strptime(DATE, '%Y-%m-%d')
  143.         except ValueError:
  144.             #raise ValueError("Incorrect data format, should be YYYY-MM-DD")
  145.             print(errorStr)
  146.             print("")
  147.             continue
  148.         break
  149.     return DATE
  150.  
  151. STARTDATE = promptDate('  Start Date (e.g. "2018-09-30" or blank): ', ARGS.startdate, "  Invalid date.")
  152. ENDDATE = promptDate('  End Date (e.g. "2018-10-30" or blank): ', ARGS.enddate, "  Invalid date.")
  153. print('')
  154.  
  155. # Maximum number of activities you can request at once.  Set and enforced by Garmin.
  156. LIMIT_MAXIMUM = 1000
  157.  
  158. WEBHOST = "https://connect.garmin.com"
  159. REDIRECT = "https://connect.garmin.com/post-auth/login"
  160. BASE_URL = "http://connect.garmin.com/en-US/signin"
  161. SSO = "https://sso.garmin.com/sso"
  162. CSS = "https://static.garmincdn.com/com.garmin.connect/ui/css/gauth-custom-v1.2-min.css"
  163.  
  164. DATA = {
  165.     'service': REDIRECT,
  166.     'webhost': WEBHOST,
  167.     'source': BASE_URL,
  168.     'redirectAfterAccountLoginUrl': REDIRECT,
  169.     'redirectAfterAccountCreationUrl': REDIRECT,
  170.     'gauthHost': SSO,
  171.     'locale': 'en_US',
  172.     'id': 'gauth-widget',
  173.     'cssUrl': CSS,
  174.     'clientId': 'GarminConnect',
  175.     'rememberMeShown': 'true',
  176.     'rememberMeChecked': 'false',
  177.     'createAccountShown': 'true',
  178.     'openCreateAccount': 'false',
  179.     'usernameShown': 'false',
  180.     'displayNameShown': 'false',
  181.     'consumeServiceTicket': 'false',
  182.     'initialFocus': 'true',
  183.     'embedWidget': 'false',
  184.     'generateExtraServiceTicket': 'false'
  185.     }
  186.  
  187. #print(urllib.parse.urlencode(DATA))
  188.  
  189. # URLs for various services.
  190. URL_GC_LOGIN = 'https://sso.garmin.com/sso/login?' + urllib.parse.urlencode(DATA)
  191. URL_GC_POST_AUTH = 'https://connect.garmin.com/modern/activities?'
  192. URL_GC_SEARCH = 'https://connect.garmin.com/proxy/activity-search-service-1.2/json/activities?'
  193. URL_GC_LIST = \
  194.     'https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?'
  195. URL_GC_ACTIVITY = 'https://connect.garmin.com/modern/proxy/activity-service/activity/'
  196. URL_GC_ACTIVITY_DETAIL = \
  197.     'https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/'
  198. URL_GC_GPX_ACTIVITY = \
  199.     'https://connect.garmin.com/modern/proxy/download-service/export/gpx/activity/'
  200. URL_GC_TCX_ACTIVITY = \
  201.     'https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/'
  202. URL_GC_ORIGINAL_ACTIVITY = 'http://connect.garmin.com/proxy/download-service/files/activity/'
  203.  
  204. URL_GC_ACTIVITY_PAGE = 'https://connect.garmin.com/modern/activity/'
  205.  
  206. print("Logging in...")
  207.  
  208. # Initially, we need to get a valid session cookie, so we pull the login page.
  209. #print('Request login page')
  210. http_req(URL_GC_LOGIN)
  211. #print('Finish login page')
  212.  
  213. # Now we'll actually login.
  214. # Fields that are passed in a typical Garmin login.
  215. POST_DATA = {
  216.     'username': USERNAME,
  217.     'password': PASSWORD,
  218.     'embed': 'true',
  219.     'lt': 'e1s1',
  220.     '_eventId': 'submit',
  221.     'displayNameRequired': 'false'
  222.     }
  223.  
  224. #print('Post login data')
  225. LOGIN_RESPONSE = http_req(URL_GC_LOGIN, urllib.parse.urlencode(POST_DATA)).decode()
  226. #print('Finish login post')
  227.  
  228. # extract the ticket from the login response
  229. PATTERN = re.compile(r".*\?ticket=([-\w]+)\";.*", re.MULTILINE|re.DOTALL)
  230. MATCH = PATTERN.match(LOGIN_RESPONSE)
  231. if not MATCH:
  232.     raise Exception('Did not get a ticket in the login response. Cannot log in. Did \
  233. you enter the correct username and password?')
  234. LOGIN_TICKET = MATCH.group(1)
  235. #print('login ticket=' + LOGIN_TICKET)
  236.  
  237. #print("Request authentication URL: " + URL_GC_POST_AUTH + 'ticket=' + LOGIN_TICKET)
  238. #print("Request authentication")
  239. LOGIN_AUTH_REP = http_req(URL_GC_POST_AUTH + 'ticket=' + LOGIN_TICKET).decode()
  240. #print(LOGIN_AUTH_REP)
  241. #print('Finished authentication')
  242.  
  243.  
  244. SEARCH_PARAMS = {'start': 0, 'limit': LIMIT_ACTIVITY_LIST}
  245.  
  246. if STARTDATE:
  247.     SEARCH_PARAMS['startDate'] = STARTDATE
  248. if ENDDATE:
  249.     SEARCH_PARAMS['endDate'] = ENDDATE
  250.  
  251. ACLISTURL = URL_GC_LIST + urllib.parse.urlencode(SEARCH_PARAMS)
  252. #print("Activity list URL: " + ACLISTURL)
  253.  
  254. print('')
  255. print('Searching for activities (this might take a while)...')
  256. print('')
  257. #print('Loading activity list')
  258. ACTIVITY_LIST = http_req(ACLISTURL)
  259.  
  260. #print('Processing activity list')
  261. #print('')
  262.  
  263. JSON_LIST = json.loads(ACTIVITY_LIST)
  264.  
  265. if len(JSON_LIST) == 0:
  266.     print("No activities found for the given date range.")
  267. else:
  268.     print("Found " + str(len(JSON_LIST)) + " activities.")
  269.  
  270. for a in JSON_LIST:
  271.     print('Activity: ' + a['startTimeLocal'] + (' | ' + a['activityName'] if a['activityName'] else ''))
  272.  
  273.     print('  Setting activity type to running')
  274.     ACTIVITY_URL = URL_GC_ACTIVITY + str(a['activityId'])
  275.     ACTIVITY_PAGE_URL = URL_GC_ACTIVITY_PAGE + str(a['activityId'])
  276.     ACTIVITY_POST_DATA = {'activityTypeDTO': {'typeId': 2, 'typeKey': 'cycling', 'parentTypeId': 17}, 'activityId': a['activityId'] }
  277.     ACTIVITY_POST_HEADERS = {
  278.         'referer': ACTIVITY_PAGE_URL,
  279.         'authority': 'connect.garmin.com',
  280.         'origin': 'https://connect.garmin.com',
  281.         'Content-Type':'application/json',
  282.         'X-HTTP-Method-Override': 'PUT',
  283.         'X-Requested-With': 'XMLHttpRequest',
  284.         'nk': 'NT'
  285.     }
  286.     http_req(ACTIVITY_URL, json.dumps(ACTIVITY_POST_DATA), ACTIVITY_POST_HEADERS)
  287.  
  288. print('')
  289. print('Done!')
  290.  
  291. input('Press ENTER to quit');
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top