Advertisement
Guest User

gc-geartime.py

a guest
Mar 21st, 2019
139
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.35 KB | None | 0 0
  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('--gearid', nargs='?')
  64.  
  65.    
  66. ARGS = PARSER.parse_args()
  67.  
  68. if ARGS.version:
  69.     print(argv[0] + ", version " + SCRIPT_VERSION)
  70.     exit(0)
  71.  
  72. COOKIE_JAR = http.cookiejar.CookieJar()
  73. OPENER = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(COOKIE_JAR))
  74. # print(COOKIE_JAR)
  75.  
  76.  
  77. # url is a string, post is the raw post data, headers is a dictionary of headers.
  78. def http_req(url, post=None, headers=None):
  79.     """Helper function that makes the HTTP requests."""
  80.     request = urllib.request.Request(url)
  81.     # Tell Garmin we're some supported browser.
  82.     request.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, \
  83.        like Gecko) Chrome/54.0.2816.0 Safari/537.36')
  84.     if headers:
  85.         for header_key, header_value in headers.items():
  86.             request.add_header(header_key, header_value)
  87.     if post:
  88.         #post = urllib.parse.urlencode(post)
  89.         post = post.encode('utf-8')  # Convert dictionary to POST parameter string.
  90.     # print("request.headers: " + str(request.headers) + " COOKIE_JAR: " + str(COOKIE_JAR))
  91.     # print("post: " + str(post) + "request: " + str(request))
  92.     response = OPENER.open((request), data=post)
  93.  
  94.     if response.getcode() == 204:
  95.         # For activities without GPS coordinates, there is no GPX download (204 = no content).
  96.         # Write an empty file to prevent redownloading it.
  97.         #print('Writing empty file since there was no GPX activity data...')
  98.         return ''
  99.     elif response.getcode() != 200:
  100.         raise Exception('Bad return code (' + str(response.getcode()) + ') for: ' + url)
  101.     # print(response.getcode())
  102.  
  103.     return response.read()
  104.  
  105. print('Welcome to the Garmin Connect Gear Time Tool!')
  106. print('')
  107. USERNAME=''
  108. PASSWORD=''
  109. while not USERNAME:
  110.     USERNAME = ARGS.username if ARGS.username else input('Username: ')
  111.     if not USERNAME:
  112.         print("Please enter a username.")
  113.         print("")
  114. while not PASSWORD:
  115.     PASSWORD = ARGS.password if ARGS.password else getpass()
  116.     if not PASSWORD:
  117.         print("Please enter a password.")
  118.         print("")
  119.  
  120. print('')
  121. print('Gear ID')
  122. print('  To find your gear ID: ')
  123. print('  1) Login to Garmin Connect website')
  124. print('  2) Navigate to https://connect.garmin.com/modern/gear')
  125. print('  3) Click on the down arrow beside gear (unretire first if necessary)')
  126. print('  4) Click "Edit"')
  127. print('  5) Look at the resulting URL: e.g.')
  128. print('  https://connect.garmin.com/modern/gear/11064e7122c34042921a435469b9e9ab/edit')
  129. print('  6) Copy the string of letters and numbers between "gear/" and "/edit". e.g.')
  130. print('  11064e7122c34042921a435469b9e9ab');
  131.  
  132. print ('')
  133.  
  134.  
  135. GEARID=''
  136. while not GEARID:
  137.     GEARID = ARGS.gearid if ARGS.gearid else input("  Enter Gear ID: ")
  138.     if not USERNAME:
  139.         print("Please enter a Gear ID.")
  140.         print("")
  141.  
  142. # Maximum # of activities you can search for at once (URL_GC_LIST)
  143. LIMIT_ACTIVITY_LIST = 9999
  144.  
  145. #print('Select Activities')
  146. #print('  Up to ' + str(LIMIT_ACTIVITY_LIST) + ' activities can be processed at one time.')
  147. #print('  Leave the start date blank to start at the beginning.')
  148. #print('  Leave the end date blank to end at the latest activity.')
  149. #print("")
  150.  
  151. def promptDate(prompt, defaultValue, errorStr):
  152.     while True:
  153.         DATE = defaultValue if defaultValue else input(prompt)
  154.         DATE = DATE.strip()
  155.         if not DATE:
  156.             break;
  157.         try:
  158.             datetime.strptime(DATE, '%Y-%m-%d')
  159.         except ValueError:
  160.             #raise ValueError("Incorrect data format, should be YYYY-MM-DD")
  161.             print(errorStr)
  162.             print("")
  163.             continue
  164.         break
  165.     return DATE
  166.  
  167. #STARTDATE = promptDate('  Start Date (e.g. "2018-09-30" or blank): ', ARGS.startdate, "  Invalid date.")
  168. #ENDDATE = promptDate('  End Date (e.g. "2018-10-30" or blank): ', ARGS.enddate, "  Invalid date.")
  169. #print('')
  170.  
  171. # Maximum number of activities you can request at once.  Set and enforced by Garmin.
  172. LIMIT_MAXIMUM = 1000
  173.  
  174. WEBHOST = "https://connect.garmin.com"
  175. REDIRECT = "https://connect.garmin.com/post-auth/login"
  176. BASE_URL = "http://connect.garmin.com/en-US/signin"
  177. SSO = "https://sso.garmin.com/sso"
  178. CSS = "https://static.garmincdn.com/com.garmin.connect/ui/css/gauth-custom-v1.2-min.css"
  179.  
  180. DATA = {
  181.     'service': REDIRECT,
  182.     'webhost': WEBHOST,
  183.     'source': BASE_URL,
  184.     'redirectAfterAccountLoginUrl': REDIRECT,
  185.     'redirectAfterAccountCreationUrl': REDIRECT,
  186.     'gauthHost': SSO,
  187.     'locale': 'en_US',
  188.     'id': 'gauth-widget',
  189.     'cssUrl': CSS,
  190.     'clientId': 'GarminConnect',
  191.     'rememberMeShown': 'true',
  192.     'rememberMeChecked': 'false',
  193.     'createAccountShown': 'true',
  194.     'openCreateAccount': 'false',
  195.     'usernameShown': 'false',
  196.     'displayNameShown': 'false',
  197.     'consumeServiceTicket': 'false',
  198.     'initialFocus': 'true',
  199.     'embedWidget': 'false',
  200.     'generateExtraServiceTicket': 'false'
  201.     }
  202.  
  203. #print(urllib.parse.urlencode(DATA))
  204.  
  205. # URLs for various services.
  206. URL_GC_LOGIN = 'https://sso.garmin.com/sso/login?' + urllib.parse.urlencode(DATA)
  207. URL_GC_POST_AUTH = 'https://connect.garmin.com/modern/activities?'
  208. URL_GC_SEARCH = 'https://connect.garmin.com/proxy/activity-search-service-1.2/json/activities?'
  209. URL_GC_LIST = \
  210.     'https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?'
  211. URL_GC_ACTIVITY = 'https://connect.garmin.com/modern/proxy/activity-service/activity/'
  212. URL_GC_ACTIVITY_DETAIL = \
  213.     'https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/'
  214. URL_GC_GPX_ACTIVITY = \
  215.     'https://connect.garmin.com/modern/proxy/download-service/export/gpx/activity/'
  216. URL_GC_TCX_ACTIVITY = \
  217.     'https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/'
  218. URL_GC_ORIGINAL_ACTIVITY = 'http://connect.garmin.com/proxy/download-service/files/activity/'
  219.  
  220. URL_GC_ACTIVITY_PAGE = 'https://connect.garmin.com/modern/activity/'
  221.  
  222. URL_GC_GEAR_ACTIVITIES_START='https://connect.garmin.com/modern/proxy/activitylist-service/activities/'
  223. #ID goes here
  224. URL_GC_GEAR_ACTIVITIES_END='/gear'
  225.  
  226. print("Logging in...")
  227.  
  228. # Initially, we need to get a valid session cookie, so we pull the login page.
  229. #print('Request login page')
  230. http_req(URL_GC_LOGIN)
  231. #print('Finish login page')
  232.  
  233. # Now we'll actually login.
  234. # Fields that are passed in a typical Garmin login.
  235. POST_DATA = {
  236.     'username': USERNAME,
  237.     'password': PASSWORD,
  238.     'embed': 'true',
  239.     'lt': 'e1s1',
  240.     '_eventId': 'submit',
  241.     'displayNameRequired': 'false'
  242.     }
  243.  
  244. #print('Post login data')
  245. LOGIN_RESPONSE = http_req(URL_GC_LOGIN, urllib.parse.urlencode(POST_DATA)).decode()
  246. #print('Finish login post')
  247.  
  248. # extract the ticket from the login response
  249. PATTERN = re.compile(r".*\?ticket=([-\w]+)\";.*", re.MULTILINE|re.DOTALL)
  250. MATCH = PATTERN.match(LOGIN_RESPONSE)
  251. if not MATCH:
  252.     raise Exception('Did not get a ticket in the login response. Cannot log in. Did \
  253. you enter the correct username and password?')
  254. LOGIN_TICKET = MATCH.group(1)
  255. #print('login ticket=' + LOGIN_TICKET)
  256.  
  257. #print("Request authentication URL: " + URL_GC_POST_AUTH + 'ticket=' + LOGIN_TICKET)
  258. #print("Request authentication")
  259. LOGIN_AUTH_REP = http_req(URL_GC_POST_AUTH + 'ticket=' + LOGIN_TICKET).decode()
  260. #print(LOGIN_AUTH_REP)
  261. #print('Finished authentication')
  262.  
  263.  
  264. SEARCH_PARAMS = {'start': 0, 'limit': LIMIT_ACTIVITY_LIST}
  265.  
  266. #if STARTDATE:
  267. #   SEARCH_PARAMS['startDate'] = STARTDATE
  268. #if ENDDATE:
  269. #   SEARCH_PARAMS['endDate'] = ENDDATE
  270.  
  271. ACLISTURL = URL_GC_GEAR_ACTIVITIES_START + GEARID + URL_GC_GEAR_ACTIVITIES_END + '?' + urllib.parse.urlencode(SEARCH_PARAMS)
  272. #print("Activity list URL: " + ACLISTURL)
  273.  
  274. print('')
  275. print('Searching for activities (this might take a while)...')
  276. print('')
  277. #print('Loading activity list')
  278. ACTIVITY_LIST = http_req(ACLISTURL)
  279.  
  280. #print('Processing activity list')
  281. #print('')
  282.  
  283. JSON_LIST = json.loads(ACTIVITY_LIST)
  284.  
  285. if len(JSON_LIST) == 0:
  286.     print("No activities found for the given date range.")
  287. else:
  288.     print("Found " + str(len(JSON_LIST)) + " activities.")
  289.  
  290. def format_timedelta(td):
  291.     minutes, seconds = divmod(td.seconds + td.days * 86400, 60)
  292.     hours, minutes = divmod(minutes, 60)
  293.     return '{:d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
  294.    
  295. D=0
  296. for a in JSON_LIST:
  297.     print('Activity: ' + a['startTimeLocal'] + (' | ' + a['activityName'] if a['activityName'] else ''))
  298.     print('  Duration: ' + format_timedelta(timedelta(seconds=a['duration'])))
  299.     D += a['duration']    
  300. print('')
  301. print('Total Duration: ' + format_timedelta(timedelta(seconds=D)))
  302. print('')
  303. print('Done!')
  304.  
  305. input('Press ENTER to quit');
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement